Skip to content

Releases: dzerik/melitta-barista-ha

v0.83.0 — Nivona brew-override improvements

22 Jun 07:34

Choose a tag to compare

Nivona brew-override improvements, ported from community testing on real NICR hardware.

Added

  • Brew Water Amount override for Nivona. A new water_amount slider (0–240 mL, default 100) joins the existing strength / coffee-amount / temperature / milk-amount overrides. The field was already supported by the temp-recipe write path; it is now exposed as an entity.
  • Reset Brew Overrides button for Nivona. Clears the user_set flag on every override slider and restores defaults, so the next brew uses the machine's own saved recipe instead of a temp-recipe override.

Fixed

  • Partial temp-recipe writes brewed wrong amounts on Nivona. When any override slider was set, only the changed fields were written, and the firmware filled the omitted fields with hardware defaults (not the saved-recipe values) — silently brewing wrong amounts for the untouched fields. Brew overrides are now all-or-nothing: setting any slider sends the complete set of current slider values; setting none leaves the saved recipe untouched.

v0.82.0 — Freestyle bean hopper selection

22 Jun 07:33

Choose a tag to compare

Added

  • Bean hopper selection in Freestyle (dual-hopper Barista TS) (#31). Each freestyle component can now choose its bean hopper. Two new selects — Freestyle Bean Hopper 1 and Freestyle Bean Hopper 2 (hopper_1 / hopper_2) — plus matching blend1 / blend2 parameters on the brew_freestyle service. This makes it possible to brew, e.g., decaf from hopper 2. The selects are shown on a Barista TS (and when the machine type is not yet known) and hidden on a single-hopper Barista T. Previously the hopper byte was hardcoded to hopper 1, so the second hopper was unreachable over BLE.

Fixed

  • brew_freestyle service raised HTTP 500 on every call. The service handler passed two_cups as a keyword, but MelittaBleClient.brew_freestyle had no such parameter, raising TypeError. The flag is now a keyword-only argument forwarded to the HE double-brew flag in start_process. The freestyle button (single-cup) is unaffected.

v0.81.0 — platform contract foundation

04 Jun 17:21

Choose a tag to compare

Internal architecture refactor. No user-facing behavior change — all entities, brewing, settings, Sommelier work identically.

This release lands the foundation for a transport-agnostic coffee-machine platform: a clean contract that HA entities and the Sommelier consume, decoupled from the concrete BLE client. It bundles three internal phases.

Phase 1a — contract foundation

  • New self-contained coffee_platform/ subpackage with a CoffeeMachineClient Protocol (the transport-agnostic high-level surface) and a MachineRegistry.
  • MelittaBleClient is verified to satisfy the contract via a compliance test.
  • The Sommelier path stopped reaching into the client's private _capabilities attribute — routed through the public capabilities property.
  • sensor.py migrated to the contract as the pilot.

Phase 1b — full consumer migration

  • All remaining consumer files migrated from the concrete MelittaBleClient type hint to the CoffeeMachineClient contract: button, select, number, switch, text, time, binary_sensor, entity (the device mixin base), diagnostics.
  • __init__.py intentionally retains MelittaBleClient — it's the composition root that constructs the concrete client.

Phase 2a — platform owns the domain vocabulary

  • The shared brand-agnostic types — status enums (MachineProcess, SubProcess, InfoMessage, Manipulation), MachineStatus, capability descriptors, MachineCapabilities, BrandProfile, FeatureNotSupported — moved into coffee_platform/domain.py.
  • The old locations (brands/base.py, protocol.py, const.py) are now thin re-export shims, so all existing imports keep working unchanged (zero import-site churn across ~30 sites).
  • The CoffeeMachineClient contract drops its Any placeholders and references the real types.
  • coffee_platform/ is now fully self-contained (stdlib-only, no imports back into the integration) — enforced by an AST test — and ready for extraction to a standalone package.

Tests

New tests/coffee_platform/ suite (contract compliance, private-leak guard, registry behavior, domain self-containment + shim identity). Full suite: 1000 passed (was 992).

Full changelog

v0.79.1 — Docker HA Container troubleshooting (issue #14 follow-ups)

28 May 14:23
4753de5

Choose a tag to compare

Patch release closing issue #14 follow-ups after user verification of v0.74.2.

Added

  • README troubleshooting section for Docker HA Container users. Three host-side prerequisites that are commonly missed and surface as misleading HU handshake timeout / Authentication failed errors:
    • Install bluez package on the host (the full daemon, not just bluez-obexd)
    • Mount /run/dbus:/run/dbus:ro into the container
    • Run with --privileged (or NET_ADMIN) + --net=host
    • Plus a BlueZ cache-reset recipe (bluetoothctl disconnect <MAC> / remove <MAC>)
  • HCL.md: Apple Broadcom BCM2046B1 / BCM20702A0 (USB ID 05ac:828d) graduated to ✅ verified — first 7xx-family + Docker datapoint we have full diagnostic data for (Ubuntu 24.04 / BlueZ 5.72 / HA Container, NICR 779).

Fixed

  • Misleading ble_agent log when system D-Bus is unreachable. Previously said "Assuming ESPHome BLE proxy" — incorrect and misleading for Docker users whose local adapter is present but unreachable due to missing /run/dbus mount. Now explicitly names both valid scenarios (ESPHome proxy = expected; Docker without D-Bus mount = broken setup) and points to README troubleshooting.

Test results

992 tests passing (unchanged from v0.79.0 — pure docs + log message fix).

Credits

Hardware diagnostic data from @sharonovstan-spec in issue #14. README troubleshooting steps come directly from his successful reproduction.

Full changelog · PR #29 · HCL update

v0.79.0 — capability-driven platform refactor

28 May 11:45
6a4fb0a

Choose a tag to compare

Pure-refactor release. No user-facing behavior change.

What changed

The integration now operates as a brand-agnostic platform: HA entity factories and BLE mixins no longer test client.brand.brand_slug == "X" or import directly from brands.<vendor>. All brand-specific behavior flows through the BrandProfile Protocol contract and per-family MachineCapabilities flags.

Adding a new coffee-machine brand (Krups SubLime, Severin, hypothetical 3rd Eugster-OEM) is now a self-contained operation:

  1. Implement the BrandProfile Protocol
  2. Create per-family modules with EXPORTS dicts
  3. Register in brands/__init__.py

No edits to shared layers (sensor.py, button.py, select.py, number.py, _ble_commands.py, _ble_recipes.py) required.

Refactored

  • BrandProfile contract extended with 6 new methods: temp_recipe_type_register (class attr), temp_recipe_register, fluid_write_scale, mycoffee_layout, mycoffee_register, is_chilled_selector. NivonaProfile already implemented these as @staticmethod; MelittaProfile gets stubs returning None / 1 / False. Shared mixins (_ble_commands, _ble_recipes) and sensor.py no longer from .brands.nivona import ….
  • 3 new MachineCapabilities feature flags: supports_factory_reset, supports_brew_overrides, uses_legacy_total_cups_sensor. Set per-family in _family_*.py and in MelittaProfile. Entity factories use these flags instead of brand_slug == "X". Hardcoded _FACTORY_RESET_FAMILIES = {"600", "700", …} frozenset deleted.
  • Redundant gates dropped: button.py:55 / select.py:146 HE-selector brew entities now key off "HC" not in supported_extensions alone (brand_slug half was redundant); number.py:101 / select.py:130-132 settings-entity gates rely on caps.settings being empty for Melitta.

Added

  • docs/BRAND_PROFILE_SPEC.md — contract specification documenting:
    • Required BrandProfile Protocol surface (identity, crypto, family resolution, parse_status, recipe write-path methods)
    • Required MachineCapabilities fields + feature flag conventions
    • Step-by-step guide for adding a new brand
    • Explicit anti-patterns banned in shared layers (brand_slug == checks, direct brands.<vendor> imports, hardcoded family sets, isinstance checks, mutating frozen capabilities)
    • Forward-looking notes on what's NOT in the contract yet (plugin loader, Protocol versioning, capability-coverage tests)

Test infrastructure

Test fixtures across tests/test_button.py, tests/test_init.py, tests/test_sensor.py switched from client.brand = MagicMock() to real MelittaProfile() / NivonaProfile() instances + capabilities_for(family_key). Real BrandProfile gives capability-driven gates actual values; previous MagicMock-truthy false positives caused setup-time TypeErrors. Tests mutating caps.my_coffee_slots directly now use dataclasses.replace().

Full suite: 992 passed (unchanged from v0.78.1).

Full changelog · PR #28 · Contract spec

v0.78.1 — Nivona brand profile refactored into per-family package

28 May 10:39
af9e0ee

Choose a tag to compare

Pure-refactor release. No user-facing behavior changes.

Refactored

brands/nivona.py split into a package. The 1277-line monolith becomes brands/nivona/ with one module per concern and per coffee-machine family:

brands/nivona/
  __init__.py          NivonaProfile + dispatch loop
  _crypto.py           NIVONA_RC4_KEY, NIVONA_HU_TABLE (brand-wide)
  _options.py          Setting-option enums (hardness, auto-off, …)
  _registers.py        RECIPE/TEMP/MYCOFFEE register bases + helpers
  _stats_helpers.py    _count / _pct / _flag factories
  _prefixes.py         serial→family + MODEL_OVERRIDES + EXCLUDE
  _family_600.py       NICR 6xx
  _family_700.py       NICR 7xx + 79x
  _family_900.py       NICR 9xx + 9xx-light
  _family_1030.py      NICR 1030 + 1040
  _family_8000.py      NIVO 8xxx + chilled-brew selectors

Each family module exposes a single EXPORTS = {family_key: {...}} dict; nivona/__init__.py merges them in a loop and slices off per-aspect dispatch tables. Adding a new family is now: drop in _family_NEW.py with an EXPORTS entry, add the module to the loop tuple, register the serial-prefix in _prefixes.py.

External imports through brands.nivona (NivonaProfile, mycoffee_layout, mycoffee_register, MY_COFFEE_BASE_REGISTER, MY_COFFEE_SLOT_STRIDE, TEMP_RECIPE_TYPE_REGISTER) are preserved unchanged.

Fixed

NivonaProfile.parse_status lazy-import regression. Its from ..const import … / from ..protocol import MachineStatus referenced the wrong relative path after the file moved one level deeper into the new package, so any HX status frame would have crashed with ModuleNotFoundError. Re-pointed at ...const / ...protocol and added two regression tests pinning the 8000 (process=3 → READY) and 700 (process=11 → PRODUCT) process-code dispatch. The previous test suite never exercised parse_status at all.

Tests

  • 992 passed (was 990 on v0.78.0).
  • 2 new regression tests in tests/test_brands.py for parse_status.

Files

  • 1 file deleted (brands/nivona.py), 11 files added (brands/nivona/*.py).
  • __init__.py is now 368 lines; the largest family module is _family_1030.py at 248 lines.

Full changelog · PR #27

v0.78.0 — Factory reset + MyCoffee read/brew for Nivona

28 May 09:37
2c387de

Choose a tag to compare

Five merged PRs bundled into one release. Focused on Nivona feature parity — factory-reset buttons and the first end-to-end MyCoffee read+brew path. Melitta installs see no functional change.

Added

Factory reset buttons (Nivona)

Two new button entities under entity_category=config, device_class=restart (HA dashboards surface a confirm dialog):

  • Factory Reset Settings — sends HE with commandId = 50 (HE_CMD_FACTORY_RESET_SETTINGS); wipes machine-wide settings.
  • Factory Reset Recipes — sends HE with commandId = 51 (HE_CMD_FACTORY_RESET_RECIPES); wipes per-recipe customizations.

Available on families 600 / 700 / 79x / 900 / 900-light / 1030 / 1040. NIVO 8000 hidden — the vendor app has no factory-reset menu on that family, so we mirror the gating. Melitta brand gets no factory-reset buttons (Melitta uses its own per-register HD reset path).

New generic execute_command(command_id) / execute_he_command(command_id) path on EugsterProtocol / BleCommandsMixin for future HE-with-commandId actions.

MyCoffee bulk read (Nivona)

On every connect, after capability resolution, the client now bulk-reads MyCoffee slot params and caches them on MelittaBleClient.my_coffee_slots. Per-(slot, param) MyCoffee slot N <param> diagnostic sensors expose the cached values. Sensors stay unavailable until the first bulk read completes.

Params read per slot, gated on whether the family's MyCoffee layout defines the corresponding offset:

Param Type Notes
coffee_amount int (vendor units, typically ml) every family
water_amount int every family
milk_amount int every family except 600 (no offset on 600)
milk_foam_amount int every family
enabled 0/1 flag every family — used as the "armed" gate for brew buttons
strength code 1..N every family
temperature code 0..3 single-byte families only (600 / 700 / 79x / 8000); 900 / 1030 / 1040 use per-fluid temperature offsets and skip this param

Sensor counts per family (all under EntityCategory.DIAGNOSTIC):

Family Slots Params Sensors
NIVO 8000 9 7 63
NICR 1040 18 6 (no temperature) 108
NICR 1030 18 6 108
NICR 700 / 79x / 900 / 900-light per-model 6–7 up to 63
NICR 600 1–5 (per-model) 6 (no milk_amount) up to 30

Brew MyCoffee slot N buttons (Nivona)

One button per slot 0..N-1, display name "Brew MyCoffee slot 1..N" (1-indexed). Pressing slot M sends:

HE.payload[3] = caps.first_mycoffee_selector + M   (vendor base 20)
HE.payload[5] = 0                                  (use saved recipe — no temp-recipe write)

Availability gated on:

  1. client.connected
  2. status.is_ready_for_brew(tolerated_brew_manipulations)
  3. The slot's cached enabled flag == 1 — so unconfigured slots stay unavailable instead of firing an empty recipe

Tests

  • 8 new tests for the factory-reset path (tests/test_protocol.py::TestExecuteCommand + tests/test_button.py).
  • 6 new tests for MyCoffee bulk read (tests/test_ble_client.py::TestReadMyCoffeeSlots) + 4 sensor-registration tests (tests/test_sensor.py).
  • 5 new tests for the brew-by-slot path (tests/test_ble_client.py::TestBrewMyCoffeeSlot) + 3 button tests.
  • Full suite: 990 passed (was 964 on v0.77.1).

What's intentionally NOT in this release

  • Sommelier on Nivona — auto-brew from a Sommelier-generated recipe still requires brew_freestyle (Melitta HJ); enabling on Nivona needs a recipe-shape mapper to Nivona temp-recipe overrides.
  • MyCoffee writes — read-only this release. Write support (edit slot params, set name) comes later, behind family-aware encoding for the name field.
  • Card adaptation for Nivona recipes-tab — pending UI work.
  • NICR 1040 HotMilk slot 8 — known recipe-layout discrepancy, pending hardware confirmation from a 1040 owner.

🤖 Generated with Claude Code

v0.77.1 — first_mycoffee_selector capability

28 May 07:16
432fb02

Choose a tag to compare

PR C from the protocol-verification plan. Pure additive change preparing the ground for future MyCoffee brew-by-slot plumbing.

Added

  • `MachineCapabilities.first_mycoffee_selector` (default `20`) — base brew-selector for MyCoffee slots. To brew MyCoffee slot N, the HE command needs `payload[3] = first_mycoffee_selector + N`. The vendor protocol uses 20 for every Nivona model. The constant lives on the capability bag now instead of being a hardcoded magic number scattered through future call sites.

No consumer is wired up in this release; this is foundational work for a later MyCoffee CRUD release.

Tests

  • 1 new test in `tests/test_brands.py` locking the default at 20 for all eight Nivona families.
  • Full suite: 964 passed (was 963 on v0.77.0).

🤖 Generated with Claude Code

v0.77.0 — Total Cups fix on Nivona + stats alignment

28 May 07:08
b714b5e

Choose a tag to compare

Closes the remaining bug reported in #15.

Fixed

  • Total Cups sensor no longer registers on Nivona. The legacy MelittaTotalCupsSensor reads HR id 150 — a Melitta-specific register that doesn't exist on Nivona, so the sensor stayed unknown forever (reported in #15 for NIVO 8101). Nivona users see the equivalent via the capability-driven total_beverages stat sensor (id 213 on 8000-family, id 215 on 1030-family). Melitta installs unchanged.

Changed

  • Stat slugs renamed to align with vendor terminology:

    • 8000 family, id 206: warm_milkhot_milk (vendor labels this counter "Heisse Milch" / hot milk).
    • 1030/1040 family, id 201: lungocoffee (vendor labels this counter "Coffee", not Lungo).

    Existing entity registry entries migrate automatically — HA's long-term statistics follow the rename.

  • 1030/1040 id 224 beverages_via_kanne title prefixed (experimental) — register not confirmed by vendor reference data; kept until field-confirmed.

Added — new diagnostic sensors for NIVO 8000 family

All four are read-only HR register polls from the extended vendor register set:

  • id 211 grinding_count ("Anz_Mahlung")
  • id 212 reserve_count ("Anz_Reserve")
  • id 602 descale_status ("Entkalken_Status")
  • id 630 frother_rinse_needed ("SpuelenAufsch_Notwendig")

Added — 1030/1040 family

  • id 210 my_coffee ("Anz_Bezuege_MyCoffee") — was missing entirely.

Migration

Config entry version 2 → 3. async_migrate_entry handles the renames; restart HA after upgrade and history continues seamlessly.

Tests

  • 5 new tests in tests/test_brands.py (sizes + register-set alignment) and tests/test_sensor.py (brand gating).
  • 3 new tests in tests/test_init.py::TestMigrateEntryV2ToV3 covering both renames + Melitta no-op.
  • Full suite: 963 passed (was 956 on v0.76.1).

🤖 Generated with Claude Code

v0.76.1 — HU handshake response verification

28 May 06:45
99d3fd5

Choose a tag to compare

PR A from the protocol-verification plan. Zero hardware-risk; pure validation tightening.

Fixed

  • HU handshake response is now validated end-to-end. The response handler now requires the 8-byte response to:

    • have payload[0:4] echo the random seed we sent
    • have payload[6:8] equal hu_verifier(payload[0:6]) (re-derived from our brand's HU table)

    Any mismatch is logged at WARNING and the handshake fails fast — _key_prefix stays None and _handshake_done is set so perform_handshake returns False immediately instead of hanging until the frame-timeout.

    Previously the handler trusted whatever the machine sent: a corrupted / mismatched response would silently install a junk session key, and every subsequent RC4-encrypted frame would decrypt to gibberish with no obvious log entry pointing back at the handshake.

Tests

  • 4 new tests in tests/test_protocol.py::TestHandshakeResponseVerification covering happy path + 3 reject paths.
  • Existing handshake tests updated for the new "event set on reject" contract.
  • 956 passed (was 952 on v0.76.0).

🤖 Generated with Claude Code