From 57e06c5ccbd1fea67bfc0d649a0e1b8fac3e65bb Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Tue, 28 Jul 2026 13:31:37 +0000 Subject: [PATCH] De-duplicate the legacy ARTIK051 AC board-generation test (#161) capabilities/airconditioner.py's is_legacy_board() (renamed from the private _is_legacy_board -- it's now a cross-module helper) and climate.py's _legacy_airflow() implemented the same "does this board have /airflow/vs/0 but no /wind/strength/vs/0" test independently, one via literal href strings and the other via coordinator.resource() truthiness. is_legacy_board() now uses the module's own HREF_AIRFLOW/HREF_WIND_STRENGTH constants, and _legacy_airflow() delegates to it via a minimal two-key presence dict (cheaper than a full last_resources snapshot copy) instead of re-implementing the check, so the token entities and the climate card's legacy read/write paths can't drift apart on which board generation is in play. --- custom_components/localthings/climate.py | 22 +++++++++++++++---- .../registry/capabilities/airconditioner.py | 7 +++--- tests/test_airconditioner_artik051_krac.py | 19 +++++++++++++++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/custom_components/localthings/climate.py b/custom_components/localthings/climate.py index 5ba2137..3de683b 100644 --- a/custom_components/localthings/climate.py +++ b/custom_components/localthings/climate.py @@ -51,6 +51,7 @@ from .registry.capabilities.airconditioner import ( HREF_WIND_OSCILLATION as WIND_OSCILLATION_HREF, HREF_CONVENIENT as CONVENIENT_HREF, HREF_AIRFLOW as AIRFLOW_HREF, + is_legacy_board, ) from .registry.capabilities.common import normalize_temp_unit @@ -247,10 +248,23 @@ class LocalThingsClimate(LocalThingsEntity, ClimateEntity): def _legacy_airflow(self) -> dict: """The /airflow/vs/0 rep, but only when it is the fan/swing channel to - use -- i.e. this board has no /wind/strength/vs/0.""" - if self.coordinator.resource(WIND_STRENGTH_HREF): - return {} - return self.coordinator.resource(AIRFLOW_HREF) or {} + use -- i.e. this board has no /wind/strength/vs/0. + + Delegates the board-generation test to is_legacy_board (the same + test capabilities/airconditioner.py's token entities are gated on) + instead of re-implementing it, via a minimal presence dict built + from the two hrefs it actually inspects -- cheaper than + last_resources' full snapshot copy, since is_legacy_board only + checks key membership. + """ + airflow = self.coordinator.resource(AIRFLOW_HREF) + wind_strength = self.coordinator.resource(WIND_STRENGTH_HREF) + presence = {} + if airflow: + presence[AIRFLOW_HREF] = airflow + if wind_strength: + presence[WIND_STRENGTH_HREF] = wind_strength + return airflow if is_legacy_board(presence) else {} def _legacy_preset(self) -> bool: """Whether presets come from the Comode_* token rather than a resource. diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index 47c402c..f060d75 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -152,7 +152,7 @@ def _option_token(rep, prefix): return None -def _is_legacy_board(resources): +def is_legacy_board(resources): """True for the board generation whose airflow lives in /airflow/vs/0. Newer families carry several of the same option tokens (Volume, Sleep, @@ -163,13 +163,12 @@ def _is_legacy_board(resources): /airflow/vs/0. Same test as climate.py's _legacy_airflow(), so the entities below and the climate entity can never disagree about the generation. """ - return ('/airflow/vs/0' in resources - and '/wind/strength/vs/0' not in resources) + return HREF_AIRFLOW in resources and HREF_WIND_STRENGTH not in resources def _has_option_token(prefix): return lambda rep, resources: ( - _is_legacy_board(resources) and _option_token(rep, prefix) is not None) + is_legacy_board(resources) and _option_token(rep, prefix) is not None) def _option_token_on(prefix): diff --git a/tests/test_airconditioner_artik051_krac.py b/tests/test_airconditioner_artik051_krac.py index 2e71e89..d6b4b98 100644 --- a/tests/test_airconditioner_artik051_krac.py +++ b/tests/test_airconditioner_artik051_krac.py @@ -24,7 +24,8 @@ from custom_components.localthings.registry.by_type import ( airconditioner, for_device_by_model, ) from custom_components.localthings.registry.capabilities.airconditioner import ( - HREF_AIRFLOW, _option_number_write, _option_switch_write, + HREF_AIRFLOW, HREF_WIND_STRENGTH, _option_number_write, + _option_switch_write, is_legacy_board, ) from custom_components.localthings.registry.discovery import discover from custom_components.localthings.registry.entities import ClimateDesc @@ -119,6 +120,22 @@ def test_token_entities_stay_off_newer_boards(): assert key not in state, key +def test_climate_legacy_airflow_gate_agrees_with_is_legacy_board(): + """issue #161: climate.py's _legacy_airflow() delegates to + capabilities/airconditioner.py's is_legacy_board() instead of + re-implementing the same presence/absence check, so the token entities + and the climate card's legacy read/write paths can't drift apart on + which board generation is in play.""" + legacy_resources = _load_device(FIXTURE) + assert is_legacy_board(legacy_resources) is True + assert _climate(legacy_resources)._legacy_airflow() == legacy_resources[HREF_AIRFLOW] + + newer_resources = _load_device('airconditioner_tp1x_rac') + assert is_legacy_board(newer_resources) is False + assert _climate(newer_resources)._legacy_airflow() == {} + assert HREF_WIND_STRENGTH in newer_resources + + def test_absent_token_yields_no_entity(): """The issue #136 unit of this same model reports no Spi token.""" resources = _load_device(FIXTURE)