Releases: dzerik/melitta-barista-ha
v0.83.0 — Nivona brew-override improvements
Nivona brew-override improvements, ported from community testing on real NICR hardware.
Added
- Brew Water Amount override for Nivona. A new
water_amountslider (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_setflag 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
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 matchingblend1/blend2parameters on thebrew_freestyleservice. 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_freestyleservice raised HTTP 500 on every call. The service handler passedtwo_cupsas a keyword, butMelittaBleClient.brew_freestylehad no such parameter, raisingTypeError. The flag is now a keyword-only argument forwarded to the HE double-brew flag instart_process. The freestyle button (single-cup) is unaffected.
v0.81.0 — platform contract foundation
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 aCoffeeMachineClientProtocol (the transport-agnostic high-level surface) and aMachineRegistry. MelittaBleClientis verified to satisfy the contract via a compliance test.- The Sommelier path stopped reaching into the client's private
_capabilitiesattribute — routed through the publiccapabilitiesproperty. sensor.pymigrated to the contract as the pilot.
Phase 1b — full consumer migration
- All remaining consumer files migrated from the concrete
MelittaBleClienttype hint to theCoffeeMachineClientcontract:button,select,number,switch,text,time,binary_sensor,entity(the device mixin base),diagnostics. __init__.pyintentionally retainsMelittaBleClient— 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 intocoffee_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
CoffeeMachineClientcontract drops itsAnyplaceholders 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).
v0.79.1 — Docker HA Container troubleshooting (issue #14 follow-ups)
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 failederrors:- Install
bluezpackage on the host (the full daemon, not justbluez-obexd) - Mount
/run/dbus:/run/dbus:rointo the container - Run with
--privileged(orNET_ADMIN) +--net=host - Plus a BlueZ cache-reset recipe (
bluetoothctl disconnect <MAC>/remove <MAC>)
- Install
HCL.md: Apple Broadcom BCM2046B1 / BCM20702A0 (USB ID05ac: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_agentlog 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/dbusmount. 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.
v0.79.0 — capability-driven platform refactor
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:
- Implement the
BrandProfileProtocol - Create per-family modules with
EXPORTSdicts - 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 returningNone/1/False. Shared mixins (_ble_commands,_ble_recipes) andsensor.pyno longerfrom .brands.nivona import …. - 3 new MachineCapabilities feature flags:
supports_factory_reset,supports_brew_overrides,uses_legacy_total_cups_sensor. Set per-family in_family_*.pyand in MelittaProfile. Entity factories use these flags instead ofbrand_slug == "X". Hardcoded_FACTORY_RESET_FAMILIES = {"600", "700", …}frozenset deleted. - Redundant gates dropped:
button.py:55/select.py:146HE-selector brew entities now key off"HC" not in supported_extensionsalone (brand_slug half was redundant);number.py:101/select.py:130-132settings-entity gates rely oncaps.settingsbeing empty for Melitta.
Added
docs/BRAND_PROFILE_SPEC.md— contract specification documenting:- Required
BrandProfileProtocol surface (identity, crypto, family resolution, parse_status, recipe write-path methods) - Required
MachineCapabilitiesfields + feature flag conventions - Step-by-step guide for adding a new brand
- Explicit anti-patterns banned in shared layers (
brand_slug ==checks, directbrands.<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)
- Required
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).
v0.78.1 — Nivona brand profile refactored into per-family package
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.pyforparse_status.
Files
- 1 file deleted (
brands/nivona.py), 11 files added (brands/nivona/*.py). __init__.pyis now 368 lines; the largest family module is_family_1030.pyat 248 lines.
v0.78.0 — Factory reset + MyCoffee read/brew for Nivona
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:
client.connectedstatus.is_ready_for_brew(tolerated_brew_manipulations)- The slot's cached
enabledflag == 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
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
Closes the remaining bug reported in #15.
Fixed
- Total Cups sensor no longer registers on Nivona. The legacy
MelittaTotalCupsSensorreads HR id 150 — a Melitta-specific register that doesn't exist on Nivona, so the sensor stayedunknownforever (reported in #15 for NIVO 8101). Nivona users see the equivalent via the capability-driventotal_beveragesstat 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_milk→hot_milk(vendor labels this counter "Heisse Milch" / hot milk). - 1030/1040 family, id 201:
lungo→coffee(vendor labels this counter "Coffee", not Lungo).
Existing entity registry entries migrate automatically — HA's long-term statistics follow the rename.
- 8000 family, id 206:
-
1030/1040 id 224
beverages_via_kannetitle 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) andtests/test_sensor.py(brand gating). - 3 new tests in
tests/test_init.py::TestMigrateEntryV2ToV3covering 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
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]equalhu_verifier(payload[0:6])(re-derived from our brand's HU table)
Any mismatch is logged at WARNING and the handshake fails fast —
_key_prefixstaysNoneand_handshake_doneis set soperform_handshakereturnsFalseimmediately 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.
- have
Tests
- 4 new tests in
tests/test_protocol.py::TestHandshakeResponseVerificationcovering 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