diff --git a/custom_components/localthings/registry/by_type/airconditioner.py b/custom_components/localthings/registry/by_type/airconditioner.py index 9de96e0..e218de5 100644 --- a/custom_components/localthings/registry/by_type/airconditioner.py +++ b/custom_components/localthings/registry/by_type/airconditioner.py @@ -4,10 +4,8 @@ The first device whose core controls surface as a single composite HA `climate` entity (see capabilities/airconditioner.py and climate.py). Power/mode/temp/wind are consumed by that entity rather than exposed as separate switches/selects, so this registry includes *common.UNIVERSAL but deliberately NOT common.POWER -- -on/off is the climate entity's HVACMode.OFF / TURN_ON/OFF, and /power/0 + -/power/vs/0 are already pre-claimed (no-entity) by airconditioner.COVERAGE via -CLIMATE_CONSUMED_HREFS. A real POWER_GENERIC/POWER_VS_FALLBACK cap on either -href alongside that bare claim would make _build() raise (see its docstring). +on/off is the climate entity's HVACMode.OFF / TURN_ON/OFF. See common.POWER's +own comment in capabilities/common.py for why it's excluded. Reuses dishwasher.DIAGNOSIS for /diagnosis/vs/0. """ diff --git a/custom_components/localthings/registry/capabilities/common.py b/custom_components/localthings/registry/capabilities/common.py index d2b2ac5..7a490a1 100644 --- a/custom_components/localthings/registry/capabilities/common.py +++ b/custom_components/localthings/registry/capabilities/common.py @@ -348,15 +348,16 @@ SELF_CHECK = Capability( # these to a new family is a safe, harmless guess (issue #40's AI energy # level: 2 of 6 families confirmed, blanket-added everywhere else). # -# POWER is kept separate: airconditioner deliberately does NOT include it. -# AC's climate entity (registry/capabilities/airconditioner.py) already -# owns /power/0 and /power/vs/0 -- they're pre-claimed there via bare, -# no-entity Capability objects (COVERAGE) so a second, real POWER_GENERIC/ -# POWER_VS_FALLBACK cap on the same href would make _build() raise -# (a href with >1 cap must have every cap discriminated by rt_filter/ -# match_fn, and the bare COVERAGE cap has neither). Kids-lock and remote- -# control don't have this conflict -- no AC dump has ever reported those -# hrefs -- so they stay in UNIVERSAL. +# POWER is kept separate -- airconditioner is the one family that opts out +# of it. Canonical reason (see by_type/airconditioner.py and its test for +# pointers back here, not restatements): AC's climate entity already owns +# /power/0 and /power/vs/0 via bare, no-entity Capability objects +# (airconditioner.COVERAGE), and a second, real POWER_GENERIC/ +# POWER_VS_FALLBACK cap on the same href would make _build() raise (a href +# with >1 cap must have every cap discriminated by rt_filter/match_fn, and +# the bare COVERAGE cap has neither). Kids-lock/remote-control don't have +# this conflict -- no AC dump has ever reported those hrefs -- so they stay +# in UNIVERSAL. # --------------------------------------------------------------------------- UNIVERSAL = ( diff --git a/tests/test_common_capabilities.py b/tests/test_common_capabilities.py index 4a38654..e852809 100644 --- a/tests/test_common_capabilities.py +++ b/tests/test_common_capabilities.py @@ -142,10 +142,14 @@ class TestEnergyMeter: # --------------------------------------------------------------------------- +def _ai_energy_level_desc(cls_name): + return next(e for e in common.AI_ENERGY_LEVEL.entities + if e.__class__.__name__ == cls_name) + + class TestAiEnergyLevelSwitch: def _desc(self): - return next(e for e in common.AI_ENERGY_LEVEL.entities - if e.__class__.__name__ == 'SwitchDesc') + return _ai_energy_level_desc('SwitchDesc') def test_href(self): assert common.AI_ENERGY_LEVEL.href == '/energy/ailevel/vs/0' @@ -190,8 +194,7 @@ class TestAiEnergyLevelSwitch: class TestAiEnergyLevelSelect: def _desc(self): - return next(e for e in common.AI_ENERGY_LEVEL.entities - if e.__class__.__name__ == 'SelectDesc') + return _ai_energy_level_desc('SelectDesc') def test_shown_only_with_multiple_supported_levels(self): desc = self._desc() @@ -282,10 +285,7 @@ class TestUniversalAndPowerBundles: } def test_power_kept_separate_for_airconditioners_sake(self): - """airconditioner.py deliberately doesn't unpack this bundle -- its - climate entity already owns /power/0 and /power/vs/0 via a bare, - no-entity claim (airconditioner.COVERAGE); a real POWER_GENERIC/ - POWER_VS_FALLBACK cap on the same href would make _build() raise.""" + """See common.POWER's own comment for why airconditioner opts out.""" assert set(common.POWER) == {common.POWER_GENERIC, common.POWER_VS_FALLBACK} def test_no_overlap_between_the_two_bundles(self):