A Python library for interacting with Jura coffee machines over Bluetooth
Project description
py-jura
Python library for controlling JURA coffee machines over Bluetooth
Brew drinks, check machine status, read maintenance counters, and track live brew progress - all from Python, over Bluetooth, with a simple async API.
[!NOTE] Hardware testing has only been performed on a JURA E8 (EF533, firmware EF533M V01.18). All other machine families should work, but are untested. Feedback and bug reports from other models are very welcome.
Installation
pip install py-jura
# or with uv
uv add py-jura
Requires Python 3.11+ and a Bluetooth adapter supported by bleak.
Quick start
import asyncio
from py_jura import JuraMachine, MachineBlockedError, Product
async def main():
async with JuraMachine("AA:BB:CC:DD:EE:FF") as machine:
print(f"Connected to {machine.display_name}") # e.g. "E8"
try:
await machine.brew(Product.ESPRESSO, strength=6, water_ml=40)
except MachineBlockedError as e:
print("Machine not ready:", [a.key for a in e.alerts])
asyncio.run(main())
JuraMachine is an async context manager. On entry it scans for the device, reads the BLE advertisement to identify the machine model and extract the encryption key, then connects and starts a background heartbeat. On exit it sends a graceful disconnect.
Supported machines
| Series | Models |
|---|---|
| E-line | E4, E6, E7, E8 |
| S-line | S8 |
| Z-line | Z6, Z8, Z10 |
| J-line | J6, J8, J10, J8 twin, J10 twin |
| X-line | X4, X6, X8, X10, X4c, X8c, X10c |
| W-line | W4, W8 |
| C-line | C3, C8, C9 |
| ENA | ENA 4, ENA 5, ENA 8 |
| GIGA | GIGA 5, GIGA 6, GIGA X3/X7/X8/X9/X10, W10 |
| Professional | GIGA X3/X7/X8/X9 Professional, W3 |
| D-line | D4, D6 |
| WE-line | WE6, WE8 |
API reference
JuraMachine(address, max_retries=3)
| Parameter | Description |
|---|---|
address |
BLE address - "AA:BB:CC:DD:EE:FF" on Linux/Windows, UUID string on macOS |
max_retries |
Reconnection attempts before raising MachineDisconnectedError (default 3) |
Identity properties - available immediately after connecting:
| Property | Type | Description |
|---|---|---|
.address |
str |
BLE address passed at construction |
.article_number |
int |
Article number from BLE advertisement |
.display_name |
str |
Customer-facing model name, e.g. "E8" or "GIGA X8" |
async with JuraMachine("AA:BB:CC:DD:EE:FF") as machine:
print(machine.article_number) # 15084
print(machine.display_name) # "E8"
Method summary
| Method | Returns | Description |
|---|---|---|
get_status() |
MachineStatus |
Active alerts and ready state |
brew(product, ...) |
- | Start brewing a product |
cancel_brew() |
- | Cancel the current brew |
get_progress() |
BrewProgress |
Real-time brew state |
get_stats() |
MachineStats |
Lifetime brew counts |
get_daily_stats() |
MachineStats |
Today's brew counts |
get_maintenance() |
MaintenanceStats |
Maintenance counters and wear |
get_about() |
MachineInfo |
Firmware version strings |
lock() / unlock() |
- | Barista mode (touchscreen lock) |
shutdown() |
- | Power off the machine |
get_status() → MachineStatus
Reads the machine's current alert state.
status = await machine.get_status()
print(status.is_ready)
for alert in status.alerts:
print(alert.key, "blocking" if alert.blocking else "info")
| Field | Type | Description |
|---|---|---|
.is_ready |
bool |
True when no blocking alerts are active |
.alerts |
list[Alert] |
Active alerts; each has .key (e.g. "fill_water") and .blocking |
brew(product, *, strength, water_ml, temperature, milk_ml, milk_break_ml) → None
Starts brewing. All option parameters are optional - omitted values use the machine's defaults.
await machine.brew(Product.ESPRESSO)
await machine.brew(Product.ESPRESSO, strength=8, water_ml=35, temperature=Temperature.HIGH)
await machine.brew(Product.CAPPUCCINO, milk_ml=120)
| Parameter | Type | Description |
|---|---|---|
product |
Product |
Product to brew |
strength |
int |
Strength level (valid range is product- and model-specific) |
water_ml |
int |
Water volume in ml |
temperature |
Temperature |
LOW, NORMAL, or HIGH |
milk_ml |
int |
Milk volume in ml |
milk_break_ml |
int |
Milk break duration in ml-equivalent |
Raises: UnsupportedProductError, ValueError (option out of range), MachineBlockedError
cancel_brew() → None
Cancels the in-progress brew.
get_progress() → BrewProgress
Reads the real-time brewing state.
progress = await machine.get_progress()
print(progress.is_idle) # True when idle
print(progress.is_done) # True when product is ready
print(progress.product) # Product enum member, or None
| Field | Description |
|---|---|
.is_idle |
True when state is 0x00 |
.is_done |
True when state is 0x24 (ready) or 0x3E (enjoy) |
.product |
Resolved Product enum member, or None |
.state |
Raw state byte (0x00 idle, 0x21 heating, 0x24 ready, 0x3E enjoy) |
get_stats() → MachineStats
Reads lifetime brew counts.
stats = await machine.get_stats()
print(stats.total_count)
print(stats.product_counts[Product.ESPRESSO])
| Field | Description |
|---|---|
.total_count |
Total brews across all products |
.product_counts |
dict[Product, int]; products with zero brews are omitted |
get_daily_stats() returns the same structure for today's counts only.
get_maintenance() → MaintenanceStats
Reads maintenance counters and wear percentages.
maint = await machine.get_maintenance()
print(maint.counters["cleaning"]) # times performed
print(maint.percentages["decalc"]) # remaining capacity 0–100
| Field | Description |
|---|---|
.counters |
dict[str, int] - how many times each maintenance was performed |
.percentages |
dict[str, int] - remaining capacity 0–100 (0 = overdue, 100 = just done) |
Keys: "cleaning", "decalc", "filter_change", "cappu_rinse", "coffee_rinse", "cappu_clean" (model-dependent).
get_about() → MachineInfo
Reads firmware version strings.
info = await machine.get_about()
print(info.machine_version) # e.g. "J-EF533-V02.04"
print(info.bluefrog_version) # BLE module firmware
lock() / unlock() → None
Locks or unlocks the touchscreen (Barista mode).
shutdown() → None
Sends the machine shutdown command.
Products
Full product list (46 products)
Product |
Drink |
|---|---|
RISTRETTO |
Ristretto |
ESPRESSO |
Espresso |
ESPRESSO_DOPPIO |
Espresso Doppio |
COFFEE |
Coffee |
CAPPUCCINO |
Cappuccino |
ESPRESSO_MACCHIATO |
Espresso Macchiato |
LATTE_MACCHIATO |
Latte Macchiato |
MILK_COFFEE |
Milk Coffee |
MILK_PORTION |
Milk Portion |
MILK_FOAM |
Milk Foam |
FLAT_WHITE |
Flat White |
CAFE_BARISTA |
Café Barista |
LUNGO_BARISTA |
Lungo Barista |
CORTADO |
Cortado |
LONG_BLACK |
Long Black |
AMERICANO |
Americano |
XL_LUNGO |
XL Lungo |
MOCACCINO |
Mocaccino |
RAF_COFFEE |
Raf Coffee |
CHOCOLATE_MILK_FOAM |
Chocolate Milk Foam |
HOT_WATER |
Hot Water |
HOT_WATER_GREEN_TEA |
Hot Water (green tea temp) |
POT |
Pot |
POT_SPEED |
Pot (Speed) |
COFFEE_BIG |
Coffee (large) |
CAPPUCCINO_BIG |
Cappuccino (large) |
MILK_COFFEE_BIG |
Milk Coffee (large) |
LATTE_MACCHIATO_BIG |
Latte Macchiato (large) |
MILK_BIG |
Milk (large) |
HOT_WATER_BIG |
Hot Water (large) |
TWO_RISTRETTI |
2× Ristretto |
TWO_ESPRESSI |
2× Espresso |
TWO_COFFEES |
2× Coffee |
TWO_CAPPUCCINI |
2× Cappuccino |
TWO_MILK_COFFEES |
2× Milk Coffee |
TWO_ESPRESSO_MACCHIATI |
2× Espresso Macchiato |
TWO_LATTE_MACCHIATI |
2× Latte Macchiato |
TWO_MILK_FOAM |
2× Milk Foam |
TWO_MILK_PORTIONS |
2× Milk Portion |
TWO_CAFE_BARISTAS |
2× Café Barista |
TWO_LUNGO_BARISTAS |
2× Lungo Barista |
TWO_LUNGOS |
2× Lungo |
TWO_CORTADOS |
2× Cortado |
TWO_ESPRESSI_ENA |
2× Espresso (ENA) |
TWO_COFFEES_ENA |
2× Coffee (ENA) |
TWO_FLAT_WHITES |
2× Flat White |
Which products are available depends on the machine model. Attempting to brew an unsupported product raises UnsupportedProductError.
ARTICLE_NAMES
A dict[int, str] mapping every article number to its customer-facing model name. Useful for scanning and discovery flows where you don't connect to the machine yet.
from py_jura import ARTICLE_NAMES
ARTICLE_NAMES[15084] # "E8"
ARTICLE_NAMES[15234] # "E7"
Exceptions
| Exception | When raised |
|---|---|
MachineNotFoundError |
Device not found, or unrecognised article number |
MachineDisconnectedError |
Not connected, or reconnection failed after max_retries |
MachineBlockedError |
Blocking alerts active; brew not started - check .alerts |
UnsupportedProductError |
Product not available on this machine model |
JuraError |
Base class for all py-jura exceptions |
Development
git clone https://github.com/OpenDisplay-org/py-jura.git
cd py-jura
uv sync --all-extras
uv run pytest tests/ # unit tests
uv run pytest tests/ --cov=src/py_jura # with coverage
uv run pytest -m hardware # hardware tests (real machine required)
uv run ruff check .
uv run mypy src/py_jura
Contributing
Contributions are welcome. Please open an issue before starting significant work so we can align on approach. Run tests and linting before submitting a PR.
Acknowledgements
The BLE protocol implementation is based on reverse-engineering work by the Jutta-Proto project, specifically protocol-bt-cpp - a C++ JURA protocol implementation released under GPL-3.0.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_jura-0.1.0.tar.gz.
File metadata
- Download URL: py_jura-0.1.0.tar.gz
- Upload date:
- Size: 107.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2090b045e8195acb2a76a3d891ad9dcda9333521e4f8573c70e6ac5db2f2d86c
|
|
| MD5 |
e90496f23e52fca1d048b69044921dec
|
|
| BLAKE2b-256 |
b1f4af85b9d4ff7c9872b257740df19ff7804c68ad19f352b6abdf8235c56370
|
Provenance
The following attestation bundles were made for py_jura-0.1.0.tar.gz:
Publisher:
release.yml on g4bri3lDev/py-jura
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_jura-0.1.0.tar.gz -
Subject digest:
2090b045e8195acb2a76a3d891ad9dcda9333521e4f8573c70e6ac5db2f2d86c - Sigstore transparency entry: 1041648088
- Sigstore integration time:
-
Permalink:
g4bri3lDev/py-jura@ec654ecfb81ea541f5be518536de5e3f865ac79d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/g4bri3lDev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ec654ecfb81ea541f5be518536de5e3f865ac79d -
Trigger Event:
push
-
Statement type:
File details
Details for the file py_jura-0.1.0-py3-none-any.whl.
File metadata
- Download URL: py_jura-0.1.0-py3-none-any.whl
- Upload date:
- Size: 164.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fc245e6441f8b4e43f336a3f03d796568f2a9ebac5a7dc77ccd3f0dce3d8c66
|
|
| MD5 |
e9bd17148b0f0486cb2bbde9d3997cb4
|
|
| BLAKE2b-256 |
f8a50224cabfc2d4f256cc24f8ad6e7a3192da2119a76c4dd3a5ab8da5195f77
|
Provenance
The following attestation bundles were made for py_jura-0.1.0-py3-none-any.whl:
Publisher:
release.yml on g4bri3lDev/py-jura
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_jura-0.1.0-py3-none-any.whl -
Subject digest:
2fc245e6441f8b4e43f336a3f03d796568f2a9ebac5a7dc77ccd3f0dce3d8c66 - Sigstore transparency entry: 1041648145
- Sigstore integration time:
-
Permalink:
g4bri3lDev/py-jura@ec654ecfb81ea541f5be518536de5e3f865ac79d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/g4bri3lDev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ec654ecfb81ea541f5be518536de5e3f865ac79d -
Trigger Event:
push
-
Statement type: