v0.79.0 — capability-driven platform refactor
Pure-refactor release. No user-facing behavior change.
What changed
The integration now operates as a brand-agnostic platform: HA entity factories and BLE mixins no longer test client.brand.brand_slug == "X" or import directly from brands.<vendor>. All brand-specific behavior flows through the BrandProfile Protocol contract and per-family MachineCapabilities flags.
Adding a new coffee-machine brand (Krups SubLime, Severin, hypothetical 3rd Eugster-OEM) is now a self-contained operation:
- Implement the
BrandProfileProtocol - Create per-family modules with
EXPORTSdicts - Register in
brands/__init__.py
No edits to shared layers (sensor.py, button.py, select.py, number.py, _ble_commands.py, _ble_recipes.py) required.
Refactored
- BrandProfile contract extended with 6 new methods:
temp_recipe_type_register(class attr),temp_recipe_register,fluid_write_scale,mycoffee_layout,mycoffee_register,is_chilled_selector. NivonaProfile already implemented these as@staticmethod; MelittaProfile gets stubs returningNone/1/False. Shared mixins (_ble_commands,_ble_recipes) andsensor.pyno longerfrom .brands.nivona import …. - 3 new MachineCapabilities feature flags:
supports_factory_reset,supports_brew_overrides,uses_legacy_total_cups_sensor. Set per-family in_family_*.pyand in MelittaProfile. Entity factories use these flags instead ofbrand_slug == "X". Hardcoded_FACTORY_RESET_FAMILIES = {"600", "700", …}frozenset deleted. - Redundant gates dropped:
button.py:55/select.py:146HE-selector brew entities now key off"HC" not in supported_extensionsalone (brand_slug half was redundant);number.py:101/select.py:130-132settings-entity gates rely oncaps.settingsbeing empty for Melitta.
Added
docs/BRAND_PROFILE_SPEC.md— contract specification documenting:- Required
BrandProfileProtocol surface (identity, crypto, family resolution, parse_status, recipe write-path methods) - Required
MachineCapabilitiesfields + feature flag conventions - Step-by-step guide for adding a new brand
- Explicit anti-patterns banned in shared layers (
brand_slug ==checks, directbrands.<vendor>imports, hardcoded family sets, isinstance checks, mutating frozen capabilities) - Forward-looking notes on what's NOT in the contract yet (plugin loader, Protocol versioning, capability-coverage tests)
- Required
Test infrastructure
Test fixtures across tests/test_button.py, tests/test_init.py, tests/test_sensor.py switched from client.brand = MagicMock() to real MelittaProfile() / NivonaProfile() instances + capabilities_for(family_key). Real BrandProfile gives capability-driven gates actual values; previous MagicMock-truthy false positives caused setup-time TypeErrors. Tests mutating caps.my_coffee_slots directly now use dataclasses.replace().
Full suite: 992 passed (unchanged from v0.78.1).