Skip to main content

fellow-stagg-ble

CI

Async Python library for the Fellow Stagg EKG+ electric kettle over Bluetooth LE, built on bleak and bleak-retry-connector.

It keeps one connection to the kettle open, decodes the state the kettle streams (power, target and current temperature, unit, hold, auto-off countdown, on/off base) into immutable KettleState snapshots, and sends power and temperature commands. Reconnecting, scanning and scheduling are left to the caller, so the library fits a Home Assistant coordinator as well as a script.

Installation

pip install fellow-stagg-ble

Requires Python 3.12 or newer.

Usage

import asyncio

from bleak import BleakScanner

from fellow_stagg_ble import FellowStaggKettle, KettleState, TemperatureUnit


def on_state(state: KettleState) -> None:
    print(f"{state.current_temperature} -> {state.target_temperature} °{state.unit}")


def on_disconnect() -> None:
    print("connection lost")


async def main() -> None:
    device = await BleakScanner.find_device_by_address("AA:BB:CC:DD:EE:FF")
    if device is None:
        raise SystemExit("kettle not found; lift it or press a button so it advertises")

    kettle = FellowStaggKettle(device, state_callback=on_state, disconnected_callback=on_disconnect)
    await kettle.connect()  # returns once power, target and current temperature have arrived
    print(kettle.state)

    await kettle.set_target_temperature(195, TemperatureUnit.FAHRENHEIT)
    await kettle.set_power(True)

    unsubscribe = kettle.register_callback(lambda s: print("hold:", s.hold))
    await asyncio.sleep(30)
    unsubscribe()

    await kettle.disconnect()


asyncio.run(main())

API

Member Notes
FellowStaggKettle(ble_device, *, state_callback=None, disconnected_callback=None) Takes a bleak.backends.device.BLEDevice.
await connect(*, state_timeout=5.0) Connects with bleak-retry-connector, subscribes, authenticates and waits for a complete state. Idempotent while connected. state_timeout=None returns right after authentication.
await disconnect() Closes the connection without invoking disconnected_callback.
await set_power(on)
await set_target_temperature(value, unit) ValueError outside 40–100 °C / 104–212 °F, before any I/O.
state Latest KettleState; kept across reconnects.
connected, last_frame_at, address, name last_frame_at is time.monotonic() of the last notification on the current connection.
set_ble_device(ble_device) Device for the next connect(); the open connection is untouched.
register_callback(cb) Adds a state callback; returns a function that removes it.
is_fellow_stagg(name, service_uuids) Advertisement filter: service UUID or FELLOW name prefix.

Callbacks fire only when the state changed. Exceptions raised by callbacks are logged and swallowed. Loss of an established connection is reported exactly once through disconnected_callback, whether bleak noticed it or a failed write did; a failed write also raises FellowStaggCommandError and drops the connection. Commands never connect on their own.

KettleState.complete is true once power, target temperature and unit are known; current_temperature is None both before the first reading and while the kettle reports none (off or lifted), so it is not part of the check. connect() itself waits for an actual current-temperature frame.

Exceptions: FellowStaggError is the base of FellowStaggConnectionError, FellowStaggNotSupportedError (characteristic missing), FellowStaggCommandError and FellowStaggTimeoutError.

Protocol

Item Value
Service 00001820-0000-1000-8000-00805f9b34fb
Characteristic 00002a80-0000-1000-8000-00805f9b34fb, properties write-without-response, notify
Advertised name FELLOW + 4 hex digits, e.g. FELLOW46B9
Init sequence ef dd 0b 30 31 32 33 34 35 36 37 38 39 30 31 32 33 34 9a 6d; written after subscribing, the kettle then streams state at about one frame per second per field
Frame ef dd + type byte + payload; notification boundaries do not match frame boundaries
0x00 power [on]
0x01 hold button [on] (slider position; pulses at the setpoint)
0x02 target temperature [temp, unit], unit 1 = °F, else °C; values outside the limits are ignored
0x03 current temperature [temp, unit]; temp = 0x20 (32 °F) or 0 (0 °C) means no reading
0x04 auto-off countdown [lo, hi], 16-bit little-endian seconds (3600 with hold, 300 without)
0x05, 0x07 constants ff ff ff ff and 00 00 00, ignored
0x06 hold engaged [on]
0x08 position [on_base, x, y], 3 bytes, 0 = lifted; the ~11-byte 0x08 echo of the init sequence is ignored
Command ef dd 0a <seq> <type> <value> <(seq + value) & 0xff> <type>; type 0 power (0/1), type 1 target temperature in the kettle's unit; seq wraps at 255
Limits 40–100 °C, 104–212 °F

The pure protocol helpers live in fellow_stagg_ble.protocol (split_frames, parse_frame, build_command) and have no Bluetooth dependency.

Limitations

  • The kettle accepts a single central at a time. While the Fellow app is connected, other clients cannot connect.
  • The kettle stops advertising roughly three minutes after it was last used. It still accepts a connection if the caller has a cached BLEDevice, but a fresh scan will not find it until it is lifted or a button is pressed.
  • A power cycle resets the setpoint to 212 °F.
  • While the kettle is off its base it intermittently reports temperatures in the other unit (a kettle set to °F sends 91 °C frames in between 196 °F ones). The library ignores unit changes while the kettle is lifted and otherwise accepts one only once two consecutive frames report it, so a unit change made on the base shows up about a second late.
  • A second disconnect about 27 s after the kettle is powered up has been observed; callers should expect to reconnect.
  • Writes are spaced at least 0.2 s apart.

Development

uv sync --all-groups
uv run pytest --cov=fellow_stagg_ble --cov-report=term-missing
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv build

Tests run against a fake BleakClient; no hardware is needed.

Releasing

  1. Bump version in pyproject.toml and __version__ in src/fellow_stagg_ble/__init__.py, move the CHANGELOG.md entry out of unreleased, commit.
  2. Tag vX.Y.Z and push the tag. The publish workflow builds the sdist and wheel, uploads them to PyPI through Trusted Publishing and creates a GitHub Release with the files attached.

Trusted Publishing for the pypi environment of this repository is pending setup on PyPI.

Attribution and license

MIT, see LICENSE. The command frame format and init sequence descend from Levi McCallum's MIT-licensed stagg-ekg-plus-ha integration. The Home Assistant integration that consumes this library lives at ADobin/stagg-ekg-plus-ha.

Metadata

Release files for fellow-stagg-ble 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fellow-stagg-ble 0.1.2
File Size Uploaded
fellow_stagg_ble-0.1.2.tar.gz 19.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fellow-stagg-ble 0.1.2
File Interpreter ABI Platform
fellow_stagg_ble-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 33.8 kB

Release files / fellow_stagg_ble-0.1.2.tar.gz

Download URL fellow_stagg_ble-0.1.2.tar.gz
Size 19.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ac6aa357fe65ea6d6a955960108406977729ba86f651c7b2a25b965ed51f715f
BLAKE2b-256 checksum
How to use checksums
157d8b28525a3bbddd6bde6bd44dac0c908aced0e9b728209c60331fc9802023
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.

Transparency log

Release files / fellow_stagg_ble-0.1.2-py3-none-any.whl

Download URL fellow_stagg_ble-0.1.2-py3-none-any.whl
Size 14.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
45cecbc8bbafd3699143d620f03e1436cb8b3f43c966e04249275b7627c0dd82
BLAKE2b-256 checksum
How to use checksums
09cdd4f965e8c50432bb2ec767b84c0ced639ea9496224e3e4dbddb97296d5c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page