Skip to main content

Python library and CLI for controlling Breville/Sage coffee machines

Project description

Sage Coffee Control Library

Python library and CLI for controlling Breville/Sage coffee machines via their cloud API.

Security Warning: This library handles authentication tokens that provide access to your coffee machine. Keep your tokens secure and never share them publicly. The refresh token in particular can be used to generate new access tokens indefinitely.

Features

  • OAuth authentication against Breville/Sage Auth0
  • Automatic token refresh when tokens expire
  • WebSocket connection for real-time state updates
  • REST API for sending commands (wake, sleep, etc.)
  • Appliance discovery (no need to hard-code serial numbers)
  • CLI tool (sagectl) for command-line control
  • Clean async Python API for integration into other projects

Installation

# From source
git clone https://github.com/simonjgreen/sagecoffee.git
cd sagecoffee
pip install -e .

# With development dependencies
pip install -e ".[dev]"

Quick Start

1. Bootstrap Authentication

First, you need to authenticate with your Breville account to obtain a refresh token:

# Bootstrap will prompt for your password (never stored)
sagectl bootstrap --username your.email@example.com

This stores your refresh token in ~/.config/sagecoffee/config.toml with secure permissions (0600).

2. List Your Appliances

sagectl appliances

Output:

┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Name                ┃ Model  ┃ Serial Number       ┃ Pairing Type ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ Oracle Dual Boiler  │ BES995 │ nnnnnnnnnnnnnnnnn   │ wifi         │
└─────────────────────┴────────┴─────────────────────┴──────────────┘

3. Control Your Machine

# Wake up the machine
sagectl wake

# Put the machine to sleep
sagectl sleep

# Wake and wait until ready (with timeout)
sagectl wake --wait --timeout 300

4. Monitor State

# Stream state updates (press Ctrl+C to stop)
sagectl tail

# Get a single state snapshot
sagectl tail --once

# Output as JSON lines (for scripting)
sagectl tail --jsonl

# Show specific fields
sagectl tail --fields state,boiler

Environment Variables

All configuration can be provided via environment variables:

Variable Description
SAGECOFFEE_CLIENT_ID OAuth client ID (has default, rarely needed)
SAGECOFFEE_REFRESH_TOKEN Refresh token (or use config file)
SAGECOFFEE_SERIAL Appliance serial number (optional, auto-discovered)
SAGECOFFEE_MODEL Appliance model (default: BES995)
SAGECOFFEE_APP App identifier (default: sageCoffee)

Configuration File

The config file is stored at ~/.config/sagecoffee/config.toml:

refresh_token = "your_refresh_token"
serial = "nnnnnnnnnnnnnnn"
model = "BES995"
app = "sageCoffee"

Important: The config file contains sensitive tokens. It is created with restrictive permissions (0600). The CLI will warn you if permissions are too permissive.

Python Library Usage

Basic Usage

import asyncio
from sagecoffee import SageCoffeeClient

async def main():
    async with SageCoffeeClient(
        client_id="your_client_id",
        refresh_token="your_refresh_token",
    ) as client:
        # List appliances
        appliances = await client.list_appliances()
        print(f"Found {len(appliances)} appliances")
        
        # Wake up the machine
        await client.wake()
        
        # Stream state updates
        async for state in client.tail_state():
            print(f"State: {state.reported_state}")
            if state.reported_state == "ready":
                break

asyncio.run(main())

Using ConfigStore

from sagecoffee import SageCoffeeClient
from sagecoffee.store import ConfigStore

async def main():
    store = ConfigStore()
    async with SageCoffeeClient.from_config(store) as client:
        await client.wake()

Low-Level API Access

from sagecoffee.auth import AuthClient
from sagecoffee.http_api import BrevilleApiClient
from sagecoffee.ws_client import BrevilleWsClient

# Direct auth client usage
auth = AuthClient(client_id="...")
tokens = await auth.password_realm_login(username, password)
tokens = await auth.refresh(tokens.refresh_token)

# Direct HTTP API usage
api = BrevilleApiClient(get_id_token=lambda: tokens.id_token)
await api.wake("SERIAL123")

# Direct WebSocket usage
ws = BrevilleWsClient(get_id_token=lambda: tokens.id_token)
await ws.connect()
await ws.add_appliance("SERIAL123")
async for state in ws.listen_states():
    print(state.reported_state)

Token Refresh Logic

The library automatically handles token refresh:

  1. Expiry Detection: Tokens are checked against their JWT exp claim, with a configurable skew (default 60 seconds before expiry).

  2. Automatic Refresh: When a token is near expiry:

    • The TokenManager refreshes it using the stored refresh_token
    • An asyncio lock prevents concurrent refresh stampedes
    • The new refresh_token is persisted if it was rotated
  3. 401 Handling: If an API call returns 401:

    • The token is refreshed
    • The request is retried once
  4. WebSocket Reconnection: If the WebSocket disconnects:

    • Exponential backoff with jitter
    • Token refresh before reconnect if near expiry
    • Automatic re-registration of appliances

State Model

The DeviceState object provides convenient access to appliance state:

state.reported_state      # "asleep", "warming", "ready", etc.
state.desired_state       # What state was requested
state.boiler_temps        # List of BoilerState objects
state.grind_size          # Grinder setting
state.is_remote_wake_enabled  # Whether remote wake is allowed
state.timezone            # Configured timezone
state.raw_data            # Full raw state dict

Machine Configuration Defaults

Default settings discovered from a BES995 (Oracle Dual Boiler):

Setting Default Notes
Grind size 19 Range ~1-45
Brew temp 93.3°C Boiler ID 1
Steam temp 131°C Boiler ID 0
Volume 50 0-100
Display brightness 50 0-100
Work light brightness 100 Cup warmer light, 0-100
Auto-off time 20 min Idle time before sleep
Temp unit 0 0=Celsius, 1=Fahrenheit
Theme "dark" Display theme
Timezone "Europe/London" Machine timezone
Wake schedule 20 6 * * 1-5 6:20 AM weekdays (cron format)

State Report Structure

The WebSocket returns state reports with this structure:

{
  "serialNumber": "nnnnnnnnnnnnnnnn",
  "messageType": "stateReport",
  "data": {
    "desired": { "REQUESTID": "None", "REQUEST": "None" },
    "reported": {
      "state": "ready",
      "cfg": {
        "default": {
          "work_light_brightness": 100,
          "brightness": 50,
          "theme": "dark",
          "vol": 50,
          "idle_time": 20,
          "wake_schedule": [{"cron": "20 6 * * 1-5", "on": true}],
          "temp_unit": 0,
          "timezone": "Europe/London"
        }
      },
      "grind": { "size_setting": 19 },
      "boiler": [
        { "id": 0, "temp_sp": 131, "cur_temp": 100 },
        { "id": 1, "temp_sp": 93.3, "cur_temp": 93.3 }
      ],
      "firmware": { "mcu0": "1.0.13", "appVersion": "1.1.20" }
    }
  }
}

CLI Commands Reference

Command Description
sagectl bootstrap Authenticate and store refresh token
sagectl refresh Manually refresh tokens
sagectl appliances List discovered appliances
sagectl tail Stream state updates
sagectl wake Wake up appliance
sagectl sleep Put appliance to sleep
sagectl config Show configuration
sagectl raw ws '...' Send raw WebSocket message
sagectl raw http METHOD /path '...' Send raw HTTP request

Common Options

  • --serial, -s: Specify appliance serial number
  • --debug: Enable debug logging (with redacted secrets)
  • --wait, -w: Wait for state transition (wake/sleep)
  • --timeout, -t: Timeout for wait operations
  • --jsonl: Output as JSON lines
  • --once: Exit after first state report

Exit Codes

Code Meaning
0 Success
1 Authentication failure
2 Configuration missing
3 Network failure
4 Timeout waiting for state

Development

# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run tests with coverage
pytest --cov=sagecoffee

# Type checking
mypy src/sagecoffee

# Linting
ruff check src/sagecoffee

# Formatting
black src/sagecoffee tests

Security Considerations

  1. Never commit tokens: Add config.toml to your .gitignore
  2. Use environment variables in CI: Don't store tokens in code
  3. Restrictive file permissions: The library enforces 0600 on config files
  4. Token redaction: Debug logs automatically redact sensitive values
  5. No password storage: The bootstrap command never stores your password

Known Endpoints

The library uses these Breville/Sage cloud endpoints:

  • OAuth: https://my.breville.com/oauth/token
  • REST API: https://iot-api.breville.com/appliance/v1/...
  • User API: https://iot-api.breville.com/user/v2/...
  • WebSocket: wss://iot-api-ws.breville.com/applianceProxy

License

MIT License - see LICENSE file for details.

Disclaimer

This is an unofficial library. It is not affiliated with, endorsed by, or supported by Breville or Sage. Use at your own risk.

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

sagecoffee-0.2.0rc1.tar.gz (34.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sagecoffee-0.2.0rc1-py3-none-any.whl (31.1 kB view details)

Uploaded Python 3

File details

Details for the file sagecoffee-0.2.0rc1.tar.gz.

File metadata

  • Download URL: sagecoffee-0.2.0rc1.tar.gz
  • Upload date:
  • Size: 34.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for sagecoffee-0.2.0rc1.tar.gz
Algorithm Hash digest
SHA256 bdb883f80aa87f01dbda3c6606a6060cef173ce06ba9e3b78b7b1c9fb7400739
MD5 d905a8566ec3a3102c89db9e2a02d684
BLAKE2b-256 16cb8c05c64a2a360aa884739f10526e62ecbf6dd79eb2a18e38afb6575d7332

See more details on using hashes here.

Provenance

The following attestation bundles were made for sagecoffee-0.2.0rc1.tar.gz:

Publisher: publish.yml on simonjgreen/sagecoffee

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sagecoffee-0.2.0rc1-py3-none-any.whl.

File metadata

  • Download URL: sagecoffee-0.2.0rc1-py3-none-any.whl
  • Upload date:
  • Size: 31.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for sagecoffee-0.2.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 7fe654103c31edb4b4ef5e90d784d2a075debec599d6d33ecf20e0e4d5e9d591
MD5 ef2fca0cacf171abab11c1d9ce02a125
BLAKE2b-256 d423c6537158af77a58b8b0b42d5cecd17b1e9c7846ebae694adf797794b5a05

See more details on using hashes here.

Provenance

The following attestation bundles were made for sagecoffee-0.2.0rc1-py3-none-any.whl:

Publisher: publish.yml on simonjgreen/sagecoffee

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page