Changelog¶
All notable changes to the Melitta Barista Smart & Nivona HA Integration.
[0.83.0] — 2026-06-22¶
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.
[0.82.0] — 2026-06-22¶
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.
[0.81.1] — 2026-06-10¶
Fixed¶
- Green CI lint. Cleaned up import leftovers from the Phase 2a domain relocation that tripped Ruff: removed four dead status-enum imports in
protocol.py(they moved tocoffee_platform.domain), marked the three intentional re-export shims inconst.pyas explicit re-exports, and moved thecoffee_platform/domain.pymodule docstring abovefrom __future__ import annotations(E402). No behavior change; 1000 tests still pass.
[0.81.0] — 2026-06-04¶
Refactored¶
- Platform owns the domain vocabulary (Phase 2a). 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. TheCoffeeMachineClientcontract drops itsAnyplaceholders and references the real types.coffee_platform/is now fully self-contained (stdlib-only, no imports back into the integration) — ready for extraction to a standalone package. No user-facing behavior change.
Tests¶
- New
tests/coffee_platform/test_domain.py: enforcescoffee_platform/self-containment (nomelitta_baristaimports, AST-checked) and shim re-export identity. Full suite: 1000 passed (was 998).
[0.80.1] — 2026-06-04¶
Refactored¶
- Platform contract migration completed (Phase 1b). All remaining consumer files migrated from the concrete
MelittaBleClienttype hint to theCoffeeMachineClientcontract:button.py,select.py,number.py,switch.py,text.py,time.py,binary_sensor.py,entity.py(theMelittaDeviceMixinbase),diagnostics.py.__init__.pyintentionally retainsMelittaBleClient— it is the Eugster provider's composition root (constructs the concrete client) and stays with the provider in a future package split. Type-hint-only change; no runtime behavior change. 998 tests pass.
[0.80.0] — 2026-06-04¶
Refactored¶
- Platform contract foundation (Phase 1a). New in-repo
coffee_platform/subpackage defines a transport-agnosticCoffeeMachineClientProtocol and aMachineRegistry.MelittaBleClientis verified to satisfy the contract via a compliance test. Consumers begin depending on the contract instead of the concrete client —sensor.pymigrated as the pilot. The Sommelier path no longer reaches into the client's private_capabilitiesattribute (routed through the publiccapabilitiesproperty). No user-facing behavior change; this is groundwork for extracting a standalone platform package.
Tests¶
- New
tests/coffee_platform/suite: contract compliance, private-leak guard, registry behavior. Full suite: 998 passed (was 992).
[0.79.1] — 2026-05-28¶
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 (per issue #14): hostbluezpackage install,/run/dbus:/run/dbus:rosocket mount,--privileged+--net=hostcontainer flags. Includes BlueZ cache-reset recipe. HCL.mdrow for Apple Broadcom BCM2046B1 / BCM20702A0 (USB ID05ac:828d) graduated to ✅ verified, with full diagnostic context: Ubuntu 24.04 + HA Container + BlueZ 5.72, NICR 779 (Nivona 7xx) end-to-end.
Fixed¶
- Misleading
ble_agentlog when D-Bus is unreachable. Previously a Docker user without/run/dbusmounted would see "Assuming ESPHome BLE proxy" — incorrect and misleading. The log now explicitly names both valid scenarios (ESPHome proxy → expected; Docker without D-Bus mount → broken setup) and points to the README troubleshooting section.
[0.79.0] — 2026-05-28¶
Refactored¶
- BrandProfile contract extended, brand_slug checks removed from shared layers. A multi-stage cleanup so HA entity factories and BLE mixins no longer test
client.brand.brand_slug == "X"or import directly frombrands.<vendor>. The integration is now a platform layer where adding a new coffee-machine brand means implementing theBrandProfileProtocol and per-familyMachineCapabilitiesflags — no edits to shared code. - New BrandProfile Protocol methods:
temp_recipe_type_register(class attr),temp_recipe_register,fluid_write_scale,mycoffee_layout,mycoffee_register,is_chilled_selector. NivonaProfile already implemented these (lifted from module-level @staticmethods); MelittaProfile gets stub implementations returningNone/1/False._ble_commands.py,_ble_recipes.py,sensor.pydropped theirfrom .brands.nivona import …calls in favour ofclient.brand.<method>(…). - New MachineCapabilities feature flags:
supports_factory_reset,supports_brew_overrides,uses_legacy_total_cups_sensor. Set per-family in_family_*.pyand per-model in MelittaProfile. Entity factories inbutton.py,number.py,sensor.pyuse these flags instead ofbrand_slug == "X"gates. Hardcoded_FACTORY_RESET_FAMILIES = {"600", "700", …}set deleted — the gating is nowcaps.supports_factory_reset. - Redundant brand_slug gates dropped:
button.py/select.pyHE-selector brew entities now key off"HC" not in supported_extensionsalone (the brand_slug half was redundant);number.py/select.pysettings-entity gates rely oncaps.settingsbeing empty for Melitta (which it is).
Added¶
docs/BRAND_PROFILE_SPEC.md— contract specification documenting the requiredBrandProfileProtocol surface, theMachineCapabilitiesfield set, how to add a new brand step-by-step, and explicitly-forbidden anti-patterns (brand_slug ==checks in shared layers, directbrands.<vendor>imports, etc.).
Tests¶
- Test fixtures across
tests/test_button.py,tests/test_init.py,tests/test_sensor.pyswitched fromclient.brand = MagicMock()to realMelittaProfile()/NivonaProfile()instances +capabilities_for(family_key). Real BrandProfile/Capabilities give the capability-driven gating actual values to compare against, avoiding MagicMock-truthy false positives. Tests that mutatedcaps.my_coffee_slotsdirectly now usedataclasses.replace()(MachineCapabilities is frozen). - Full suite: 992 passed (unchanged from v0.78.1 — no behavior change, only refactor).
[0.78.1] — 2026-05-28¶
Refactored¶
brands/nivona.pysplit into a package: 1277-line monolith decomposed into per-family modules. The single file becomesbrands/nivona/with five brand-level primitives (_crypto.py,_options.py,_registers.py,_stats_helpers.py,_prefixes.py) plus one module per family (_family_600.py,_family_700.pyfor 700+79x,_family_900.pyfor 900+900-light,_family_1030.pyfor 1030+1040,_family_8000.py). The publicNivonaProfileclass and every external import path throughbrands.nivona(NivonaProfile,mycoffee_layout,mycoffee_register,MY_COFFEE_BASE_REGISTER,MY_COFFEE_SLOT_STRIDE,TEMP_RECIPE_TYPE_REGISTER) stay unchanged.__init__.pyshrinks to ~280 lines: dispatch tables of family-module attributes + theNivonaProfileclass only.
Fixed¶
NivonaProfile.parse_statusbroken since v0.78.0 rename: the lazyfrom ..const import …/from ..protocol import MachineStatusreferences insideparse_statusdid not get re-rooted when the file moved one level deeper into the new package, so any HX status frame would crash withModuleNotFoundError. Re-pointed at...const/...protocoland added regression tests covering the 8000 and 700 process-code dispatch.
Tests¶
- 2 new regression tests pinning
parse_statusagainst future package moves (tests/test_brands.py::test_nivona_parse_status_8000_ready+test_nivona_parse_status_700_product). - Full suite: 992 passed (was 990 on v0.78.0).
[0.78.0] — 2026-05-28¶
Added¶
- Factory reset buttons for Nivona (
brand_slug == "nivona"). Two new button entities,entity_category=config,device_class=restart(so HA dashboards surface a confirm dialog before press): - "Factory Reset Settings" — sends
HE_CMD_FACTORY_RESET_SETTINGS = 50, wipes machine-wide settings. - "Factory Reset Recipes" — sends
HE_CMD_FACTORY_RESET_RECIPES = 51, wipes per-recipe customizations.
Both available on families 600 / 700 / 79x / 900 / 900-light / 1030 / 1040. Hidden (entity stays unavailable) on NIVO 8000 — the vendor app doesn't expose factory-reset there either. Melitta brand is unaffected — these buttons don't register for brand_slug == "melitta".
- Generic execute_command(command_id) path on EugsterProtocol and BleCommandsMixin (as execute_he_command). Builds an 18-byte HE payload with command_id BE in [0:2], sent under the existing HE opcode; the firmware distinguishes brew from "execute command" by payload shape.
- MyCoffee slot bulk read (Nivona). On every connect, after capability resolution, the client now reads each MyCoffee slot's 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:
- coffee_amount, water_amount, milk_amount, milk_foam_amount — the four fluid amounts. 600 family has only three (no milk_amount_offset); 700 / 79x / 8000 / 900 / 900-light / 1030 / 1040 have all four.
- enabled (0/1 flag) — whether the slot is "armed". Present on every family.
- strength (code 1..N) — bean-strength selector for the slot. Present on every family.
- temperature (code 0..3) — brew-temperature selector. Present only on single-byte-temperature families (600 / 700 / 79x / 8000); the 900 / 1030 / 1040 layouts use per-fluid temperature offsets and skip this param.
Sensor counts per family: NIVO 8000 → 9 slots × 7 params = 63 sensors; NICR 1040 → 18 × 6 = 108; NICR 600 → up to 5 × 6 = 30. All under EntityCategory.DIAGNOSTIC — they sit in HA's diagnostic group, not the main entity list.
Foundation for the broader MyCoffee CRUD work — write support and code-style params (profile, two_cups, per-fluid temperatures) will follow in later releases.
- Per-slot "Brew MyCoffee slot N" buttons for Nivona. One button per slot (1..my_coffee_slots) — sends HE with the saved recipe (no temp-recipe write). Press is gated at runtime on the slot's cached
enabledflag — slots that aren't "armed" stay unavailable so users don't accidentally fire an empty recipe. Button display number is 1-indexed (display "Brew MyCoffee slot 1" → cache slot 0 → HEpayload[3] = 20).
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 (registration count, availability gating, press wiring). - Full suite: 990 passed (was 964 on v0.77.1).
[0.77.1] — 2026-05-28¶
Added¶
MachineCapabilities.first_mycoffee_selector— new constant (default20) carrying the MyCoffee brew-selector base. Per observed vendor-app behavior, every Nivona model usesfirst_mycoffee_selector = 20, so to brew MyCoffee slot N the HE command needspayload[3] = 20 + N. Without this constant exposed in capabilities, callers had to hardcode the magic number. This release is purely additive — no behavior change yet; consumers will be wired up in a follow-up release.
Tests¶
tests/test_brands.py::test_nivona_first_mycoffee_selector_is_20locks the default at 20 for all eight Nivona families.- Full suite: 964 passed (was 963 on v0.77.0).
[0.77.0] — 2026-05-28¶
Fixed¶
Total Cupssensor no longer registers on Nivona (reported in #15). The legacyMelittaTotalCupsSensorreads HR id 150 (TOTAL_CUPS_ID), which is a Melitta-specific register that does not exist on Nivona machines — so the sensor stayedunknownforever on every Nivona install. The equivalent "total brews" counter on Nivona is already exposed via the capability-drivenBrandStatSensor(total_beverages, id 213 on the 8000 family, id 215 on the 1030 family). For Melitta the sensor still registers unchanged.
Changed¶
- Stat slugs renamed to align with vendor terminology (
brands/nivona.py): - 8000 family, id 206:
warm_milk→hot_milk(the vendor labels this counter "Heisse Milch" / hot milk). - 1030/1040 family, id 201:
lungo→coffee(the vendor labels this counter "Coffee", not Lungo). Existing entity registry entries are migrated automatically viaasync_migrate_entryv2 → v3 — HA's long-term statistics history follows the rename. _STATS_1030id 224 (beverages_via_kanne) title updated to"Beverages via Kanne (experimental)"to mark it as unverified — this register hasn't been observed in any vendor reference data we trust. Keep watching field reports; remove the entry in a future release if no real machine ever reports a non-zero value.
Added (new diagnostic sensors for NIVO 8000-family)¶
All four are read-only HR register polls; the values are present in vendor reference data but were missing from our _STATS_8000:
- id 211 grinding_count ("Anz_Mahlung") — total grinder uses.
- id 212 reserve_count ("Anz_Reserve") — internal reserve counter; semantics not fully clear, surfaced for parity.
- id 602 descale_status ("Entkalken_Status") — descale state machine flag, complements id 600 (descale percent) and id 601 (descale warning).
- id 630 frother_rinse_needed ("SpuelenAufsch_Notwendig") — flag that the milk frother needs a rinse cycle.
Added (1030/1040 family)¶
- id 210
my_coffee("Anz_Bezuege_MyCoffee") — the MyCoffee dispense counter was missing entirely on these families. Cross-references the existing MyCoffee slot system.
Migration¶
- Config entry version bumped from 2 → 3.
async_migrate_entryhandles the slug renames automatically on the next HA restart after upgrade. Old entity IDs (e.g.sensor.<name>_warm_milk) become the new ones (sensor.<name>_hot_milk); statistics history is preserved.
Tests¶
- 3 new tests in
tests/test_brands.pyverifying per-family stat sizes and specific (id, key) alignment against the vendor register set. - 2 new tests in
tests/test_sensor.pyfor the Total Cups brand gating. - 3 new tests in
tests/test_init.py::TestMigrateEntryV2ToV3covering both renames and a Melitta no-op case. - Full suite: 963 passed (was 956 on v0.76.1).
[0.76.1] — 2026-05-28¶
Fixed¶
- HU handshake response is now fully validated end-to-end. The response handler now requires the response to be ≥ 8 bytes, verifies that
payload[0:4]echoes the random seed we sent, and recomputes the HU verifier overpayload[0:6]to checkpayload[6:8]. Any mismatch is logged at WARNING and rejected —_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 install a junk session key, and every subsequent RC4-encrypted frame would fail to decrypt with no obvious error.
Tests¶
- 4 new tests in
tests/test_protocol.py::TestHandshakeResponseVerification(happy path, wrong echoed seed, wrong verifier, short response). - Existing
test_handshake_sends_challengepinsos.urandomto a fixed seed so the test response can be crafted with the correct verifier; existingtest_handshake_response_too_shortupdated for the new "event set on reject" contract.
[0.76.0] — 2026-05-27¶
Fixed¶
- Nivona advertisement regex is now permissive about digit-count and dash-count (#15). Every new Nivona model series has revealed a new advertisement shape — NIVO 8107 / NICR 6xx-7xx use 10 digits + 5 dashes, NICR 930 uses 15 digits with no dashes, NICR 779 (#14) uses 15 digits + 5 dashes, and NIVO 8101 (#15) uses 17 digits + 3 dashes. The
ble_name_regexpreviously enumerated these combinations individually and required a follow-up patch for each new model; it now accepts any\d{10,}serial with optional trailing dashes (and optionalNIVONA-prefix). Brand discrimination from Melitta is unaffected because Melitta's tighter 6-prefix regex is still matched first. - D-Bus connect failure in
ble_agentnow falls through to "skip pairing" instead of being reported as a pairing failure (#15). When the system D-Bus is unreachable (typical for HA OS containers without a host BlueZ stack — the device is reached via ESPHome BLE proxy that handles bonding at the ESP32 level), the integration used to bubble theMessageBus.connect()exception up aspairing_failed. It now treats this case the same as a missingAdapter1interface —async_pair_devicereturns"ok"with an info-log, and pairing succeeds against the proxy.
Notes¶
_PREFIX_TO_FAMILYmirrors the known model-prefix table (8101/8103/8107→8000). The reporter of #15 originally posted the issue as "NIVO 8001" — turned out to be a typo for "NIVO 8101", which is already supported. Discovery and pairing now succeed thanks to the permissive regex; if a future report surfaces an unknown serial prefix, the resulting family will beNoneand the user sees "unknown model" — preferred over guessing capabilities wrongly.
[0.75.0] — 2026-05-27¶
Added (panel)¶
- Dedicated "Producers" tab in the admin panel. Producers (the table referenced from both Beans and Additives via
producer_id) get their own top-level CRUD UI between Additives and System. The inline producer-management widget in the Beans tab is removed; the producer dropdown inside the Bean Add/Edit modal stays as a selector. Empty list shows a hint pointing to the new tab. EN + RU i18n. - Per-row "in stock" toggle on milk types in the Additives panel, matching the syrup / topping pattern from P4a (
✓/○chip, dimmed row when out-of-stock). Backed by two new WS endpoints:melitta_barista/sommelier/milk/list_full(returns all rows with theavailableflag) andmelitta_barista/sommelier/milk/set_available(per-row upsert toggle). The existingmelitta_barista/sommelier/milk/getcontinues to return only in-stock milks, so the Sommelier chip picker respects availability with zero FE change.
Changed¶
- Additives Add/Edit modal field order matches Beans: producer dropdown → name → Fill-from-LLM → variant → notes → composition → flavor-notes chips → attribute chips. The standalone "Brand" text input is removed — the producer dropdown already covers that role. On save, the
brandDB column is populated from the selected producer's name so existing queries / external consumers keep working (no migration). - LLM autofill for syrups / toppings takes
(name, producer_id)instead of free-textbrand. The handler resolves the producer's name (and falls back to the producer's stored website when the message omitswebsite) via a SELECT againstproducers.DEFAULT_PROMPTS["syrups_autofill"]/["toppings_autofill"]now reference{name}and{producer}; the{brand}placeholder is dropped.PROMPT_PLACEHOLDERSupdated accordingly. Beans autofill is unchanged (it already used the{brand}+{product}pair correctly). async_set_milk(list)preservesavailableon surviving rows. The previous DELETE-everything-then-INSERT-with-default-true behavior resurrected any milk the user had toggled off. New behavior is closer to UPSERT-with-prune:INSERT OR IGNOREfor new rows,DELETEfor rows not in the new list, leave the rest untouched. Empty-list still wipes the table.
Notes¶
- The
brandcolumn on syrups / toppings stays — no migration. The UI no longer exposes it for direct edit, but it's still populated on save from the producer dropdown so downstream queries / external consumers see a stable value. - Five new tests on top of the existing milk-config + autofill suites: bulk-save-preserves-toggle, single-row-upsert, list_full-returns-all-rows, autofill-unknown-producer-returns-not-found, autofill-falls-back-to-producer-website.
[0.74.2] — 2026-05-26¶
Fixed¶
- Nivona NICR 779 (and other 7xx variants advertising as 15 digits + 5 dashes) is now detected as Nivona instead of falling back to Melitta (#14). The
ble_name_regexonly matched\d{15}(no dashes) OR\d{10}-----; NICR 779's779573191222251-----advertisement form was neither, so the brand resolver picked Melitta, the wrong RC4 keys were used, and the HU handshake timed out. The regex now accepts\d{10,15}-----alongside the existing\d{15}branch.
Notes¶
- The second half of #14 (registering the
_NoInputOutputAgentD-Bus agent during ongoing connections, not just initial pairing) is not included in 0.74.2. The agent is already wired in viaconfig_flow.pyfor first-time pairing and viableak-retry-connector'sestablish_connection(pair=True)for reconnects. If headless-Linux Nivona installs still need agent-during-every-notify-subscribe coverage we'll iterate after hardware confirmation from the reporter. - 16+ digit + dashes ad forms continue to be rejected, intentionally.
[0.74.1] — 2026-05-26¶
Fixed¶
- Panel components now import the i18n resolver from
./i18n/index.jsdirectly instead of going through the./i18n.jsre-export shim. The static-import path matters for browser HTTP caching: existing installs that had the pre-P11 monolithici18n.jscached would keep serving the stale module to new component code, surfacing raw i18n keys (presets.label,additives.fill_from_llm, etc.) in the UI. The new path is a fresh URL that's never been in any browser cache. The./i18n.jsshim stays in place for external consumers / Lovelace cards.
[0.74.0] — 2026-05-26¶
Changed (P11 — i18n decomposition)¶
- Panel translations split into per-locale ESM modules under
www/i18n/locales/. The 731-line monolith is nowwww/i18n/index.js(54-line resolver) +www/i18n/locales/en.js+www/i18n/locales/ru.js.www/i18n.jsis a tiny re-export shim so existing callsites (import { t } from "./i18n.js") keep working unchanged. tests/test_i18n_parity.pyenforces full key-set parity between every non-English locale anden.js(both directions — no missing keys, no stale orphans). Adding a new key requires touching every locale in the same PR.
Notes¶
- Resolver API (
t,makeT,SUPPORTED_LANGUAGES) is unchanged. No JS callsite edits. - 302 keys × 2 locales as of 0.74.0. Adding a new language is now a single PR: drop
www/i18n/locales/<HA-lang-code>.jsmirroringen.js, register it inindex.js's STRINGS dict, parity test guards the rest.
[0.73.0] — 2026-05-26¶
Added (P10 — Nivona-safe Sommelier)¶
LiveCapabilitiesnow carriessupports_recipe_writes: bool(schema v1 → v2). The flag is sourced from each family'sMachineCapabilities.supports_recipe_writes. Surfaced throughmelitta_barista/capabilities/get. v1 cached blobs default toTrueon parse so existing Melitta installs see no change.ws_brewandws_favorites_brewrefuse the BLE write when the active machine reportssupports_recipe_writes=False, surfacingrecipe_writes_unsupportedviasend_errorinstead of failing silently in the BLE layer. The newRecipeWritesUnsupportedErrorcarries the offendingfamily_key.- Brew-related buttons are disabled in the Sommelier UI when the active machine doesn't support recipe writes — applies to the per-card "Brew this" action, the brewing wizard's "Start brewing" button, and the brew / "Brew again" actions in the Favorites and History modals. The wizard still opens so the user can read the recipe + steps as a print-only card; an inline note in the pre-phase explains why brewing is unavailable. EN + RU i18n via
brewing.unsupported_tooltip/unsupported_note/unsupported_error.
Notes¶
- All Nivona families declare
supports_recipe_writes=Falsebecause their recipe protocol differs from Melitta's freestyle slot — the integration cannot write a custom recipe to a Nivona machine. Adding Nivona freestyle support is a separate RE / protocol effort, out of scope here. - The Sommelier data + UI surface (generation, favorites, history, ratings, presets, pantry) is brand-agnostic and remains fully functional on Nivona as a recipe notebook.
[0.72.0] — 2026-05-26¶
Fixed (TZ §10 B6 — preference-key write allowlist)¶
use_weather,weather_entity, anduse_presenceare now writable viamelitta_barista/sommelier/preferences/set. They have always been read byws_generate(powering the weather and presence context blocks in the LLM prompt), but the write-side allowlistVALID_PREFERENCE_KEYSonly covered the fourdefault_*keys, so the WS API surface couldn't actually configure them. Frontends had to write into the DB out-of-band. Now they're first-class preferences.
Notes¶
- This closes the only live backend bug from the TZ's §10 open-issues snapshot. The remaining items in that list are either documentation placeholders (
recipes/listemptybase_recipes), explicit deferrals (multi-machine routing, milk catalogue rewrite), or non-issues now (R9 audit, P3 ratings/history, rich-field syrups/toppings).
[0.71.0] — 2026-05-26¶
Added (P8b — R1 slice 2: autofill endpoints + UI modal extension)¶
melitta_barista/syrups/autofillandmelitta_barista/toppings/autofillWS endpoints. Mirror/beans/autofill: take{brand, variant?, website?, agent_id?}, call_structured_callagainst an HA conversation agent, return{raw, parsed, validation_errors, via, schema_version}. Theparseddict is validated by the new sharedAdditiveAutofillResultpydantic model (flavor_notes: list[str],composition: str,attributes: dict[str, bool],variant: str). Backed byDEFAULT_PROMPTS["syrups_autofill"]/toppings_autofillwith{brand}/{variant_hint}/{website_hint}placeholders.- Additives modal: rich-field block for syrups & toppings in
melitta-additives.js. Producer dropdown (loadsmelitta_barista/producers/list), Variant input, Flavor-notes chips (removable), Composition textarea, predefined Attribute chips (vegan/sugar_free/lactose_free/gluten_free/nut_free). Save sends only the populated fields — partial-patch semantics on the backend keep prior values intact for fields the user didn't touch. - "Fill from LLM" button in the modal. Disabled until the user enters a brand. On success merges the parsed response into the editing state (variant only fills if empty; attributes filtered to
truevalues; flavor_notes deduped). Errors stay scoped to a.autofill-errorbanner inside the modal.
Notes¶
- Milk rows are intentionally unchanged — the milk catalogue still uses the flat-list
/milk/get|setshape. Rewriting it to a CRUD catalogue with the same rich fields requires a legacy shim and stays out of scope. - The new fields are persisted via the existing P8a
<table>/add/<table>/updateendpoints; no breaking changes to schemas.
[0.70.0] — 2026-05-26¶
Added (P8a — R1 slice 1: rich-field syrups & toppings catalogue)¶
syrupsandtoppingscatalogue tables gainproducer_id,variant,flavor_notes,composition, andattributescolumns. Legacy DBs migrated via idempotentALTER TABLEguards inside_ensure_panel_schema(extends the P4a pattern).flavor_notesis a JSON-encoded list of strings;attributesis a JSON-encoded object; both are NULL by default. The existingbrandcolumn carries over unchanged.<table>/listreturns the new fields with JSON values parsed back into Python lists/dicts. Bad JSON in a column returnsNoneinstead of crashing the handler (defensive fallback).<table>/addand<table>/updateaccept the new fields as optional parameters with voluptuous length/type constraints. Existing partial-patch semantics onupdate(no_fieldserror when nothing changes) are preserved; the new fields count toward the patch.
Notes¶
- Mirrors the existing
coffee_beansrich-metadata shape. The producers table already supports cross-category use, so no producer-table changes are needed. - UI extensions to the Additives modal (producer dropdown, variant input, flavor-notes chips, attributes chips) and the LLM-backed
/syrups/autofill//toppings/autofillendpoints are deferred to P8b. - Milk-config rewrite (turning the flat list into a CRUD-able catalogue with the same rich fields) is a separate, larger refactor with a legacy shim for the existing
/milk/get|setendpoints — scoped out of P8. - The Sommelier LLM prompt does not yet consume the new fields. Whether to enrich the prompt with brand / flavor_notes / composition is gated on observed recommendation quality — that's an optional future P8c.
[0.69.0] — 2026-05-26¶
Added (P7b — R8 slice 2: machine_profile UI)¶
- "Profile N" badge in the Sommelier panel header when the machine reports an active hardware profile. Sourced from
melitta_barista/status'sactive_profilefield; falls back silently if the integration cannot reach the machine. - Preset / favorite / history lists auto-filter to the active profile by passing
machine_profile_filteron every list call. Shared entries (machine_profile IS NULL) always come through, so nothing disappears when a user switches profiles. - "Save as preset" gains an optional "Bind to profile N" checkbox — visible only when a profile is active, default OFF (shared). Checked → preset is bound to the current machine profile via the P7a
machine_profileparameter. sommelier/generateis auto-tagged with the active profile so the resultinggeneration_sessionsrow surfaces under the same filter in History.
Notes¶
- R8 is now functionally closed: tagging + filtering work across presets, favorites, history, and new generate sessions. The favorites "Add" path inherits shared semantics (no per-machine binding UI yet — the Manage modal hides per-profile edit because R8 didn't request it for favorites).
- Profile-aware filtering is implicit (no toggle). The TZ §R8 mention of a "Show shared toggle" is deferred — every profile-aware list already includes shared rows, which addresses the common case.
[0.68.0] — 2026-05-26¶
Added (P7a — R8 slice 1: machine_profile tagging, data layer)¶
machine_profile INTEGERcolumn onsommelier_presets,favorites, andgeneration_sessions. NULL means "shared" (visible across all machine profiles); a specific integer binds the row to that profile slot. Existing rows are retro-flagged shared via the v8 → v9 migration'sALTER TABLE ... ADD COLUMN(SQLite leaves the new column NULL for existing rows by default). Row dicts now includemachine_profile.- Optional
machine_profileparameter onmelitta_barista/sommelier/presets/add,favorites/add, andsommelier/generate(tags the createdgeneration_sessionsrow). - Optional
machine_profile_filterparameter onmelitta_barista/sommelier/presets/list,favorites/list, andhistory/list. When set to an integer N, the response includes rows wheremachine_profile = NORmachine_profile IS NULL. Shared rows always come through.
Notes¶
- This is the data-layer slice of TZ §R8. The FE work — surfacing the active profile from
client.active_profile, choosing a profile on save, filtering the lists by current profile — is P7b. - The
machine_profileinteger here refers to the machine's hardware profile (the same valueclient.active_profileexposes viamelitta_barista/status), not the Sommelier user-profile concept tracked separately bysommelier_profiles.
[0.67.0] — 2026-05-26¶
Added (P6b — schema_version envelope, R10 slice 2)¶
- Every
melitta_barista/*WS response now carries aschema_version: intkey alongside its existing fields.API_VERSION(from 0.66.0) still tracks the integration-wide surface;schema_versionis the per-endpoint discriminator that lets consumers pin against an individual endpoint's response shape. All endpoints ship atschema_version = 1in this release. - Centralised helper
_send_versioned(connection, msg_id, data, *, schema_version=1)inpanel_api.py, imported bysommelier_api.pyand__init__.py. Everyconnection.send_result(...)call in the integration now routes through this helper. Future endpoints should use it instead ofsend_resultdirectly.
Notes¶
- Purely additive — existing response keys are unchanged; clients that ignore uf nknown keys (the default for both Lit and HA companion app) keep working without changes. Tests asserting exact response shape were relaxed to ignore
schema_versionvia a small_assert_resulthelper in the affected test files. - TZ §R10's MUST for per-endpoint versioning is now satisfied. Slim list variants and a REST wrapper (§O10.1) stay out of scope until a real consumer asks for them.
[0.66.0] — 2026-05-26¶
Added (P6a — API contract foundation, R10 slice 1)¶
docs/SOMMELIER_API.md. Exhaustive enumeration of everymelitta_barista/*WebSocket endpoint (~55 entries), organised by namespace, with inputs / outputs / decorators / stability tag. This is now the canonical API contract — bumpingapi_versionrequires updating this doc. Each endpoint is marked≤0.65.0(pre-existing) or0.66.0(the newapi/infoitself).API_VERSION = "1.0"constant inconst.py. Semver: bump major on a breaking change to any endpoint's input or output shape; bump minor on additive changes (new endpoint, new optional field, new optional response key).melitta_barista/api/infoWS endpoint. Non-admin discovery handshake returning{api_version, integration_version, schema_db_version, endpoints}.endpointsis the domain-prefixed subset of HA's registered WS commands; consumers cross-referencedocs/SOMMELIER_API.mdfor per-endpoint details. The integration-version lookup is wrapped in a broad try/except so the handshake never fails.
Notes¶
- Per-response
schema_versionenvelopes (TZ §R10's MUST) are deferred to P6b — a mechanical retrofit across ~55 handlers and their tests. - Slim list variants and a REST wrapper (TZ §O10.1) stay out of scope until there's a concrete consumer asking for them; HA companion app and Lovelace both speak WS.
[0.65.0] — 2026-05-26¶
Added (P5b — Sommelier presets closing slice + §O7.1)¶
- Four built-in presets seeded on first setup: Morning, After lunch, Work, Guests. Names resolve through i18n (
presets.system.*), so the select shows them in the user's language. All four ship withdynamic_occasion=true(see below), mirroring the existing_suggestOccasionByTime()time bands. dynamic_occasionflag in the preset payload. Whentrue,_applyPresetrecomputesoccasionfrom the current local time instead of taking the snapshot value — applying "Morning" at 3pm fires the form withoccasion=after_lunch. System presets default totrue; user-created presets default tofalse(snapshot semantics).- System presets are read-only. The DB layer raises
ValueError("system_preset_readonly")for update/delete onis_system=1rows; the WS handlers translate that intosend_error("system_preset_readonly", ...). The Manage modal hides the rename + delete buttons for built-ins and shows a(built-in)badge instead. - DB schema v7 → v8. Adds
is_systemanddynamic_occasioncolumns tosommelier_presets. Existing user presets retainis_system=0anddynamic_occasion=0. Seeding runs idempotently at the end ofasync_setup.
Fixed (R9)¶
melitta-beans.js:619rendered "Validation errors:" as a hardcoded literal — the last visible string outsidei18n.js. It now lives incommon.validation_errorswith English and Russian entries.
[0.64.0] — 2026-05-26¶
Added (P5a — Sommelier presets, R7 slice 1)¶
- Named presets for the Sommelier generate form. Snapshot of
mode,preference,cup_size,moods,occasion,temperature,caffeine_pref, anddietary— applied with one click from the new "Preset" select in the Sommelier header. Save the current form state via "Save as preset"; manage existing entries (rename, edit description, delete) via the new<melitta-sommelier-presets>modal. sommelier_presetstable (DB v6 → v7). Backed by fourmelitta_barista/sommelier/presets/{list,add,update,delete}WS endpoints (require_admin, opaque JSON payload,preset_idto avoid the HA WS top-levelidcollision).
Changed (incidental)¶
- The pre-existing
melitta_barista/sommelier/presets/listhandler that served the staticcoffee_presets.jsonbean catalogue was renamed tomelitta_barista/sommelier/bean_presets/listto free the namespace. No in-tree caller used the old name.
Notes¶
- Pantry constraints (
allow_syrups/allow_toppings/allow_milk) and per-generationcountare intentionally not snapshotted — pantry tracks catalogue availability and count is a knob, both better picked fresh per generation. - System defaults, profile binding (R8), and the "dynamic occasion" toggle from §O7.1 are deferred.
[0.63.0] — 2026-05-26¶
Changed (P4b — Pantry, R6 closing slice)¶
- The Sommelier AI now reads its pantry directly from the syrups / toppings catalogue (
available=1), not fromuser_extras.ws_generate,melitta_barista/sommelier/extras/get, and thesommelier_introprompt-preview path are all backed by the newasync_get_pantry_extrashelper. Liqueurs and misc/ice still live inuser_extrasand are unaffected. - Sommelier UI chip list hides out-of-stock items.
_loadAvailablefilters by the catalogue'savailableflag, so toggling a syrup off in the Additives panel removes it from the chip picker on the next Sommelier visit. The backend enforces the same filter. set_availableno longer mirrors intouser_extras. The mirror was a P4a bridge so the AI (which readuser_extrasthen) would see catalogue toggles. With the AI reading the catalogue directly, the mirror is dead weight and is dropped.
Migration note¶
- If your Sommelier suggestions used to draw on syrups/toppings that were stored only in
user_extras(never added via the Additives panel), re-add them via the Additives panel. They will land in the catalogue withavailable=1and become visible to the AI again.
[0.62.0] — 2026-05-26¶
Added (P4a — Pantry, R6 slice 1)¶
availablecolumn on thesyrups/toppingspanel catalogue tables. Schema lifted on fresh DBs viaCREATE TABLE; legacy DBs get the column via an idempotentPRAGMA table_info-guardedALTER TABLEinside_ensure_panel_schema. Existing rows are retro-flagged in stock (DEFAULT 1). The flag is exposed inmelitta_barista/syrups/list/toppings/listresponses and patchable via the existing<table>/updatehandler.melitta_barista/<table>/set_availableWS endpoints forsyrupsandtoppings. Resolves the catalogue row byadditive_id, updates the flag, and mirrors the state intouser_extrasso the Sommelier AI prompt context (which still readsuser_extrasin P4a) reflects pantry state without a separate UI step. The mirror is one-way (catalogue →user_extras) and upserts on enable.- Inline "in stock" toggle on every syrup / topping row in the Additives panel. Out-of-stock rows render at 50% opacity; the toggle reads
additives.in_stock/additives.out_of_stockfor its tooltip.
Notes¶
- This is the first slice of TZ §R6. Beans availability, milk-row toggle, and the Sommelier-side "Show only in-stock items" UX are intentionally out of scope here. Full cutover (Sommelier reads the catalogue directly,
user_extrasretired) is P4b/P4c.
[0.61.0] — 2026-05-25¶
Added (frontend, finishes the P3 ratings/history surface)¶
<melitta-sommelier-history>modal. Opened from the new "🕓 History" button in the Sommelier header. Paginated session list (loadsmelitta_barista/sommelier/history/list) with expandable rows: clicking a session inlines its recipes with per-recipe star rating, "Brew again" action (routes through the wizard withsource="generated"so the user can re-tune extras / cup before brewing), and a tasting note when one is recorded.- "Clear history" footer action. Confirms via
<melitta-confirm>and calls the P3amelitta_barista/sommelier/history/clearendpoint withkeep_favorited=true. Sessions referenced by favorites are preserved (server-enforced); the returned{cleared}count drives the success toast.
Notes¶
- Star rating uses the shared
<melitta-star-rating>component introduced in 0.60.0; ratings written from the history view are immediately reflected because the modal re-loadshistory/liston every open. - This release closes the P3 surface. Further Sommelier ergonomics (e.g. cross-session recipe search, comparative ratings across capability snapshots) are out of scope.
[0.60.0] — 2026-05-25¶
Added (frontend, consumes P3a ratings backend)¶
<melitta-star-rating>shared component (www/components/ui/). Five clickable stars; emitsratewith the chosen value andunratewhen the user clicks the currently-active star. Supports areadonlymode for view-only contexts. Used in both Generate result cards and the Favorites modal.<melitta-sommelier-favorites>modal. Opened from the new "★ Favorites" button in the Sommelier header. Lists saved favorites with inline rating, optional tasting note (gated by an existing rating, matching the P3afavorites/updatecontract), rename / edit description (inline form), brew (routes through the wizard withsource="favorite"), and delete (with confirm). Loadsfavorites/liston every open.- Star rating inline in Generate result cards. A rated recipe persists across history reads —
_ratingsis a local optimistic cache layered on top of the server-suppliedratingfield.
Changed¶
<melitta-brew-wizard>acceptssource("generated" | "favorite") +sourceId. Whensource === "favorite", the brew call goes throughmelitta_barista/sommelier/favorites/brew(which incrementsbrew_count) instead ofsommelier/brew. Backward-compat: when unset, defaults to "generated" /recipe.id.
Notes¶
- History view (
<melitta-sommelier-history>) and full Sommelier sub-tabs refactor are deferred to P3c. The Favorites modal is a pragmatic shortcut that exposes the P3a backend without restructuring the Sommelier tab. - Tasting notes still require a rating to exist first (P3a constraint). The Favorites modal surfaces this with
favorites.note_needs_ratinghint when the favorite has no rating yet.
[0.59.0] — 2026-05-25¶
Added (backend, foundation for History/Favorites/Ratings UI in P3b)¶
recipe_ratingstable (DB v5 → v6). Stores 1..5 star ratings + optional tasting notes, keyed by(target_id, target_type)wheretarget_type ∈ {"generated", "favorite"}. Lets the same recipe carry a separate "first impression" (on the generated row) and an "after saving" rating (on the favorite copy). Validation: range check via CHECK constraint + PythonValueErrorfor defensive depth.melitta_barista/sommelier/recipe/rateandrecipe/unrateWS endpoints. Upsert / delete a rating for any recipe. Voluptuous validation: rating in 1..5; target_type in the two enum values.require_admin.melitta_barista/sommelier/favorites/updateWS endpoint. Patch a favorite's name, description, or note. Notes route through the unifiedrecipe_ratingstable (a rating must exist first; the UI is expected to combine the two operations in P3b).melitta_barista/sommelier/history/clearWS endpoint withkeep_favorited(defaulttrue) — protects sessions whose recipes are referenced byfavorites.source_recipe_id. Relies on the existingON DELETE CASCADEbetweengeneration_sessionsandgenerated_recipes(PRAGMA foreign_keys=ONalready set inasync_setup). Returns{cleared: <count>}.async_list_favorites,async_get_favorite,async_list_history, andasync_get_recipenow exposerating+notevia aLEFT JOIN recipe_ratings. Bothtarget_type='favorite'andtarget_type='generated'JOINs supported. Recipes without ratings returnnullfor both fields.
Notes¶
- Frontend components (favorites view, history view,
<melitta-star-rating>) are out of scope here — P3b will consume these endpoints. - The
brew_countregression (wizard path calls/sommelier/brewinstead of/favorites/brew, so the favorite'sbrew_countnever increments after the initial add) is also deferred to P3b — it requires wizard-level wiring that depends on the favorites view UX.
[0.58.0] — 2026-05-25¶
Added¶
<melitta-brew-wizard>component (R3). Pre/during/post-brew wizard opened from "Brew this" in the Sommelier panel. The pre phase lists user prep steps (cup choice, ice, additive measurement) and the chosen cup type. The during phase fires the BLE brew, animates a progress bar driven by an estimated duration, and pollsmelitta_barista/statusevery 2 s to auto-advance when the machine returns to READY. The post phase lists finishing-touch steps and the recipe'sextras.instructiontext. Cancel / "I'm done" buttons keep the wizard usable in offline / no-poll modes.RecipeStep.phasefield (Literal["pre", "during", "post"], default"during"). LLM is now explicitly instructed to phase-tag each step; the wizard splits steps by this field. Recipes without phase fields (legacy or LLM oversight) all render as during-brew steps, matching prior behaviour.- Estimated brew duration heuristic (
estimateBrewSeconds(recipe)inmelitta-brew-wizard.js). Frontend-side formula:8 s warmup + portion_ml / 50 × 5 sper phase. Conservative; drives the progress bar saturation at 95 % and the manual-finish-button timeout (estimated + 30 s).
Changed¶
- Sommelier "Brew this" no longer fires
melitta_barista/sommelier/brewdirectly. Instead it opens the wizard, which orchestrates the brew call when the user clicks "Start brewing" in the pre phase. The user-facing latency between click and machine starting is roughly the same; the wizard adds explicit phases and the option to back out before any BLE write fires. ws_favorites_addnow storesmachine_phasesalongside legacycomponent1/component2(closes a pre-existing bug wherews_favorites_brewreadmachine_phasesbutws_favorites_addonly stored the old shape).
Notes¶
- WS status pushing is NOT in scope — the wizard polls
melitta_barista/statusinstead. A future P2c / P3+ may introduce a proper push subscription if poll-latency becomes a UX bottleneck. - Sequential per-phase brewing with explicit user-action pauses between machine phases is deferred to P2c. For multi-phase recipes today, the BLE protocol still fires both components in a single
brew_freestylecall (the machine sequences them internally without exposing the gap to the host). - TTS / voice-prompted wizard steps are explicitly out of scope.
[0.57.0] — 2026-05-25¶
Changed (data-model migration)¶
GeneratedRecipe.component1+component2replaced bymachine_phases: list[MachinePhase](length 1..2). EachMachinePhasecarries acomponent: RecipeComponentand auser_action_before: list[RecipeStep](always empty in P2a; populated by the Brewing Wizard in P2b). The pydantic model change automatically propagates into the LLM-prompt JSON Schema and the validation/retry loop.- DB schema v4 → v5. New
machine_phases TEXTcolumn ongenerated_recipesandfavorites. Migration populates the new column from existingcomponent1/component2via SQLite JSON1 (json_array/json_object). Old columns stay NOT NULL for cross-version readability; new rows write synthesized placeholders. Physical column drop is a P3+ housekeeping task. _brew_recipe_componentsnow takesphases: list[dict]instead ofcomp1/comp2kwargs. The BLE call (client.brew_freestyle(component1=..., component2=...)) is unchanged — the helper unpacks the first two phases and synthesizes a"none"-process component2 for single-phase recipes. Theportion // 5conversion andblend-alternation between components are preserved.- LLM prompt: example JSON, rules block, and capability-instruction text reference
machine_phasesinstead ofcomponent1/2. Single-phase brews are encouraged by default; a second phase is added only when a single-phase brew can't achieve the result.
UI¶
melitta-sommelier.jsiteratesr.machine_phasesto render per-phase chips. A fallback tor.component1/r.component2for legacy WS responses is kept until P2b.
Notes¶
- BLE-protocol layer (
client.brew_freestylesignature) is untouched in P2a. Sequential brewing with explicit user-action pauses between phases is the Brewing Wizard's job (P2b), not the BLE layer's. - Read path for favorites / history synthesizes
machine_phasesfromcomponent1/component2for legacy rows; it still returnscomponent1/component2for backwards-compat readers (the frontend keeps its fallback). - 19 new tests:
test_machine_phases.py(7 on the pydantic model) + updates totest_ai_recipes.py/test_sommelier_db.py/test_capabilities_db.py.
[0.56.0] — 2026-05-25¶
Added¶
- Capability-driven LLM prompt (R4).
_build_promptnow accepts aLiveCapabilitiesobject (from P1a) and emits the## Machine Capabilitiessection from the connected machine's actual supported set. Explicit instruction tells the LLM to ignore JSON-schema values not listed in this section.ws_generatefetches caps from the DB cache, falls back to live-derive, then toNone(legacy universal block). - Per-request
agent_idoverride (B7).melitta_barista/sommelier/generateWS now accepts an optionalagent_idfield;_resolve_agent_idis the single source of truth (msg.override > settings > HA default). - Optional
entry_idforprompts/preview. Capability-aware prompt preview for development.
Changed¶
- Pydantic is now a mandatory dependency (B8).
pydantic>=2.0added tomanifest.json. Thetry/except ImportErrorsoft-degrade path is removed;_PYDANTIC_OKflag retired;_schema_forand_validate_parsedno longer guard against missing pydantic. In HA's runtime, pydantic v2 is always available — the degrade path was dead code. - Eager
sommelier_dbinitialization inasync_setup_entry. Fixes the P1a probe-on-connect caveat: capabilities cache is now populated on the very first handshake rather than only after a panel open.
UI¶
melitta-sommelier.jscapability-aware temperature chips. Chips forhot/iceddim and disable when not supported by the connected machine. Tooltip ("Not supported by this machine" / "Не поддерживается этой машиной") explains why. New state field_capabilitiespopulated viamelitta_barista/capabilities/getat mount.
Notes¶
- Dynamic-per-request pydantic models (
Literal[*supported_processes]) explicitly deferred: the JSON schema still enumerates the universal set, but the Machine Capabilities section explicitly instructs the LLM to ignore schema values not listed. Pragmatic — re-evaluate if LLM compliance turns out poor. - Cup-size / mood / occasion / caffeine / dietary chips are UI-only conventions (no direct machine mapping) — not gated against capabilities.
[0.55.0] — 2026-05-25¶
Added¶
LiveCapabilitiesdata model (custom_components/melitta_barista/capabilities.py) — typed view of machine capabilities (supported processes / intensities / aromas / temperatures / shots, per-process portion limits, forbidden combinations). Frozen dataclass withto_json()/from_json()round-trip; rejects unsupportedschema_versionearly.machine_capabilitiesSQLite table (sommelier DB schema v3 → v4). Cached perentry_idwithprobed_atUTC timestamp. Newasync_get_capabilities(entry_id)/async_save_capabilities(entry_id, json_payload)methods onSommelierDB.derive_capabilities(client)builder — producesLiveCapabilitiesfrom a client's static brand profile +const.pyenum maps.strength_levels=3→ center three intensities (mild/medium/strong);strength_levels=5→ all five.has_aroma_balance=False→ onlystandardaroma. Per-process portion limits use a global default ({min: 0, max: 250, step: 5}) for P1a.- Probe-on-connect hook —
_make_capabilities_probe_callbackfactory in__init__.pywired viaclient.add_connection_callback; on a successful handshake it derives + saves capabilities. Errors during derive / save are swallowed and logged. melitta_barista/capabilities/getWebSocket endpoint — returns cached blob withsource: "cache", or falls back to on-the-fly derive withsource: "derive", orsend_errorif neither is possible. Corrupt or future-schema cached payloads are gracefully detected and fall through to the live-derive path (regression test included).
Notes (P1a scope)¶
- No new BLE round-trips.
forbidden_combinationsis always[]; real values arrive when protocol observation produces them. - Known caveat:
sommelier_dbis initialized lazily on the first WS call — atasync_setup_entrytime it is usuallyNone, so the probe-on-connect callback silently no-ops on a fresh setup and the cache is populated only after the panel opens. The fallback-to-derive path in the WS endpoint covers this transparently. Eager DB initialization is deferred to P1b (or a follow-up hotfix). - LLM-prompt rewrite (R4 prompt section), B7 (agent_id override in
/generate), B8 (pydantic mandatory dep), UI gating, and per-process portion limits are explicitly out of scope — seedocs/SOMMELIER_TZ_DRAFT.md§13 / §14.
[0.54.1] — 2026-05-25¶
Changed¶
- HACS download counter enabled.
hacs.jsonnow declareszip_release: true+filename: melitta_barista.zip. New.github/workflows/release.ymlbuilds a release zip and uploads it as an asset on every published GitHub Release. Previously HACS fell back to the GitHub Contents API (which doesn't increment any download counter), so the badge in HACS UI always showed 0 / "unknown". Existing installs are unaffected — HACS will simply pull the zip on next update instead of fetching files individually. Old releases (pre-0.54.1) still have no asset and will not retroactively gain a counter; only v0.54.1+ will count.
[0.54.0] — 2026-05-25¶
Added¶
<melitta-confirm>shared dialog component — promise-based replacement for nativewindow.confirm(). All deletion dialogs in Beans / Add-ins now use it with destructive-styling.design-tokens.js+sharedStylesexport fromlit-base.js— spacing scale, radius, focus-ring tokens for uniform component styling. Foundation for the wider design-system work in upcoming P1–P5 plans.<melitta-system>container with sub-tabs Status / Settings / Diagnostics / Machine recipes — collapses what used to be four top-level navigation buttons into one.
Changed¶
- Top tabs reduced to four in new order: Sommelier / Beans / Add-ins / System. Sommelier becomes the primary (first) tab; everything machine-side / configuration / diagnostic lives behind a single "System" tab in last position.
- Hopper-assignment flash messages are now localized through
i18n.js(en + ru) — previously the strings were hardcoded in Russian regardless of the user's HA language. - LLM autofill
via:debug info moved into a collapsed<details>block — production UI no longer leaks the agent backend name as a top-level label on the bean modal.
Removed¶
- Native
window.confirm()frommelitta-beans.js(2 sites) andmelitta-additives.js(1 site). tabs.status,tabs.diagnostics,tabs.recipes,tabs.settingsi18n keys (replaced bytabs.system+system.subtabs.*).
Notes¶
- No backend, BLE, LLM, or schema changes — pure frontend refactor. First plan in the P0–P5 series.
[0.53.4] — 2026-05-24¶
Changed¶
- Migrated AES from
pycryptodometocryptography(pyca). Both AES-CBC call sites (protocol._derive_rc4_key,brands/melitta._derive_rc4_key) now usecryptography.hazmat.primitives.ciphers. Ciphertext bytes are bit-identical to the previous implementation (verified against the pycryptodome reference). Dependency change:pycryptodome>=3.0.0→cryptography>=41.0.0.cryptographyis already a transitive dependency of Home Assistant core, so installed footprint shrinks slightly.
Fixed¶
- Bandit
B413and 5×B608warnings. B413 (PyCrypto deprecation) is now moot after the cryptography migration. B608 (SQL injection) findings onpanel_api.pywere false positives — column names come from a literal whitelist in the same function, and{table}is a closure-captured string literal ("producers","syrups","toppings"). Annotated with# nosec B608plus a comment explaining the invariant. The lint job now passes end-to-end.
[0.53.3] — 2026-05-24¶
Fixed¶
- CI green again. Three classes of breakage that piled up since 0.53.0:
- Hassfest translations rejected
<machine>placeholders inclock_entity_migration.description(parsed as HTML) — replaced with plainMACHINEacrossstrings.jsonand 29 translation files. - Hassfest rejected
target.devicefilter on thesync_clockservice (HA no longer supports device filters ontargetsince the validator started callingraise_on_target_device_filter). Moved to an optionalfields.device_idwith a properdeviceselector; behavior is unchanged (omit the field to sync all configured machines, same asrepair_connection). - Tests workflow missed
aiosqlite—tests/test_sommelier_db.pyandtests/test_review_fixes.pyfailed at collection. Added it to the test job's pip install line.aiosqlitewas already declared inmanifest.jsonrequirements; only the CI runner needed it. - Ruff lint cleanup: dropped unused
datetime.timeimport in__init__.py, unusedasync_generate_recipesimport insommelier_api.py, and split two;-joined statements inbutton.py(E702).
[0.53.2] — 2026-05-22¶
Fixed¶
- Recorder warning + DB bloat from oversized select attributes (#13). The Profile select exposes the full DirectKey recipe table (
directkey_recipes) and the Recipe select the full base-recipe table (recipes) live for the companion card/app — but these exceed the recorder's 16 KB per-state attribute cap, triggeringState attributes ... exceed maximum sizewarnings and DB performance degradation. Both bulk attributes are now marked_unrecorded_attributes, so the recorder skips them while the live state attribute is preserved — the card and app keep working unchanged, and the small per-recipe /active_profilefields stay in history.
[0.53.1] — 2026-05-22¶
Fixed¶
- False
pairing_wedgedrepair when the machine is powered off (issue #12). The reconnect loop counted every failed connect toward the wedge threshold regardless of whether the device was actually advertising. Turning the machine off between uses (a common pattern) racked up 5 consecutive failures and raised a bogus repair, plus spammed the log with hundreds of "Connection failed" errors. The loop now consultsbluetooth.async_address_present: a device that is not advertising is treated as powered off / out of range — the connect attempt is skipped, the wedge counter is cleared, and the loop waits quietly for the next advertisement. A genuinely wedged device keeps advertising, so real wedges are still detected and recovered.
[0.53.0] — 2026-05-21¶
Added¶
- Serial number sensor (
sensor.<machine>_serial) — read once on connect via theHLcommand (20-byte ASCII response). Confidence 0.95 from the Nivona protocol spec; size matches Melitta's frame registration, so the same opcode is expected to work across both brands. - BLE frame log in diagnostics —
diagnostics.pynow exposes two new sections: ble_trace.recent_frames_raw— last 100 raw notifications (pre-decryption hex preview, captured on every BLE event byble_client._on_notification).ble_trace.frame_log_decoded— last 200 decoded frames (post-RC4 payloads), captured byprotocol._dispatch_frame. Useful for inspectingHF(16 B) /HQ(15 B) /HP(14 B) — three opcodes whose payload semantics remain unresolved across all public sources.- DEBUG log line
[FRAME-UNH] cmd=X len=N hex=...for any decoded frame the integration does not actively handle (i.e. notHX, notHU, not a response to a pending request). Activated via the standard HA "Enable debug logging" toggle on the integration page.
Changed¶
device.serialis now included in the diagnostics download.
[0.52.0] — 2026-05-21¶
Added¶
- New
time.<machine>_clockentity — display and set the machine RTC directly from HA. - New service
melitta_barista.sync_clock— push HA local time to the machine on demand. - Auto-sync coordinator: writes the machine clock on BLE reconnect (with 12 h throttle) and once per day at a configurable time (default 03:17).
- Options Flow → Advanced:
auto_sync_clock(toggle),auto_sync_drift_minutes(skip threshold),auto_sync_daily_time(HH:MM).
Changed¶
docs/PROTOCOL.mdclock setting notes filled in with verified semantics.
Removed (BREAKING)¶
number.<machine>_clockandnumber.<machine>_clock_send(introduced in 0.20.0). They were two writable sliders for what is really a read-only / write-only pair. Migration: use the newtimeentity for manual changes, or thesync_clockservice for automations. A Repair card surfaces on upgrade.
[0.51.0] — 2026-05-15 — Pairing recovery — GA¶
Stable release of the pairing-recovery work that ran through beta.1 through beta.7. Real-device validated: the Force re-pair (hard) flow now resolves the issue #10 wedge end-to-end as long as the user puts the coffee machine into pairing mode before pressing Submit.
Required ESPHome action: pull the latest
esphome/ble-proxy-xiao-*.yamlfrom this repo and flash via the ESPHome dashboard. The recovery flow depends on three actions that ship in the YAML:clear_ble_bonds,disconnect_ble_peer, and thefactory_resetbutton. Without a reflash, the integration falls back to a partial recovery path.
Summary of what made it into 0.51.0¶
- Layered recovery for the issue #10 wedge:
- Settle delay between
pair=Falsefail andpair=Trueso the ESP has time to release the BLE socket (beta.1). disconnect()resets_paired(beta.1).- Counter for
_consecutive_connect_failures; auto-trigger of the repair routine after 5 consecutive failures (beta.1). - Soft repair: reload the ESPHome ConfigEntry to evict the cached
BLEDevicefromhabluetooth._previous_service_info(beta.1). melitta_barista.repair_connectionservice for manual trigger (beta.1).- Options Flow → Repair connection menu entry (beta.2).
clear_ble_bondsaction inesphome/ble-proxy-xiao-*.yamlto wipe the ESP NVS bond table (beta.3).- Robust proxy-entry matcher: tries
entry.unique_id,entry.data["bluetooth_mac_address"],device_info.bluetooth_mac_address,device_info.mac_address, anddevice_info.name(beta.4 + beta.5). The critical fix in beta.5: ESP32 chips have separate WiFi and BLE MACs (BT = WiFi + 2); we were comparing the wrong one. - Options Flow → Force re-pair (hard) menu entry — wipes ESP bond, surgical GAP disconnect of the peer, reloads the proxy entry, and re-arms the reconnect loop (beta.4).
disconnect_ble_peeraction in the proxy YAML to drop a stuckstate: ESTABLISHEDconnection slot (beta.6).- UI strings now spell out the manual pairing-mode step everywhere recovery is referenced (beta.7).
- Pairing-wedged Repair Issue in HA UI with a learn-more link to issue #10 and a 3-step recovery list. Auto-cleared on the next successful connect.
- Best-practice ESPHome YAML extras carried over from the
official
esphome/bluetooth-proxiesreference:factory_resetbutton (nuke the whole NVS),safe_modebutton (recovery boot),BLE bondstext sensor (live count fromesp_ble_get_bond_device_num),min_version: 2025.8.0,esp32_ble.max_connectionsraised to matchconnection_slots+1. - Tests:
tests/test_pairing_recovery.pycovers the settle delay, disconnect hygiene, counter increment/reset, repair callback firing at threshold, threshold=0 off switch, missing callback safety, public callback API, and all four Options Flow abort paths (repair done / partial / no-action / failed). 759 total tests passing. - Documentation: README now has an
ESPHome BLE proxy (recommended transport)section listing the YAML reference and the four buttons it provides, plus aBLE pairing recoverysection walking through the three escalating recovery paths. Pairing instructions inStep 3are updated to spell out that pairing mode is a one-time step per central.
Migration / upgrade notes¶
If you're coming from any 0.51.0-beta.*: nothing to change beyond
the manifest version bump. If you're coming from 0.50.x: pull the
latest esphome/ble-proxy-xiao-*.yaml and reflash your proxy as
described above. Without it, Force re-pair (hard) and the
melitta_barista.repair_connection service will fall back to the
soft path (reload ESPHome entry only) — that's enough for cache
eviction but does not wipe the ESP NVS bond.
[0.51.0-beta.7] — 2026-05-14 — Pairing-mode requirement documented¶
Real-device beta.6 testing surfaced the remaining piece: Melitta
firmware requires the machine to be in explicit pairing mode
(menu-activated) before it accepts SMP from a new BLE central. Even
with the ESP completely clean (no bond, no stuck slot, fresh BLEDevice
cache), the machine answers every SMP exchange with auth fail
reason=82 until you put it into pairing mode via its UI. After a
single successful pair the bond persists on both sides — pairing
mode is not needed for subsequent reconnects.
This isn't a code bug, but the integration was silent about it. beta.7 surfaces the requirement in every recovery message and in the repair issue's UI.
Changed¶
repair,full_pair, and the issuepairing_wedgeddescriptions now spell out the pairing-mode step (en + ru, plus the other 27 locales carrying the en text as placeholder).full_pair_doneabort text now says "IMPORTANT: now put the machine into pairing mode within 1 minute" so the user knows the next move.pairing_wedgedissue body lists the full 3-step recovery: pairing mode on the machine → call repair_connection → bond persists, done.
[0.51.0-beta.6] — 2026-05-14 — Surgical GAP disconnect on stuck slot¶
Production log on beta.5 showed auth fail reason=82 (SMP rejection
from the machine) continuing even after clear_ble_bonds wiped
both ESP NVS bonds. The bluetooth_proxy connection slot then sits in
state: ESTABLISHED and the next line in the log is
Connection request ignored, state: ESTABLISHED — every subsequent
client-side connect is dropped on the floor.
Wiping the bond table is necessary but not sufficient. We also need to drop the half-closed GAP link so the next pair=True actually opens a new SMP exchange.
Added¶
- New ESPHome action
disconnect_ble_peerin bothesphome/ble-proxy-xiao-c6.yamlandesphome/ble-proxy-xiao-s3.yaml. Takes apeer_macstring variable and callsesp_ble_gap_disconnect(bd_addr)on it. Logs the API return code. _async_force_repairnow callsesphome.<proxy>_disconnect_ble_peerwith the machine's MAC immediately afterclear_ble_bonds. New result keypeer_disconnectedindicates whether the GAP-disconnect ran.
What this fixes in the user-visible flow¶
Before beta.6, after issuing "Force re-pair" you would see in the ESP log:
[ble_bonds] Removed 2 bonded device(s)
... auth fail reason=82
[bluetooth_proxy] Connection request ignored, state: ESTABLISHED
[ble_bonds] Removed 2 bonded device(s)
[ble_disconnect] GAP disconnect F1:2C:72:3F:75:ED -> 0
... clean reconnect with fresh SMP
Note for users hit by issue #10¶
If the machine ALSO holds a stale bond (which the log above suggests), clearing the ESP side alone isn't enough — the machine refuses every new SMP request because its remembered LTK doesn't match what we present. Look in the machine menu for Settings → Bluetooth → Disconnect / Reset connection to forget its side. Some Melitta TS firmwares require a power-cycle of the machine after that for the state to actually persist.
[0.51.0-beta.5] — 2026-05-14 — Proxy matcher: use BT MAC (not WiFi MAC)¶
Fixed¶
beta.4's robust matcher still missed every proxy in the wild because
of a subtle ESP32 hardware detail: every ESP32 has separate WiFi
and Bluetooth MAC addresses (BT = base + 2). The scanner's
source field is the BT MAC. ESPHome's device_info.mac_address
and entry.unique_id, however, are the WiFi MAC. So the matcher
compared two MACs that were always different by a fixed offset and
never lined up.
The matcher now also checks:
- entry.runtime_data.device_info.bluetooth_mac_address — the
ESPHome-reported BT MAC at runtime.
- entry.data["bluetooth_mac_address"] — the value ESPHome
persists at discovery/reconfigure time (manager.py:563-567 in
HA core), covers the case where the proxy entry is mid-setup
and runtime_data isn't populated yet.
Verified against Home Assistant developer docs (scanner.source
is documented as "source MAC address") via context7. Confirmed
no public API exposes a scanner → config_entry_id reverse lookup;
matching by MAC keys is the supported pattern.
[0.51.0-beta.4] — 2026-05-14 — Force re-pair option + robust proxy matcher¶
Added — Options Flow "Force re-pair (hard)"¶
A second menu entry under Configure that does, in order:
- Disconnect the Melitta client.
- Find the ESPHome proxy ConfigEntry that owns the scanner for this peer.
- Call the
esphome.<proxy_name>_clear_ble_bondsservice if it exists (i.e. the user has wired theclear_ble_bondsaction fromesphome/ble-proxy-xiao-c6.yaml— beta.3 introduced this). - Reload the proxy ConfigEntry (evicts HA-side cached BLEDevice).
- Re-arm the reconnect loop.
5 localised abort outcomes: full_pair_done, full_pair_partial,
full_pair_no_action, full_pair_local_only, full_pair_failed.
Fixed — Proxy-entry matcher kept missing valid proxies¶
_find_proxy_entry_for_address used to compare scanner.source only
against entry.unique_id. When the ESPHome entry was added via zeroconf
discovery (or reconfigured later) the unique_id could drift from the
proxy's actual MAC, and the integration would return None even though
the ESPHome proxy clearly was advertising the machine. Result: every
Repair / Force re-pair call fell back to the local-adapter path with the
"No ESPHome proxy found" abort, even though one existed.
The matcher now compares the (normalised) scanner source against three
keys per ESPHome entry: entry.unique_id,
entry.runtime_data.device_info.mac_address, and
entry.runtime_data.device_info.name. Also logs the source UUID and the
candidate count at DEBUG so a future mismatch can be diagnosed without
a code change.
Tests¶
5 new tests in tests/test_pairing_recovery.py for the Force re-pair
abort paths (done / partial / no-action / local-only / failed). 759
total passing.
[0.51.0-beta.3] — 2026-05-14 — ESP-side bond clearing recipe¶
beta.1 and beta.2 reload the ESPHome scanner to evict the HA-side
cached BLEDevice. That fixes the wedge when the cache points at a
dead source UUID — but it does NOT touch the ESP-side bond
table (LTK stored in NVS flash on the proxy). If the ESP firmware
holds a stale bond key and the machine reset its internal SMP state,
the proxy keeps presenting an LTK the machine refuses to acknowledge,
and every pair=True lands on the same rejection.
client.unpair() only fixes that when the ESP firmware was built
with the unpair feature flag. Many community-built proxies don't
have it, so the call returns BluetoothConnectionDroppedError and
the bond stays forever.
Added¶
esphome/ble-proxy-xiao-c6.yamlnow ships anapi.actions:block with aclear_ble_bondsaction that callsesp_ble_remove_bond_devicefor every entry in the ESP bond table. After flashing the proxy, HA exposes the action as the serviceesphome.<proxy_name>_clear_ble_bonds. Calling it once resets the NVS bond table; the nextpair=Truetriggers a fresh SMP exchange and the machine creates a new bond from scratch.
Changed¶
_try_unpairnow logs a WARNING (with concrete service name) whenclient.unpair()fails on the ESPHome path, pointing users at theclear_ble_bondsworkaround instead of just hiding the failure in DEBUG.
Recovery workflow when handshake stays wedged¶
If the integration's auto-recovery / repair_connection does not
fix the red-indicator state, the bond on the ESP is the next thing
to clear:
- Update your ESPHome proxy YAML with the
clear_ble_bondsaction fromesphome/ble-proxy-xiao-c6.yaml(api → actions block). - Flash the proxy (OTA from the ESPHome dashboard is fine).
- Developer Tools → Services →
esphome.<your_proxy_name>_clear_ble_bonds→ Call. - Settings → Devices & Services → Melitta entry → Configure → Repair connection → Submit.
Step 3 wipes the NVS bond on the ESP; step 4 evicts the cached
BLEDevice and forces our _connect_impl to start from pair=False
(which now fails fast because there is no bond) then escalate to
pair=True, this time provoking a fresh SMP exchange.
[0.51.0-beta.2] — 2026-05-14 — Repair step in Options Flow¶
Builds on beta.1. Same recovery routine, now also exposed as a UI button — no need to remember the service name.
Added¶
- "Repair connection" entry in the integration's Options Flow menu
(Configure → Repair connection → Submit). Calls the same
_async_repair_pairingroutine that the service and the auto- trigger use. Three abort outcomes with localised messages:repair_proxy_reloaded(an ESPHome entry was reloaded),repair_local_reconnect(no proxy found — fell back to a local disconnect + advertisement wait),repair_failed(the routine raised — see HA logs). - 4 new tests in
tests/test_pairing_recovery.pycovering the three abort paths and the initial form display.
Translations¶
step.repair and abort.repair_* blocks added to all 29 locale
files (en + ru real translations; 27 placeholders carry the EN text).
[0.51.0-beta.1] — 2026-05-14 — Pairing recovery (PRE-RELEASE)¶
Pre-release. Must be installed explicitly from HACS by toggling "Show beta versions" on the integration page. Targets issue #10: after long BLE silence the encrypted HU handshake gets stuck, the machine displays a red Bluetooth indicator, and the only known recovery used to be removing and re-adding the integration via UI.
This release adds a 3-layer fix; each layer is independent so partial backports work.
Root cause (full write-up: see docs/PAIRING.md after this commit)¶
habluetooth's BaseHaRemoteScanner._previous_service_info caches the
BLEDevice instance per peer address with a frozen
details["source"] / details["address_type"]. After long quiet
periods the cached source can point at a dead scanner UUID (e.g.
the ESP proxy reconnected to HA between sessions), or the address
type can drift after the machine resets its bond. Every reconnect
attempt then hands establish_connection that stale BLEDevice, the
HU response never finds its way back, and the machine displays red.
hass.config_entries.async_reload(melitta_entry) does NOT clear that
cache (the scanner lives on the ESPHome entry, not ours).
Deleting the integration entry happens to work because by the time
the user finishes the config flow, the long pause has caused
_previous_service_info[address] to expire on its own — the next
advertisement then builds a fresh BLEDevice with current source /
address_type.
Added — Layer 3 (root-cause recovery)¶
melitta_barista.repair_connectionservice. Walks every melitta config entry, finds the ESPHome config entry that owns the proxy scanner for that peer (viabluetooth.async_scanner_devices_by_addressand matchingscanner.sourceagainstConfigEntry.unique_id), and reloads the ESPHome entry. The reload unregisters the scanner, which is the only HA-public path that evicts the cached BLEDevice. The next advertisement builds a fresh one and the nextpair=Truesucceeds in ~1 s.- Automatic recovery: after
DEFAULT_REPAIR_AFTER_FAILURES = 5consecutive failedconnect()calls the reconnect loop calls the same routine without user action. Counter resets on every successful connect; threshold can be set to 0 to disable (useful for debugging the underlying transport). - Repair issue (
pairing_wedged_<address>) raised at the same threshold so the user gets a UI card explaining what happened and pointing at issue #10 for logs. Auto-cleared on the next successful connect. - New constants in
const.py:DEFAULT_PAIR_SETTLE_DELAY = 2.0,DEFAULT_REPAIR_AFTER_FAILURES = 5.
Added — Layer 1 (paint-the-bike-shed timing fix)¶
- 2-second settle delay in
_connect_implbetween a failedpair=Falsehandshake and the nextpair=Trueattempt, and again between_try_unpair()and the finalpair=True. Without this gap the ESP proxy / BlueZ does not always release the previous BLE socket before we re-pair, manifesting as the 60-secondTimeoutAPIError waiting for BluetoothDevicePairingResponseusers see in logs. Configurable via the newpair_settle_delayctor arg.
Added — Layer 2 (hygiene)¶
disconnect()now resetsself._paired = False. Field was tracked on connect but never cleared — kept the bond-state mental model inconsistent across reconnects.
Fixed¶
- Reconnect-loop now tracks
_consecutive_connect_failuresand resets it on every successfulconnect(). Surfaces as a public propertyconsecutive_connect_failuresfor diagnostics.
Tests¶
tests/test_pairing_recovery.py— 10 new tests covering the settle delay invariant (no sleep on success, sleep between attempts on failure), disconnect hygiene, counter increment / reset, callback firing exactly at threshold, threshold=0 off switch, missing callback not crashing the loop, and the public callback API.
Known limitations¶
- The recovery does not auto-tune itself: if the ESPHome proxy keeps
re-acquiring stale state quickly (e.g. flaky power), the user will
see repeated reload spikes. The
repair_after_failuresknob is exposed for diagnostics but not yet wired through Options Flow. - The repair routine assumes one ESPHome entry per peer address. A setup with multiple proxies all hearing the same machine will only reload the first matching entry per cycle.
- No
melitta_barista.repair_connectiontarget selector in the services dialog yet — the service applies to every melitta entry it finds.
[0.50.2] — 2026-05-14 — hassfest validation fixes¶
Closes the three findings raised by the nightly validate-hassfest
CI job (these were broken before 0.50.1 — the audit didn't surface
them because the agents reviewed source, not CI logs).
Fixed¶
strings.json+ 29 translation files: removed the top-level"brand": {melitta, nivona}block. HA's translations schema does not allow a top-levelbrandkey, so every nightly hassfest run was failing. The block was dead code anyway — notranslation_key="brand"lookup exists in the integration.manifest.json: declaredhttpindependenciessince__init__.py:131callshass.http.async_register_static_paths()to mount the panel SPA assets.
[0.50.1] — 2026-05-14 — Code review: critical security + crash fixes¶
Closes the full Critical list, all Important findings (including the BLE test gaps and the i18n / enum-ID rendering), and the Minor pack from the v0.50.0 code review. 740 tests passing (was 721).
Security / authorization¶
- Admin guard on every mutating WebSocket command in panel_api.py and
sommelier_api.py (31 handlers in total) plus sensitive reads
(diagnostics, diagnostics/llm_calls, prompts/list, prompts/preview).
require_admin=Trueon the panel registration only hides the sidebar entry — without these guards, any authenticated household user could change the system prompt template, the LLM agent setting, or physically start a brew viasommelier/brew. safeHttpUrl()for the producer website link in melitta-beans.js so ajavascript:-scheme URL stored in the producers table cannot execute when the row is rendered.rel="noopener noreferrer"added on thetarget="_blank"link.- Allowlist for
sommelier/settings/setandsommelier/preferences/setkeys viavol.In(...). Previously a caller could overwrite the sharedsettings.schema_versionrow and break future migrations. - WS error responses now log full exception traces server-side and return a static message to the panel — SQLite paths and conversation-integration error text no longer leak via WS errors for /producers/add, /beans/autofill, /sommelier/generate, /sommelier/brew, /sommelier/favorites/brew, /sommelier/presets/list.
Correctness¶
sommelier/profiles/activateno longer crashes. The handler calleddb.async_activate_profile()which never existed; renamed to the actualasync_set_active_profile(). The DB method now returnsboolso the not-found path is reported correctly to the panel.ws_profiles_add: the old code called the DB method with kwargsname=..., preferences=...that the method never accepted — every add-profile call raised TypeError. The handler now builds a single data dict so the DB row + the nested preferences both land in storage.- Schema migration: SCHEMA_VERSION 2 → 3 with a new MIGRATE_V2_TO_V3
block that adds a
steps TEXTcolumn togenerated_recipesandfavorites. The headline new feature of 0.50.0 — numbered preparation steps with dosages — used to be dropped on reload and on brew-from-favorite. Migration runner now applies each version step in sequence instead of a hardcoded v1→v2 jump. ws_favorites_addforwardsextras,steps, andcup_typefrom the source recipe (they used to be dropped)._find_clientresolves through entity registry'sconfig_entry_idinstead of substring-matching the BLE address into the entity unique_id — eliminates a multi-machine false match path.async_unload_entryremoves all six services (brew_freestyle,brew_directkey,save_directkey,reset_recipe,confirm_prompt,nivona_write_recipe_param,nivona_write_mycoffee_param) on last-entry removal so they don't leak in HA's service registry until restart. Gated onunload_okso we don't tear down shared state while platform entities are still live.MelittaTotalCupsSensor.availablenow gates on the connection state. It used to returnTrueforever after the first read, masking BLE drops from automations.services.yaml:enabledandiconadded to thenivona_write_recipe_paramparam_key selector (the voluptuous schema already accepted them).config_flow: explicit WARNING before the BleakScanner.discover fallback so ESPHome-proxy-only setups can see why discovery returned empty results._auto_confirm_taskis now tracked with a done callback so any unexpected exception surfaces in HA logs instead of being swallowed by asyncio's "task exception was never retrieved".
Frontend i18n + a11y¶
melitta-sommelier.js: every user-visible label, toast, and diagnostics message now resolves through_t()(was a mix of hardcoded Russian and English). Constraints block, Add-ins section, favorite toast, machine-line label, Why?-summary, plus the 26 enum values for cup-size / mood / occasion / temperature / caffeine / dietary preferences. en + ru keys added.melitta-beans.js: the autofill-brewing-recommendation note that gets appended into the bean composition field goes through_t(beans.brewing_label)— earlier it wrote literal "Заваривание: …" into the row regardless of locale.melitta-additives.js/melitta-beans.js:confirm("Delete?")now uses a localized prompt.melitta-modal.js: bindaria-labelledbyon the dialog role to the<h3>id so screen readers announce the modal title.melitta-diagnostics.js: null outthis._timerafterclearInterval, same pattern as melitta-status.js.
Tests / tooling¶
tests/test_review_fixes.py— regression coverage for steps round-trip,async_set_active_profilereturn contract, settings / preferences allowlist schemas,safeHttpUrltext contract, and the service-removal block in init.py.tests/test_protocol_full.py— split-BLE-notification parser tests (frame cut mid-payload + byte-by-byte feed).tests/test_ble_client.py—_auto_confirm_taskerror-path coverage (BleakError caught; unexpected exception routed to the done-callback).pyproject.toml— pytesttimeout = 10default per the project memory rule.
Internals¶
- Removed dead
_CRC_TABLE/_compute_handshake_crcfrom protocol.py; live code routes throughMelittaProfile.hu_verifier. _BleClientProtocoltyping stub now declares_brand,_capabilities,_profile_callbacks,_recipe_refresh_callbacks, andrecord_errorso mypy sees the full mixin contract.ai_recipes._validate_extrasdrops the hardcoded English VALID_SYRUPS / VALID_TOPPINGS / VALID_LIQUEURS allowlists — the LLM is informed of the user's actual extras via the prompt and Pydantic accepts any string. Kept normalisation (strip, lowercase, 64-char cap).- Docstrings added on the 13 WS handlers in panel_api.py that were missing them.
Known limitations carried over¶
- WS handlers' end-to-end coverage with a full HA harness is still out of scope; the new regression tests target DB invariants and text contracts of the critical fixes.
- Some recipe metadata (
estimated_caffeine, badges) still formats English-only on the recipe card.
[0.50.0] — 2026-04-27 — Admin SPA panel + AI Coffee Sommelier (alpha)¶
Fixed¶
sommelier/profiles/activateno longer crashes. Handler calleddb.async_activate_profile()which never existed; renamed to the actualasync_set_active_profile(). The DB method now returnsboolso the not-found path surfaces correctly to the panel.- Admin guard on all mutating WebSocket commands.
require_admin=Trueon the panel registration only hides the sidebar; the WS endpoints themselves had no authorization check, so any authenticated household user could change prompt templates, modify the LLM agent setting, or physically start a brew viasommelier/brew. Added@websocket_api.require_adminto all CRUD endpoints (producers, beans, syrups, toppings, tags, prompts, milk, hoppers, favorites, history config, profiles, sommelier preferences/extras/settings, generate, brew, autofill) plus sensitive read endpoints (diagnostics, diagnostics/llm_calls, prompts/list, prompts/preview). Read-only data endpoints (status, list, get) remain open. javascript:URI XSS on producer website link. Producer rows rendered<a href=${p.website} target="_blank">with a user-stored value. Ajavascript:website would execute in the HA frontend origin (with access to the WS token). AddedsafeHttpUrl()that only returns the URL if it parses as http/https, plusrel="noopener noreferrer"on the link.- Services no longer leak after the last entry is removed. Six
services (
brew_freestyle,brew_directkey,save_directkey,reset_recipe,confirm_prompt,nivona_write_recipe_param,nivona_write_mycoffee_param) were registered with ahas_serviceguard but never deregistered; when the last config entry was removed they stayed in HA's service registry until restart and would reportdevice_not_foundon every call. - Domain-wide teardown now gated on
unload_ok. Panel unregistration, Sommelier DB close, and service removal are now only executed when the platform unload actually succeeded — they used to run unconditionally even if entities were still live.
[0.50.0] — 2026-04-27 — Admin SPA panel + AI Coffee Sommelier (alpha)¶
Big release. The integration now ships an in-HA admin panel with a full Sommelier workflow that goes from "I have these beans + this milk + this mood" to a one-tap brew on the machine.
Added¶
- Admin SPA panel in the HA sidebar (
/melitta-barista). Built on vendored Lit 3.x (no HACS-card side effects required), localised en + ru, panel module URL is cache-busted by the integration version. - Tabs: Status (live BLE + machine snapshot), Diagnostics (ring-buffered errors + frames + recent LLM calls with full prompt / raw response / validation errors), Recipes (DirectKey viewer), Beans (producers + beans CRUD with dynamic flavour tags + LLM autofill + hopper assignment), Add-ins (syrups / toppings / milk via a unified modal), Sommelier, Settings.
- AI Coffee Sommelier (alpha) — end-to-end:
- Rich form: allowed syrups / toppings / milk multi-selects, mood multi-select, cup size, occasion (auto-suggested from the local clock), temperature, caffeine, dietary multi-select.
- Hybrid structured-output pipeline. SmartChain agents go through that integration's native JSON Schema mode (OpenAI Structured Outputs / Gemini responseSchema / Anthropic tool-use / Ollama 0.5+ format=schema). All other agents go through a Pydantic-validated text-with-retry path with the JSON Schema appended to the prompt.
- Locale-aware prompt:
hass.config.languageis forwarded so names / descriptions / step instructions come back in the user's language; enum values stay English so validation works regardless. - Recipes carry a complete numbered preparation sequence with
explicit dosages (
1. Brew espresso — 30 ml,2. Add Vanilla syrup — 15 ml, …) on top of the machine portion. ★ to favourite, "Brew this" to send the freestyle payload. - Diagnostic transparency: every LLM round-trip is recorded and visible in the Diagnostics tab (full prompt, response, validation errors, the path that handled it).
- Beans LLM autofill: brand + product + producer URL → strict Pydantic-validated bean fields (roast / bean_type / origin / origin_country / flavor_notes / composition / brewing recommendation). Any agent works; the URL is passed as a hint that browsing-capable agents follow on their own.
- Settings tab: LLM model picker, prompt template editor with inline placeholders documentation and "Preview assembled prompt" showing the exact text that will be sent.
- Diagnostics tab: ring-buffered BLE errors + notification frames with consecutive-duplicate collapsing, full LLM call log, configuration snapshot.
Changed¶
sommelier_apiflavor_notes / milk_types schemas relaxed from hardcoded English vocabularies to free-form string lists. Russian / brand-specific names work everywhere now.- Status tab uses a compact single-line label-value layout.
- Bean / producer / additive saves use explicit writable-field
allowlists (no more spreading the whole record back, which tripped
voluptuous extra-keys validation on
created_at/updated_at).
Fixed (along the way)¶
- HA WS payload key collision:
idis the framework's message id, not a row pk. Renamed toproducer_id/additive_id/bean_idin all panel-side schemas. - Hopper dropdown lost selection on tab switch — Lit's
.value=on<select>races with option rendering; switched to per-option?selected. customElements.define()registration race on panel re-import: every component file now guards withif (!customElements.get(...)).
For users¶
- Backwards compatible. Existing config entries continue to work as before; the panel adds a sidebar entry and a couple of new per-domain WS commands.
- HACS will pick up 0.50.0 as a normal upgrade — the alpha label is scoped to the Sommelier feature, not the integration as a whole. Reload the browser hard (Ctrl+Shift+R) once after the update so the panel module URL refreshes its cache.
[0.49.7] — 2026-04-27 — Fix HA startup blocking + bleak-retry-connector warning¶
Bug fix release addressing issue #9.
Fixed¶
- HA startup no longer blocked by our reconnect loop.
async_setup_entrywas scheduling_async_connect_and_poll(an infinitewhile True:reconnect loop) viahass.async_create_task, which puts the task intohass._tasksand makes HA's bootstrap wait for it during theEVENT_HOMEASSISTANT_START→runningtransition. With an unreachable / slow machine this surfaced as theSetup of domain melitta_barista is taking over 10 minuteswarning and a delayed "started" state for HA itself. Switched tohass.async_create_background_taskwhich is exactly the right primitive for never-returning monitor loops. habluetooth.wrapperswarning eliminated. The connect path used to fall back to rawBleakClient.connect()whenever the cachedBLEDevicewasNone(typical on a cold boot via ESPHome BLE proxy) or wheneverestablish_connection()raised — which triggered:
BleakClient.connect() called without bleak-retry-connector. For reliable connection establishment, use bleak_retry_connector.establish_connection().
Inside HA we now always route through
bleak_retry_connector.establish_connection(). Without a cached
BLEDevice the call raises BleakError so the reconnect loop waits
for the next advertisement (via set_ble_device /
_reconnect_event) instead of burning a 30 s timeout per attempt
on a raw connect. Without bleak_retry_connector (tests / CLI
scripts) the raw fallback still works.
Why it matters¶
The two bugs reinforced each other: the raw BleakClient.connect()
fallback grabbed BlueZ slots without coordination, and the
async_create_task choice made the resulting slow connect cycles
visible as a startup hang. After this release a cold boot with the
machine off / out of range completes the integration setup
immediately and the reconnect loop runs quietly in the background
until the first advertisement arrives.
Changed¶
tests/test_ble_client.py::TestEstablishConnection: rewrote two tests that previously locked in the raw-fallback antipattern. The new tests assert thatBleakErrorpropagates and that a missingBLEDeviceraises instead of silently falling back.
[0.49.6] — 2026-04-15 — Documentation site (MkDocs Material)¶
Documentation infrastructure.
Added¶
- MkDocs Material site at https://dzerik.github.io/melitta-barista-ha/
serving the committed docs (
docs/BLE_ARCHITECTURE.md,docs/PROTOCOL.md,docs/adr/001-...md) plus an auto-included changelog. Mermaid diagrams render natively, full-text search, dark / light toggle, edit-on-GitHub links. - GitHub Actions workflow (
.github/workflows/docs.yml) auto-deploys on push tomainwhenever README, CHANGELOG,docs/, ormkdocs.ymlchange. mkdocs.ymlexclude_docs:whitelist guards local-only RE / audit notes from being published if a contributor runsmkdocs buildlocally with those files present.
Changed¶
- README header gains a docs-site link near the top.
.gitignorenow excludes/site/and.cache/.
[0.49.5] — 2026-04-15 — Docs: BLE name formats + companion app/card scope¶
Documentation-only patch.
Changed¶
- README and docs (
multi-brand-architecture.md,adr/001-...md,NIVONA_HA_INTEGRATION_AUDIT.md) now describe all three observed Nivona advertisement formats: legacyNIVONA-NNN-----, bareNNN-----, and the 15-digit no-dash serial form (e.g.930254000000000) seen on real NICR 930 / firmware0254A013A10. Previously docs only mentioned the legacy form, even though the regex was broadened in 0.49.1. - README marks the Custom Lovelace card and Standalone PWA as Melitta only — both companion projects assume Melitta-shaped entities (HC/HJ extensions, named cup counters, profile selects) and don't yet render Nivona's per-family stats / brew override layout.
[0.49.4] — 2026-04-15 — README + repo description: full Nivona scope¶
Documentation-only patch.
Changed¶
- README intro and Features bullet now spell out the full Nivona family list — NICR 6xx / 7xx / 79x / 9xx / 1030 / 1040 plus NIVO 8xxx — instead of the misleading "Nivona NICR/NIVO 8xxx". The supported-families table further down was already correct; only the headline copy understated coverage.
- GitHub repo description updated to match. Also dropped "AI Coffee Sommelier" from the repo blurb pending the recipe → brew handoff (see 0.49.3 README clarification).
- Mention that NICR 930 is now validated on real hardware (PR #7, Cyrill).
[0.49.3] — 2026-04-15 — README: clarify AI Sommelier WIP status¶
Documentation-only patch.
Changed¶
- README marks AI Coffee Sommelier as work-in-progress: the WebSocket API, persistence, bean catalog, and conversation-agent prompt building are functional, but the recipe → Freestyle brew handoff is not yet wired up — generated recipes can be inspected but not brewed with one tap. Section reorganized into "Currently working" / "Not yet working" / "Planned end-to-end flow".
- Project tagline no longer lists "generate AI recipes" as a shipped feature; references the Sommelier section for current scope instead.
[0.49.2] — 2026-04-15 — NICR 930 follow-up: cleanup of PR #7¶
Quality follow-up to 0.49.1 — same external behavior on NICR 930 plus regressions fixed.
Fixed¶
- Brew button respects override sliders again. PR #7 removed the
override-collection loop because
payload[5]=0x01brewed with zeros when no temp-recipe HW writes happened. The new implementation flags eachNivonaBrewOverrideNumberasuser_setonly when the user actually moves the slider; the brew button forwards just those fields, so unchanged sliders fall through to the machine's saved recipe defaults. is_readystrict again — the global relaxation in 0.49.1 could let Melitta brews fire in states they shouldn't. TheMOVE_CUP_TO_FROTHERtolerance is now declared per-family onMachineCapabilities.tolerated_brew_manipulationsand applied via a newis_ready_for_brew(tolerated)helper. Only Nivona 9xx / 9xx-light opt in.
Changed¶
EugsterProtocol.start_process_nivonagained an explicituse_temp_recipe: boolparameter — replaces the overloadedchilledflag that PR #7 was reusing to mean "use saved defaults".payload[5]is now0x00when eitherchilledornot use_temp_recipe. Docstring updated.
Tests¶
+18 unit tests covering the override pipeline, the new readiness
helper, the per-family tolerated-flag declaration, and the byte
layout of start_process_nivona. 721 total (+18 from 0.49.1's 703).
[0.49.1] — 2026-04-15 — Nivona NICR 930 support (PR #7 by @Cyrill)¶
Fixes for NICR 930 (family 900), validated on real hardware (firmware 0254A013A10).
Fixed¶
- BLE name regex now accepts the 15-digit no-dash advertisement
form (
930254000000000) observed on real NICR 930. Previously the regex required\d{10}-----and brand detection fell through to Melitta, breaking the HU handshake. - Stats enabled for families
900/900-light— the_STATS_900table was already populated but gated behindsupports_stats=False. - Capabilities resolved at setup time via the HA bluetooth
scanner cache + BLEDevice advertisement-name fallback. Previously
client.capabilitieswasNonewhen entity platforms ranasync_setup_entry(BLE not connected yet), so no stat / setting entities were created. - Brew defaults now use
payload[5]=0x00(saved recipe defaults) when no overrides are pre-written via HW; previously0x01caused the machine to brew with zeros. is_readyno longer rejectsMOVE_CUP_TO_FROTHER— this flag was observed to persist after a completed brew on some Nivona models and blocked subsequent brews.
Known limitations (addressed in 0.49.2)¶
- Nivona brew button currently ignores user-facing override number entities (strength / coffee_amount / temperature / milk_amount).
is_readyrelaxation is global (also affects Melitta).
[0.49.0] — 2026-04-14 — Nivona accuracy pass + new entities for every family¶
Closes the prioritised findings of docs/NIVONA_HA_INTEGRATION_AUDIT.md.
The biggest item is a critical fix: NivonaBrewOverrideNumber no
longer corrupts the persistent standard-recipe slots on real
hardware. Most of the rest is bringing per-family stats / settings
into line with what the machines actually expose.
Fixed (CRITICAL)¶
- Temp-recipe HW writes go to the dedicated 9001-based register
instead of the persistent
10000 + selector*100 + offsetslot. The previous code permanently rewrote the standard recipe definition on the machine whenever a user adjusted a Nivona override number entity. New flow announces the recipe class at register 9001 first, then writes per-field offsets into the same temp slot, then issues HE — matching how the official app builds a temporary recipe.
Fixed (HIGH)¶
- Stat tables now exist for
600,900,900-light,1030,1040— these previously rendered zero stat sensors on any Nivona of that family. New tables expose 7–24 recipe / cumulative counters plus the universal 600/610/620/640 maintenance gauges per family. _STATS_700no longer over-includes IDs 213-221 that don't exist on 700-family hardware (would have shown 7 broken sensors)._STATS_79Xrebuilt — adds 202 Lungo, drops the spurious 213, adds the universal maintenance gauges; selector 4 (Cappuccino) remains absent, matching real 79X hardware.strength_levelscorrected for 8 NICR models (660 / 670 / 675 / 680 / 768 / 769 / 778 / 779) from 3 to 5 — previously truncated the strength dropdown for these owners.- HX parser is
>hhhhon the Nivona side — bytes 4-5 are a single 16-bit Message field, not info(U8)+manip(U8). Old shape worked for currently observed Message values 0/11/20 but would have silently lost any future ≥256 value. - NICR 8107 chilled recipes — selectors 8/9/10 are now exposed via the recipe select on NICR 8107 entries; HE flag byte switches to 0x00 (chilled) for those selectors.
Added — settings¶
_SETTINGS_900expanded from 3 → 11 entries (tank lighting accents, save_energy, touch lock, AutoOn deactivated + hours/minutes pair)._SETTINGS_900_LIGHTexpanded from 2 → 6 entries (save_energy, AutoOn-deactivated + pair)._SETTINGS_1030and_SETTINGS_1040expanded from 7/10 to 14/17 entries (cup heater, milk-products toggle, direct-start-deactivated, touch lock, AutoOn pair)._SETTINGS_79Xis now its own table (not the 700 alias) — drops id 103 (off-rinse) which 79X hardware does not expose.- NICR 758 drops setting 106 (profile) via a new per-model filter — that specific model omits the aroma-balance feature and HR-reading id 106 would NACK on real hardware.
Added — entity wiring¶
- New
BrandSettingNumberinnumber.pyfor options-less capability settings (auto_on_hours / auto_on_minutes; future numeric settings).BrandSettingSelectinselect.pyis now gated to descriptors that carry an options list — previously it would have crashed on options-less entries.
Changed — docs / hygiene¶
Manipulationenum docstring flags values 1-6 as Melitta-derived (only 0 / 11 / 20 are observed identical on Nivona); future per-brand overrides may rebind 1-6.confirm_promptdocstring spells out the fire-and-forget contract — a False return is "the write didn't ACK", not "the prompt is still showing"; callers should poll HX for authoritative state.reset_recipe_defaultswallowsFeatureNotSupportedon the HC re-read step so calling HD on a Nivona machine no longer raises.fluid_write_scale_10reverted to False on 900 / 900-Light — the ×10 scaling assumption was unverified by observed behaviour.
Translations¶
- All entity translation keys (sensor / select / number) added to
strings.json; the same keys mirrored into all 29 translation files with English fallback. Native translations for the new strings will land as community contributions.
[0.48.1] — 2026-04-14 — Decouple emulator versioning¶
Policy change: the ESP32 BLE emulator under esp_emulator/ now
has its own independent version (esp_emulator/VERSION) and its own
changelog (esp_emulator/CHANGELOG.md), and will be tagged separately
as emu-v<MAJOR>.<MINOR>.<PATCH>. The HA-integration version in
manifest.json no longer bumps for emulator-only changes and vice
versa. This commit itself contains the emulator Phase A work, released
as emu-v0.2.0; see the emulator changelog for details. From
emu-v0.2.0 onwards the two projects move independently.
Added¶
docs/NIVONA_RE_NOTES.md— living scratch-pad for per-family Nivona protocol findings (Phases A→H of the emulator roadmap). Each fact is sourced to a specific external-reference line so later contributors can re-verify.esp_emulator/VERSION+esp_emulator/CHANGELOG.md— independent versioning channel for the emulator.
[0.48.0] — 2026-04-14 — Show brand & model at discovery time¶
Added¶
- Discovery picker now shows brand + model, not just the raw
advertisement local_name. Instead of
"8107000001----- (MAC)"you see"Nivona NICR 8107 · 8107000001----- · MAC"— resolved at advertisement time (no BLE connect required) via the new_describe_advertisement()helper. - Bluetooth-confirm + pair forms list the resolved brand, model, raw advertisement name, and MAC before you commit to pairing, so a misdetection is caught before the config entry is created.
- Config-entry title and
CONF_NAMEdefault to the brand + model (e.g."Melitta Barista TS Smart"/"Nivona NICR 8107") instead of the raw advertisement name. The device shows up in Home Assistant's device registry under the friendly name straight away — no manual rename required.
Changed¶
- strings.json bluetooth_confirm / pair descriptions gained
{brand}/{model}/{address}placeholders alongside{name}; all 29 translation files updated with native-language labels (Marke/Modell, Marque/Modèle, Марка/Модель, Μάρκα/Μοντέλο, Zīmols/Modelis, …) —tr,sv,el, etc. all localised. - Direct-scan fallback in
_async_discover_devicesalso matches"nivona"substring and delegates todetect_from_advertisementso discovery picks up both brands uniformly.
[0.47.2] — 2026-04-14 — Fix Nivona brand detection for bare-serial advertisements¶
Fixed¶
- Emulator and real Nivona machines were being misdetected as
Melitta. The Nivona
ble_name_regexstill required the legacy"NIVONA-"prefix (^NIVONA-\d{10}-----$), but real machines (and therefore the emulator, as of v0.45.0) advertise the bare serial form"8107000001-----"so the official Nivona Android app can derive the model code viaSubstring(0, 4). The regex never matched,detect_from_advertisementreturned None, andMelittaProfile(the default) was picked — entities appeared under "Melitta" manufacturer and process-code parsing fell back to Melitta's 2/4 table. - Regex now accepts both forms:
^(?:NIVONA-)?\d{10}-----$. Trailing 5-dash suffix remains the distinguisher from Melitta's8xxx + hexadvertisement. - Direct-scan fallback in
config_flow._async_discover_devicesnow also delegates todetect_from_advertisement(in addition to the legacy Melitta substring checks) and matches"nivona"in the BLE name.
[0.47.1] — 2026-04-14 — Highlight the ESP32 BLE emulator in README¶
- Added a Features bullet and a dedicated
## ESP32 BLE Emulator (unique)section in README.md describing the bundled ESP-IDF firmware (esp_emulator/) that impersonates a real Nivona machine at the BLE layer — byte-exact ADV, AD00 GATT, full Eugster/EFLibrary encrypted protocol, HU handshake, HX FSM, HE brew ramp. Discovered and controlled by HA and the official Nivona Android app, so the whole pair → discover → brew flow works without physical hardware.
[0.47.0] — 2026-04-14 — Brand-neutral UI, docs, and legal notices¶
Comprehensive de-branding sweep — no more "Melitta Barista" strings shown to Nivona users, no legal disclaimers that forget Nivona / Eugster, and no stale module docstrings that claim Melitta-only scope when the code handles both brands.
Changed — user-facing strings¶
- Config-flow titles, descriptions, and placeholders are now
brand-neutral ("Coffee Machine Setup", "Select your coffee
machine…") across
strings.jsonand all 29 translation files (bg/bs/cs/da/de/el/en/es/et/fi/fr/hr/hu/it/lt/lv/mk/nb/nl/pl/pt/ro/ ru/sk/sl/sr/sv/tr/uk). Each translation uses its native term for "coffee machine" (Kaffeemaschine, Machine à café, Кофемашина, …) rather than the English literal. - Entity-name fallbacks in
config_flow.py,button.py,sensor.py,switch.py,text.py,select.py,number.py,binary_sensor.pynow derive the default from the activeBrandProfile.brand_name(e.g."Melitta Coffee Machine","Nivona Coffee Machine") instead of the hardcoded"Melitta Barista"literal. model_name(used byDeviceInfo.model) falls back tof"{brand_name} Coffee Machine"when no DIS / legacy model-table hit is available, rather than"Melitta Barista".- AI sommelier prompt ("You are an expert barista…") describes the target as "a bean-to-cup smart coffee machine" rather than "a Melitta Barista Smart".
conversation-facing error messages ("No coffee machine available") and WebSocket sommelier API errors no longer mention a specific brand.- Log lines —
"Connecting to Melitta at …"→"Connecting to {brand_name} machine at …".
Changed — docstrings and module headers¶
- Module-level docstrings in
__init__.py,ble_client.py,protocol.py,config_flow.py,diagnostics.py,entity.py,sensor.py,switch.py,number.py,binary_sensor.py,button.py,select.py,text.py,_ble_commands.py,_ble_recipes.py,_ble_settings.pynow describe their actual scope (coffee-machine entities / Eugster protocol / multi-brand) rather than claiming Melitta Barista only.
Changed — documentation, metadata, legal¶
NOTICEnow carries full trademark disclaimers for Melitta Group Management GmbH & Co. KG, Nivona Apparate GmbH, and Eugster/Frismag AG (OEM). Previously only Melitta was disclaimed.README.mdDisclaimer mirrors the NOTICE file and names all three trademark holders.README.mdRequirements section lists both Melitta Barista T/TS Smart (stable) and Nivona NICR/NIVO 8xxx (alpha) as supported machines.README.mdinstallation / UI paths reference"Melitta Barista Smart & Nivona"(matching the manifestname) instead of the legacy"Melitta Barista Smart".README.mdKnown Limitations single-BLE-connection note covers both the Melitta Connect and Nivona App.hacs.jsonnamesynced withmanifest.json(adds& Nivona).CHANGELOG.mdheader updated to multi-brand scope.docs/PROTOCOL.mdretitled to reflect the shared Eugster/EFLibrary OEM protocol rather than Melitta-only.docs/BLE_ARCHITECTURE.mdsubtitle updated for multi-brand scope.
Unchanged¶
- On-device entity identity, unique IDs, storage keys, and service payloads are untouched — this release is purely cosmetic / descriptive. Existing installations see new labels after restart; no reconfiguration required.
[0.46.0] — 2026-04-14 — Brand-aware HX status parsing¶
Fixed¶
- Nivona machines no longer report "unknown" state.
MachineStatus. from_payloadused a hardcoded MelittaMachineProcessenum (READY=2, PRODUCT=4), so raw process codes from Nivona firmware (NIVO 8000 uses 3/4, other Nivona families use 8/11) fell through toprocess=Noneand the whole integration looked idle / never-ready. Surfaced while the official Nivona Android app refused to start brewing against the emulator with "machine not ready" — the vendor app expected family-specific codes that the emulator wasn't reporting.
Changed¶
BrandProfileProtocol gained aparse_status(family_key, data)method.MelittaProfiledelegates to the canonicalMachineStatus.from_payload(Melitta-native codes);NivonaProfileoverrides with per-family tables —8000 → {3:READY, 4:PRODUCT}, other Nivona families →{8:READY, 11:PRODUCT}.EugsterProtocolnow tracks the detected family (set_family) and routes every HX parse throughbrand.parse_status(family, payload).MelittaBleClientpushes the family key immediately after_resolve_capabilities().
[0.45.0] — 2026-04-14 — Nivona emulator app compatibility¶
Completes the BLE emulator so the official Nivona Android app discovers, connects to, and operates it exactly like a real machine.
Fixed (emulator)¶
- Advertisement format now byte-exact to a real machine. Company ID
switched from the wrong 7425 to 0x0319 (Melitta), manufacturer
payload is
ff ff 00 00 00 00(customerId=65535 LE + vendor tail), and DIS (0x180A) is advertised in the scan response so the app can see the device class during scan. - BLE name no longer prefixed with
NIVONA-. The official app treatsPeripheral.Nameas the serial number — it strips trailing dashes and takesSubstring(0, 4)to derive the model code ("8107" → NICR 8107). ANIVONA-prefix made the substring resolve to"NIVO", no family matched, and the app silently skipped the emulator. - Primary-ADV 31-byte budget respected. Moved the 16-bit DIS UUID from primary to scan response; primary keeps flags + AD00 + mfr data = 31 bytes exact.
- NimBLE stack overflow on HE brew.
nivona_framelocal buffers (plain/cs_in/frame) promoted tostaticand NimBLE host task stack raised to 8 KB — previously the emulator silently reset on valid HE frames because 1.5 KB of stack buffers collided with the 4 KB default host task size. - Per-cmd size gating in the frame parser. A spurious
0x45byte in an encrypted HE payload was triggering a prematureFRAME_END. The parser now looks up the expected request size (HE=25, HU=11, HX=7, …) per cmd and only completes frames at the exact byte count.
[0.44.0] — 2026-04-14 — Nivona brew + BLE emulator¶
Adds brewing UI for Nivona (no HC/HJ needed — uses HE with per-family recipe layouts) and introduces a standalone ESP32 firmware that impersonates a Nivona machine for offline integration development.
Added¶
- Nivona brew button + recipe select —
select.<name>_recipeexposes the per-family_RECIPES_*drink list (Espresso, Coffee, Americano, Cappuccino, Caffè Latte, Latte Macchiato, Milk, Hot Water on 8xxx);button.<name>_brewsubmits the choice via HE with the family-correctbrew_command_mode(0x04 for NIVO 8000, 0x0B for NICR). - Nivona brew overrides as persistent
numberentities —<name>_brew_strength(1–5),<name>_brew_coffee_amount(20–240 mL),<name>_brew_temperature_preset(0/1/2),<name>_brew_milk_amount(0–240 mL). Values survive restarts viaRestoreEntityand are written via HW into per-family temporary-recipe registers (10000 + recipe_id * 100 + field_offset) right before HE — mirrors theSendTemporaryRecipe()flow in the Android app. BrandProfile.temp_recipe_register(family, recipe_id, field)helper andfluid_write_scale()accessor onNivonaProfile, reading from the existing_STANDARD_RECIPE_LAYOUTStables.EugsterProtocol.start_process_nivona(selector, mode)— Nivona- specific 18-byte HE payload (byte[1]=mode, byte[3]=selector, byte[5]=0x01) distinct from the Melittastart_process()layout.esp_emulator/— ESP32 firmware that acts as a Nivona BLE peripheral for development. Implements HU handshake, RC4 framing, all documented H* commands, per-family recipe layouts, and a brew FSM. Exposes HTTP OTA, telnet CLI, mDNS, and diagnostic counters. Tested against a Seeed XIAO ESP32-C6 + BlueZ and Seeed XIAO ESP32-S3 ESPHome BLE proxy + Home Assistant. Python test suite inesp_emulator/tests/.
Fixed¶
ble_client.brew_nivona()accepts an optionaloverridesdict to apply HW writes before HE — previously only the bare HE-with-defaults path existed.
[0.43.0] — 2026-04-14 — Nivona gaps 1-6 closed¶
Closes the six remaining Nivona-support gaps from the upstream RE port: entity wiring for settings/stats descriptors, DIS service reads at connect, family-override in Options Flow, experimental recipe-write path, and experimental MyCoffee-slot write path. The manufacturer_data advertisement matcher (gap 4) is documented as deferred pending real Nivona adv captures.
Added¶
- Generic
BrandSettingSelectdriven bySettingDescriptortuples from the active brand'sMachineCapabilities. Reads via HR, writes via HW. For Nivona, instantiated for every setting in the per-family table (up to 10 entries on 1040). Melitta continues to use its hand-tailored setting entities. - Generic
BrandStatSensordriven byStatDescriptortuples. Per-recipe cup counters, maintenance counters, and percentage/flag gauges for Nivona 700/79x/8000 families. Up to 27 new diagnostic sensors on NIVO 8xxx. - Device Information Service (0x180A) read at connect: Manufacturer / Model / Serial / HW / FW / SW revision strings. Used to refine capability detection via serial-prefix cascade AND to populate HA Device Registry with precise model information (no longer generic "Nivona Barista").
BleCoffeeClient.capabilitiesproperty exposes the resolvedMachineCapabilities(family-level + per-model overrides).BleCoffeeClient.dis_infoproperty exposes the DIS snapshot.- Options Flow family override (
family_override, Basic Settings): dropdown of the active brand's family keys. Empty = auto-detect. Unblocks future / misdetected models without waiting for a release. - Experimental write-path services for Nivona:
melitta_barista.nivona_write_recipe_param— write a single byte of a standard recipe slot via HW. 14 supported param keys: strength / profile / two_cups / temperature (+ per-fluid temps on 900 family) / overall_temperature / coffee_amount / water_amount / milk_amount / milk_foam_amount / preparation.melitta_barista.nivona_write_mycoffee_param— write a single byte of a MyCoffee user slot. Additional param keys: enabled, icon.- Both services marked EXPERIMENTAL in description — offsets are ported from upstream RE but have NOT been validated on real Nivona hardware; writes persist. Use at your own risk.
RecipeFieldLayoutdataclass inbrands/base.pywith all 14 per-family byte offsets.- Per-family standard-recipe and MyCoffee layouts in
brands/nivona.pycovering all 8 Nivona families. Fluid writes on 900-family families multiplied ×10 per upstream quirk. NivonaProfile.standard_recipe_layout,.mycoffee_layout,.standard_recipe_register,.mycoffee_registerhelper methods.write_standard_recipe_param/write_mycoffee_paramclient mixin methods (brand-gated, graceful False on missing layout).
Changed¶
BleCoffeeClient.model_namenow prefers resolvedcapabilities.model_name, falling back to DIS model string, then to legacyMACHINE_MODEL_NAMES.MelittaDeviceMixin.device_info.modelnow reflects the precise per-model name for Nivona entries (e.g. "NICR 756", "NICR 1040", "NIVO 8101" instead of generic "Nivona NICR 7xx").
Deferred (documented)¶
- Gap #4 — manufacturer_data advertisement matcher: upstream's
CheckDiscoveredinspects a non-standard adv structure0x0Dwith Eugster-proprietarycustomerId=65535. That structure has no clean mapping to HA'sBluetoothMatcherschema, and reconstructing the exact byte layout without real Nivona adv captures is unreliable.local_nameregex continues to cover all standard advertisements. A manufacturer_data-based secondary matcher can be added once a real capture is available.
Tests¶
- 692 → 703 (+11).
- New: recipe layout validation per-family (8 families × 14 offsets), MyCoffee layout validation, register calculation (10000+ and 20000+), write_standard_recipe_param / write_mycoffee_param happy path + slot-bounds / family-gating edge cases.
[0.42.0] — 2026-04-14 — Nivona data-completeness¶
Completes the port of Nivona-specific data. Crypto + recipe lists landed in 0.40.0/0.41.0; this release ports per-family settings register descriptors and stats register descriptors, plus per-model capability overrides that are needed for correct MyCoffee slot counts.
Added¶
- Per-family settings tables (
SettingDescriptortuples) with 4–10 entries per family covering water hardness, off-rinse, auto-off, temperature, profile, and per-fluid temperatures (1030/1040). All option enums (HARDNESS / AUTO_OFF / TEMPERATURE / PROFILE / MILK_TEMPERATURE / MILK_FOAM_TEMPERATURE / POWER_ON_FROTHER_TIME) are ported with value-code → label mapping. - Per-family stats tables for families with
supports_stats=True: 27 counters on 8000, 25 on 700, 10 on 79x. Includes per-recipe cup counters, maintenance counters (clean/descale/rinse/filter), and percentage/flag registers for descale/brew-unit-clean/frother-clean/ filter progress + warnings. NivonaProfile.capabilities_for_model(ble_name, dis)— per-model refinement. Returns aMachineCapabilitieswith correctmy_coffee_slotsandstrength_levelsper individual model code (e.g. NICR 788 = 5 slots vs 756 = 1 slot; NICR 1040 = 18 slots vs 920 = 9 slots).- Recipe/MyCoffee register base constants for future recipe-write
support:
RECIPE_BASE_REGISTER = 10000,MY_COFFEE_BASE_REGISTER = 20000, both withstride = 100. - Fixed
_PREFIX_TO_FAMILYmapping for NICR 1030/1040: serial prefix is actually"030"/"040", not"1030"/"1040".
Tests¶
- 688 → 692 (+6 Nivona coverage tests).
- New tests: per-family settings count, per-family stats count, per-
model capability overrides (10 model codes covering all 8 families),
unknown model returns
None.
Gaps deliberately not closed¶
The following items remain TODO for future Nivona work:
- HN Flying Picture — not implemented anywhere we can reference; only the HI feature bit is known.
- Standard-recipe layout offsets (per-family byte positions for strength/profile/temperature in the HE payload) — data ported as register-base constants, but the full recipe-layout write path is not wired through BleCoffeeClient yet. Requires live Nivona hardware to validate HW byte-by-byte writes.
- Advertisement manufacturer_data customerId — optional secondary discovery matcher; local_name regex already works for standard Nivona advertisements.
- DIS-service reads (0x180A) — would populate device registry with precise Manufacturer/Model/Serial/FW at connect time. Currently we rely on BLE advertisement local_name only.
- HE factory-reset opcodes (0x0032/0x0033) — destructive, user explicitly deferred.
- Chilled add-ons (NICR 8xxx) — requires field data we don't have.
These are documented in the project's internal roadmap.
[0.41.0] — 2026-04-13 — Nivona support (alpha)¶
First public release with Nivona NICR / NIVO 8xxx machines as a supported brand alongside Melitta. Ships the Nivona profile that has been in the codebase since 0.40.0 but inactive, plus polish for proper multi-brand device-registry rendering.
Added (Nivona-specific)¶
NivonaProfileis now active in the BrandRegistry and advertised viabluetooth: local_name: "NIVONA-*"inmanifest.json. Home Assistant will auto-discover Nivona machines and offer to set them up.- Seven family capability entries (
600,700,79x,900,900-light,1030,1040,8000) with per-family brew command mode, MyCoffee slot count, strength levels, and aroma-balance flag. - Nivona-specific HU verifier with the upstream 256-byte S-box and
+0x5D/+0xA7fold offsets — independently validated against the publishedseed FA 48 D1 7B → verifier 7E 6Evector. - Runtime RC4 stream key
NIV_060616_V10_1*9#3!4$6+4res-?3(recovered fromde.nivona.mobileapp3.8.6 in upstream RE).
Changed¶
MelittaDeviceMixinnow rendersmanufacturerfrom the active brand profile instead of hard-coded"Melitta"— Nivona entries show up correctly asNivonain the HA Device Registry.
Known limitations / not in this release¶
- Alpha status: this release has not yet been validated on real Nivona hardware by the maintainer. The crypto + handshake implementations match the upstream reference against published test vectors, but live BLE interop (pair, handshake, brew) is unverified. Please report via GitHub issue if you own a NICR / NIVO machine.
- No recipe editing: Nivona firmware does not expose
HC/HJrecipe read/write opcodes, so the Recipe Select, Freestyle builder, and Profile Activity switches do not appear on Nivona entries. Only maintenance actions, HY prompt confirmation, HD reset, and settings (HR/HW) are available. - Cup counters: Nivona 700+ families expose stats via different
register IDs than Melitta. Currently the
Total Cupssensor showsunknownon Nivona; family-specific stats entities are planned for a future release.
[0.40.0] — 2026-04-13 — Multi-brand refactor¶
Internal architecture refactor introducing pluggable BrandProfile abstraction, preparing the integration for adding Nivona (0.41.0) and potentially other OEM Eugster/EFLibrary-family brands later. No user-visible changes for existing Melitta Barista users.
Added¶
custom_components/melitta_barista/brands/package:base.py—BrandProfileProtocol +MachineCapabilities/RecipeDescriptor/SettingDescriptor/StatDescriptorPODsFeatureNotSupportedexception.
melitta.py—MelittaProfilehosting Melitta-specific crypto (RC4 key, HU CRC table, verifier algorithm), advertisement regex (8301/8311/8401/8501/8601/8604), 2 family capability entries (barista_t,barista_ts), supported extensions{"HC", "HJ"}.nivona.py—NivonaProfile(alpha — code-complete, untested on real hardware; see 0.41.0).__init__.py—BrandRegistrywithget_profile,all_profiles,detect_from_advertisement.docs/adr/001-brand-profile-abstraction.md— architectural decision record (4 alternatives considered).- 21 new brand-profile unit tests (including a Nivona HU verifier vector guaranteed to match upstream RE).
Changed¶
MelittaProtocol→EugsterProtocol(brand=...)(brand-agnostic Eugster/EFLibrary core).MelittaProtocolretained as backward-compat alias — all existing imports continue to work.MelittaBleClientacceptsbrand: BrandProfile | Nonekwarg; all crypto is delegated to the active profile.HC/HJopcodes (recipe read/write) now gated onbrand.supported_extensions— future Nivona clients will not try to issue commands the firmware doesn't understand.- Entity registration (
button.py,select.py,text.py,number.py,switch.py) filters Melitta-only entities (recipe select, freestyle builder, profile activity switches, cup counters via HC) when"HC"/"HJ"is not in the brand's supported set. bluetoothmatchers inmanifest.jsonnow includelocal_name: "NIVONA-*"in addition to the shared service UUID.
Migration¶
- Config entries automatically upgrade from v1 → v2 via
async_migrate_entry: all pre-existing entries receivedata["brand"] = "melitta". No action required from users. - Entity unique IDs are stable — all existing automations continue to work.
Tests¶
- 665 → 686 (+21 brand-profile tests).
[0.34.1] — 2026-04-13¶
Fixed¶
- Stale recipe cache after HD reset: after
reset_recipe_defaultreceived an ACK, the Recipe select entity's cachedrecipesattribute kept showing pre-reset values until a reconnect. Now the client re-reads the recipe via HC and notifies subscribers through a newadd_recipe_refresh_callbackhook;MelittaRecipeSelectsubscribes and refreshes its cached attributes immediately.
[0.34.0] — 2026-04-13¶
Added¶
- HY confirm-prompt protocol command (
CMD_CONFIRM_PROMPT) +protocol.confirm_prompt()+ client mixinconfirm_prompt(). Awaiting Confirmationbinary_sensor (PROBLEM device class) that turns on wheneverMachineStatus.manipulationreports any active prompt (codes 1–6, 11, 20).Confirm Promptbutton — manual acknowledgement, available only when a prompt is active.melitta_barista.confirm_promptservice for automation use.- Global
Auto-confirm soft promptsOptions Flow toggle — when enabled, the integration automatically sends HY for soft prompts (MOVE_CUP_TO_FROTHER,FLUSH_REQUIRED) so brew flow proceeds without user intervention. Hardware-blocking prompts (fill water, empty trays, etc.) intentionally still require manual confirmation. - Auto-confirm uses per-code debounce — each prompt is auto-confirmed only once per "appearance" to avoid loops if the machine reasserts.
- Two new
Manipulationenum members:MOVE_CUP_TO_FROTHER = 11,FLUSH_REQUIRED = 20. - New platform:
Platform.BINARY_SENSOR. - Translations (29 languages) for new entities, options, errors.
[0.33.0] — 2026-04-13¶
Added¶
- HD reset-to-default protocol command (
CMD_RESET_DEFAULT) +protocol.reset_default(value_id)+ client mixin methodreset_recipe_default(recipe_id). Reset Recipebutton — config-category entity that sends HD for the currently selected recipe. Available only when the machine is ready and a recipe is selected. NACK/timeout logged as warning, does not crash the entity.melitta_barista.reset_recipeservice with optionalrecipe_id(defaults to currently selected). RaisesServiceValidationErrorif no machine matched the entity or no recipe selected;HomeAssistantErroron NACK/timeout.- Translations (29 languages) for the new button and error messages.
Fixed¶
- Blocking file I/O in event loop:
ws_presets_listwas readingcoffee_presets.jsonsynchronously inside the event loop, triggering HA warnings. Now cached in-memory after a single executor-thread load.
[0.32.0] — 2026-04-13¶
Added¶
- HI feature capability read on connect — machine reports supported
capability bits (currently known: bit 0 =
IMAGE_TRANSFER). Graceful degradation via 3s timeout — some firmwares do not answer HI. Featuresdiagnostic sensor (disabled by default) exposing parsed flags + raw byte inextra_state_attributes.featuresfield in diagnostics output.FeatureFlagsIntFlag enum inconst.py.send_and_wait_response()now accepts optionaltimeoutoverride (backwards-compatible).
[0.29.0] — 2026-03-20¶
Added¶
- Recipe select entity:
recipesattribute with all preloaded recipe details - Profile select entity:
directkey_recipesattribute with per-profile DK data - Info-level logging for recipe preload progress
[0.28.0] — 2026-03-20¶
Added¶
- Dark theme brand icons (dark_icon.png, dark_logo.png + @2x variants)
- GitHub community files: CODE_OF_CONDUCT, CONTRIBUTING, SECURITY, issue/PR templates
- Milk category in brew_directkey service schema
Changed¶
- Git history cleaned: removed scripts/, audit/, docs/QUALITY_SCALE_PLAN.md
- Cleaned out external-reference language from code, docs, and git history
[0.27.0] — 2026-03-19¶
Added¶
- Repair Issues: BLE connection instability warning in Settings → Repairs (Gold:
repair-issues) - GitHub Actions CI: automated tests, coverage, HACS validation, hassfest, ruff, bandit
- README badges updated: 497 tests, 97% coverage
Stats¶
- 497 tests, 97% coverage, 12 modules at 99-100%
- Bronze 18/18 ✅, Silver 10/10 ✅, Gold ~18/22
[0.26.0] — 2026-03-19¶
Added¶
- HA Quality Scale compliance:
PARALLEL_UPDATES = 0in all 6 entity platform files (Silver:parallel-updates)- Service actions now raise
HomeAssistantError/ServiceValidationError(Silver:action-exceptions) - Exception translations in
strings.json(Gold:exception-translations) ConfigFlowResultreturn types (HA best practice)- Quality Scale Plan:
docs/QUALITY_SCALE_PLAN.md— detailed roadmap to Platinum
Changed¶
- Service handlers (
brew_freestyle,brew_directkey,save_directkey) raise exceptions on failure instead of silently returning
[0.25.0] — 2026-03-19¶
Added¶
- Diagnostics support (
diagnostics.py) — HA diagnostics panel with redacted BLE address - Reconfigure flow (
async_step_reconfigure) — change BLE address/name without re-adding - Type safety (
_ble_typing.py) — Protocol class for mypy mixin type checking
Changed¶
manifest.json: addedintegration_type: "device",loggers: ["melitta_barista"]config_flow.py: migratedFlowResult→ConfigFlowResult(HA best practice)- Mixin classes now use conditional
_MixinBasefor mypy compatibility
Improved¶
- Test coverage: 89% → 89% (371 tests, was 349)
button.py: 78% → 100% (22 new tests)config_flow.py: maintained 90% (reconfigure flow added)- HA Quality Scale: 10/14 → 13/14 (diagnostics, loggers, integration_type added)
[0.24.0] — 2026-03-19¶
Changed¶
- Refactor:
ble_client.pysplit from 1386 lines into 4 modules using mixins: ble_client.py— connection, reconnect, polling (684 lines)_ble_commands.py— brew, cancel, maintenance (262 lines)_ble_recipes.py— recipe/profile CRUD, cup counters (447 lines)_ble_settings.py— settings, alpha read/write (62 lines)- All external imports unchanged — fully backward-compatible
[0.23.4] — 2026-03-19¶
Fixed¶
_SHOTS_NAMESmapped to integers instead of strings — shots entity attributes rendered as0/1/2/3instead of"none"/"one"/"two"/"three"- Brew/recipe methods used hardcoded
DEFAULT_POLL_INTERVALinstead ofself._poll_interval, silently overriding user Options Flow configuration
[0.23.3] — 2026-03-19¶
Fixed¶
_load_post_connect_datatask now tracked and cancelled on disconnect (was fire-and-forget, could write to closed BLE)set_ble_device()no longer spawns duplicate_reconnect_loopwhen_async_connect_and_pollis still active (shared_reconnect_eventrace condition)MelittaProtocol()in_try_connect_and_handshakenow passesframe_timeoutfrom Options Flow (was using hardcoded default)write_alpha()now checkswas_pollingbefore restarting poll loop infinally(was unconditionally starting polling)send_and_wait_response()now cleans up stale future viafinallyblock (was leaking future whenwrite_funcraised)- Cup counter refresh now checks
_brew_lock.locked()before launching (could interleave with brew sequence)
[0.23.2] — 2026-03-19¶
Fixed¶
- Critical: reconnect loop silently cancelled itself —
_connect_implcalled_reconnect_task.cancel()on the currently running task, preventing any reconnection after BLE disconnect - Poll-loop forced disconnect now calls
_safe_disconnect()to properly close the BLE connection on ESPHome proxy before scheduling reconnect
Added¶
- New test verifying reconnect loop does not cancel itself
[0.23.0] — 2026-03-14¶
Added¶
- Options Flow UI: configurable integration parameters via Settings → Integrations → Melitta Barista → Configure
- Basic settings: poll interval, reconnect initial delay, reconnect max backoff, poll errors before disconnect, BLE frame timeout
- Advanced settings: BLE connection timeout, pairing timeout, recipe read/write retries, initial connect delay
- All 9 parameters have sensible defaults matching previous hardcoded values — no changes needed after upgrade
- 4 new tests for Options Flow (init menu, basic form, basic submit, advanced submit)
Changed¶
MelittaProtocolacceptsframe_timeoutparameter instead of using module-level constantMelittaBleClientaccepts all configurable parameters via constructor kwargs_async_connect_and_pollacceptspoll_interval,initial_delay,reconnect_delay,reconnect_max_delayparameters- Integration reloads automatically when options are changed
[0.22.2] — 2026-03-14¶
Changed¶
- Settings switches and number entities no longer poll via BLE every 30s; values are read once on connect (
should_poll=False) - Parameter mappings (
PROCESS_MAP,INTENSITY_MAP, etc.) consolidated intoconst.py, eliminating duplication acrossbutton.pyand__init__.py - Profile data and cup counters now load in background after connect, not blocking the connection phase
- All 11
device_infoproperties replaced with sharedMelittaDeviceMixin(newentity.py) - Hardcoded
interval=5.0replaced withDEFAULT_POLL_INTERVALconstant
[0.22.1] — 2026-03-14¶
Fixed¶
- Graceful shutdown: background connect task is now cancelled on integration unload, preventing "task still running after shutdown" warnings
- Callback cleanup: all entity callbacks are unsubscribed in
async_will_remove_from_hass, preventing duplicate state updates and stale references after integration reload - Poll loop disconnect detection: 3 consecutive poll errors now force disconnect and trigger reconnect, fixing silent "zombie" connections where BLE link is dead but no disconnect callback fires (e.g. ESP32 reboot without clean disconnect)
[0.22.0] — 2026-03-14¶
Added¶
- Instant reconnect on BLE advertisement: when machine powers on after being offline, reconnect triggers immediately instead of waiting up to 5 minutes for next backoff retry
- Reconnect event mechanism (
_reconnect_event) wakes up both initial connect and reconnect loops when BLE advertisement is received - Backoff delay resets to 5s when advertisement arrives (machine is likely available)
- Catch-all exception handler in reconnect loops prevents silent reconnect death
Fixed¶
- Machine not reconnecting after long power-off without HA restart
- Profile and Recipe select entities no longer store all DirectKey/recipe data in state attributes, preventing Recorder "exceeds maximum size of 16384 bytes" warnings
- Config flow test
test_step_pair_success_creates_entryno longer times out
Removed¶
directkey_recipesattribute from Profile select (was causing >16KB state attributes)recipesattribute from Recipe select (redundant bulk data; selected recipe details still available)
[0.21.1] — 2026-03-10¶
Fixed¶
- BLE connection: 3-phase pairing strategy to handle ESPHome proxy bond issues
- Phase 1:
pair=False(reuse existing bond — fast reconnect) - Phase 2:
pair=True(create new bond — first-ever connection) - Phase 3:
unpair+pair=True(clear stale bond on ESP32, then fresh pair) - Fixes
TimeoutAPIError,BluetoothConnectionDroppedError, and pairing error 82 on ESPHome BLE proxy after ESP32 reboot - Refactored connect logic into
_try_connect_and_handshake/_try_unpairfor clean retry
Added¶
- ESPHome config for Seeed XIAO ESP32-S3 BLE proxy (
esphome/ble-proxy-xiao-s3.yaml)
[0.21.0] — 2026-03-09¶
Added¶
- Aroma parameter (standard/intense) for freestyle entities, services, and recipe attributes
- Freestyle Aroma select entities (aroma_1, aroma_2)
- Aroma fields in brew_freestyle and save_directkey services
Fixed¶
- Profile Activity switches no longer poll periodically (was causing "update taking over 10 seconds" warnings); values are now read once on connect
[0.20.0] — 2026-03-09¶
Fixed¶
select.py: Temperature.COLD now correctly maps to "cold" instead of "normal" (was losing COLD value)ble_client.py:reset_profile_recipe,update_profile_recipe,copy_profile_recipenow use_brew_lockand stop/resume polling to prevent BLE contentionble_client.py:write_profile_recipeonly restarts polling if it was active before the operationble_client.py: Race condition between disconnect callback and manual disconnect prevented with_disconnectingguardble_client.py: Replaced deprecatedasyncio.ensure_futurewithasyncio.create_tasksensor.py:MelittaActivitySensornow hasavailableproperty based on connection state
Added¶
- Number entities: Language, Clock, Clock Send, Filter machine settings
- Button entities: Filter Insert, Filter Replace, Filter Remove, Evaporating maintenance operations
- Switch entities: Profile Activity (enable/disable user profiles 1-8)
[0.19.0] — 2026-03-09¶
Fixed¶
- HJ write_recipe: omit
recipe_keybyte for DirectKey slots (recipeKey=nullskips the byte, components start at offset 3) - Only TEMP_RECIPE writes (for brewing) include
recipe_key; DK slot writes (save, reset, copy, update) do not - This fixes "ACK timeout" errors when saving DirectKey recipes
[0.18.2] — 2026-03-09¶
Fixed¶
write_profile_recipenow retries write_recipe up to 3 times on ACK timeout- Added detailed debug logging for DirectKey recipe write (recipe_id, type, key)
[0.18.1] — 2026-03-09¶
Fixed¶
write_profile_recipeno longer fails whenread_recipereturns None — falls back to defaultrecipe_typeper DirectKey category- Added
DIRECTKEY_DEFAULT_RECIPE_TYPEmapping for all 7 categories
[0.18.0] — 2026-03-09¶
Added¶
- Two cups (2x) mode:
two_cupsflag in HE startProcess payload at offset 6 two_cupsparameter inbrew_recipe,brew_directkey,brew_freestylemethodstwo_cupsfield inbrew_directkeyandbrew_freestyleservice schemas
[0.17.1] — 2026-03-09¶
Fixed¶
- HC response parsing: remove incorrect recipe_key byte skip — HC payload is
id(2)+type(1)+comp1(8)+comp2(8), no recipe_key - HJ write payload: pass correct
recipe_keyper RecipeType→RecipeKey mapping - Fix
RECIPE_KEY_MAP: Espresso Macchiato → CAPPUCCINO(2), not MACCHIATO(3) - Add
RECIPE_TYPE_TO_KEYmapping andget_recipe_key()helper for all 25 recipe types - All
write_recipecall sites now pass correctrecipe_key(brew, DirectKey, freestyle, profile edit, copy, reset)
[0.17.0] — 2026-03-09¶
Added¶
- DirectKey brewing: read DK recipe → write to temp slot → start brew
- Profile data caching for faster recipe access
Fixed¶
- BLE protocol: rewrite frame parser to match original Melitta app algorithm
- A/N (ACK/NACK) frames are plaintext — no longer RC4-decrypted
- Frame timeout to prevent stale buffer corruption
- Drop corrupted BLE frames, retry read_recipe on checksum mismatch
- Stop polling during BLE writes to prevent command conflicts
- Eliminate BLE reads from text entity polling, retry ACK on timeout
[0.11.5] — 2026-03-08¶
Added¶
- 75 new tests for ble_client.py (26→101), covering connect/disconnect, reconnect, BLE write, notifications, brew, maintenance, cup counters, discovery
- Total: 249 tests, 89% coverage (was 174 tests, 82%)
ble_client.pycoverage: 62% → 100%
[0.11.4] — 2026-03-08¶
Fixed¶
- Fix 14 ruff errors in tests (unused imports, unused variables)
- Suppress Bandit B413 false positive for pycryptodome (
# nosec B413) - Update audit report with fresh results (ruff 0→0, 174 tests, 82% coverage)
[0.11.3] — 2026-03-08¶
Changed¶
- Narrow 30 broad
except Exceptionto specific types (BleakError,OSError,asyncio.TimeoutError) across ble_client, init, config_flow, select, protocol - Refactor
async_pair_device(154→6 helper functions, CC 17→5) in ble_agent.py - Extract
_async_discover_devices()fromasync_step_user(CC 18→8) in config_flow.py
Added¶
- 51 new tests: config_flow (100% coverage), ble_agent (93% coverage)
- Total: 174 tests, 82% coverage (was 123 tests, 71%)
[0.11.2] — 2026-03-08¶
Fixed¶
- Fix failing test
test_recipe_select_option— mock missingactive_profileandread_recipe - Remove unused imports (
asyncioin config_flow,TYPE_CHECKING/HomeAssistantin ble_client) - Fix undefined
RecipeComponenttype annotation — add proper import - Add
# noqa: F821to D-Bus type signature annotations in ble_agent.py - Sync
strings.jsonaccent characters withtranslations/en.json(Café, Crème, Caffè) - Add
_write_lockto BLE client for serialized GATT writes (prevents concurrent write races) - Ruff: 18 errors → 0 errors
[0.11.1] — 2026-03-08¶
Added¶
- ESPHome
.gitignoreandsecrets.yaml.examplefor easy proxy setup - Test scripts for BLE connection verification (local adapter and ESPHome proxy)
[0.11.0] — 2026-03-08¶
Added¶
- Automatic BLE pairing via Bleak's
pair=True— works with both local BlueZ adapter and ESPHome BLE proxy - Config flow gracefully skips D-Bus pairing when unavailable (pairing handled on connect)
Changed¶
- ESPHome proxy config: removed aggressive scan parameters (1100ms/1100ms) — use defaults for stable single-core ESP32-C6 operation
establish_connection()and fallbackBleakClientnow passpair=Truefor cross-platform bonding
[0.10.2] — 2026-03-08¶
Fixed¶
- Check
Adapter1interface existence (not just D-Bus path) when detecting local BlueZ adapter - Added CHANGELOG.md
[0.10.1] — 2026-03-08¶
Fixed¶
- Skip D-Bus pairing when no local BlueZ adapter — enables ESPHome BLE proxy support
ble_agent.pynow checks forhci0existence before attempting D-Bus pairing; returns "ok" if missing (proxy handles bonding at ESP32 level)
[0.10.0] — 2026-03-08¶
Added¶
- Preload all recipe details on BLE connect — cached in
recipesattribute - Web app shows recipe details instantly without per-click BLE reads
[0.9.3] — 2026-03-08¶
Fixed¶
- Harden BLE code against race conditions and short payloads
- Capture
self._clientto local var inconnected,_write_ble,disconnect()to prevent race with_on_disconnectcallback - Add length guards to
NumericalValue,AlphanumericValue,MachineRecipe,RecipeComponentfrom_payload/from_bytes— returnNoneon short data - Handle fire-and-forget cup counter refresh task errors via
done_callback
[0.9.2] — 2026-03-08¶
Fixed¶
- Wrap handshake-failure disconnect in
try/except— preventsEOFErrornoise when D-Bus connection is already dead
[0.9.1] — 2026-03-08¶
Fixed¶
- Handle short HR payloads in cup counters (IDs 111, 122 return < 6 bytes)
- Fix race condition in
_connect_implerror handler where_on_disconnectcould nullself._clientbetween check anddisconnect()call
[0.9.0] — 2026-03-08¶
Added¶
- Cup counter sensor (
total_cups) with per-recipe statistics as attributes - Counters auto-refresh after each brew completes (PRODUCT → READY transition)
- Counter IDs discovered via BLE scan: HR 100–123 (per recipe) + HR 150 (total)
[0.8.1] — 2026-03-07¶
Fixed¶
- Map temperature
0to "normal" (standard brew temperature) instead of "low"
[0.8.0] — 2026-03-07¶
Added¶
- Expose recipe details (intensity, temperature, shots, portion) as
extra_state_attributeson recipe select entity - Reads recipe via HC command on selection, respects active profile (DirectKey)
Changed¶
- Documentation: added freestyle entities reference and PWA app section to README
[0.7.1] — 2026-03-06¶
Added¶
- Freestyle recipe entities for standard HA UI:
- Select entities for process, intensity, temperature, shots (both components)
- Number entities for portion sizes (ml)
- Text entity for recipe name
- "Brew Freestyle" button that reads entity values and brews
Fixed¶
- Rename "steam" to "milk" in freestyle UI for clarity (protocol value unchanged)
- Legacy cleanup now preserves
brew_freestylebutton
[0.7.0] — 2026-03-06¶
Added¶
- Profile select entity with DirectKey-based per-profile brewing
brew_freestyleservice for custom drink recipes via TEMP_RECIPEDirectKeyCategoryenum andget_directkey_id()calculationservices.yamlfor HA service UI- 15 new tests for profiles, DirectKey, freestyle
[0.6.6] — 2026-03-05¶
Fixed¶
- Instant entity availability update on BLE reconnect — all entities (buttons, select, sensors, switches, numbers, text) now register connection callbacks
[0.6.5] — 2026-03-05¶
Fixed¶
- Expand legacy cleanup to handle named recipe button entities (
{address}_brew_espresso, etc.) in addition to numeric IDs
[0.6.4] — 2026-03-05¶
Added¶
- Comprehensive test suite: 107 tests covering protocol, BLE client, entities, and integration lifecycle
Fixed¶
- Code quality improvements from audit:
- Fix firmware sensor race with background connect
- Fix maintenance buttons accessing private client members
- Fix incorrect
Callabletype annotations inprotocol.py - Fix state sensor returning "unavailable" string instead of
None - Move connection callback registration to
async_added_to_hass - Remove dead code
- Additional audit fixes:
- Add
pycryptodomeandbleak-retry-connectorto manifest requirements - Fix duplicate device entries in
discover_melitta_devices - Type
async_step_bluetoothwithBluetoothServiceInfoBleak
[0.6.2] — 2026-03-04¶
Added¶
- Initial release of Melitta Barista Smart integration for Home Assistant
- BLE communication via
bleakwith D-Bus Agent1 pairing - Recipe select entity + brew button pattern (replaces 24 individual buttons)
- 29 language translations
- HACS-compatible structure with GitHub Actions validation