diff --git a/custom_components/localthings/registry/capabilities/airconditioner.py b/custom_components/localthings/registry/capabilities/airconditioner.py index a44fc2d..b502a63 100644 --- a/custom_components/localthings/registry/capabilities/airconditioner.py +++ b/custom_components/localthings/registry/capabilities/airconditioner.py @@ -372,6 +372,18 @@ def _has_option_token(prefix): ) +def _has_option_token_any_board(prefix): + """Token-presence test with no board-generation gate. + + `_has_option_token` above requires `is_legacy_board`, which was right for + the settings it guards but wrong for a token whose presence is itself the + only signal that needs checking -- issue #367 found `OutdoorTemp_` live + on 14 of 23 recorded fixtures despite none of them being legacy boards. + Same shape as `beep`'s Volume_ test and `_has_display_light_option`, + which already treat token presence as sufficient across generations.""" + return lambda rep, resources: _option_token(rep, prefix) is not None + + def _option_token_on(prefix): return lambda rep: _option_token(rep, prefix) == "On" @@ -782,13 +794,30 @@ CLIMATE = Capability( ), # Outdoor temperature, offset by 55 -- calibrated against an # independent thermometer (token 75 while it read 20.3°C). + # + # exists_fn is token-presence-only (issue #367): the token was + # previously gated behind is_legacy_board for no reason tied to the + # token itself, which silently dropped the sensor on every + # non-legacy board that reports it -- 14 of 17 fixtures carrying the + # token in the issue's own survey. A 48h/289-sample field capture + # correlated it against weather.forecast_home at r=0.92, ruling out + # a firmware constant. + # + # enabled_default=False: multi-split installs (multiple indoor heads + # on one outdoor condenser) report the same token on every head, so + # a fix here creates one identical sensor per head rather than one + # per physical unit (same duplication as the shared energy/power + # counters in issue #329). Left disabled so a user with several + # heads can enable just one instead of getting N duplicates active + # by default. SensorDesc( key="outdoor_temperature", rep_fn=_option_token_num("OutdoorTemp", offset=55), - exists_fn=_has_option_token("OutdoorTemp"), + exists_fn=_has_option_token_any_board("OutdoorTemp"), device_class="temperature", state_class="measurement", unit="°C", + enabled_default=False, icon="mdi:home-thermometer-outline", ), # Filter time in tenths of an hour, counting UP since last filter diff --git a/tests/fixtures/golden/airconditioner.json b/tests/fixtures/golden/airconditioner.json index fcafcf5..086a9b2 100644 --- a/tests/fixtures/golden/airconditioner.json +++ b/tests/fixtures/golden/airconditioner.json @@ -20,6 +20,7 @@ "fine_dust", "humidity", "odor", + "outdoor_temperature", "power_watts", "super_fine_dust", "tropical_night_mode" diff --git a/tests/fixtures/golden/airconditioner_ailp_fac.json b/tests/fixtures/golden/airconditioner_ailp_fac.json index 67d3199..893ea3c 100644 --- a/tests/fixtures/golden/airconditioner_ailp_fac.json +++ b/tests/fixtures/golden/airconditioner_ailp_fac.json @@ -25,6 +25,7 @@ "mute_once", "odor_controller_active", "odor_controller_progress", + "outdoor_temperature", "power_energy_kwh", "power_watts", "selfcheck_error", diff --git a/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json b/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json index 10810e8..a6979b6 100644 --- a/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json +++ b/tests/fixtures/golden/airconditioner_ara_ww_tp1_22.json @@ -18,6 +18,7 @@ "mute_once", "odor_controller_active", "odor_controller_progress", + "outdoor_temperature", "power_watts", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_cac.json b/tests/fixtures/golden/airconditioner_cac.json index a8819ff..c479a41 100644 --- a/tests/fixtures/golden/airconditioner_cac.json +++ b/tests/fixtures/golden/airconditioner_cac.json @@ -36,6 +36,7 @@ "mute_once", "odor_controller_active", "odor_controller_progress", + "outdoor_temperature", "periodic_air_sensing", "periodic_sensing_skip_status", "power_watts", diff --git a/tests/fixtures/golden/airconditioner_caww_tp2.json b/tests/fixtures/golden/airconditioner_caww_tp2.json index a8d9f69..971da54 100644 --- a/tests/fixtures/golden/airconditioner_caww_tp2.json +++ b/tests/fixtures/golden/airconditioner_caww_tp2.json @@ -17,6 +17,7 @@ "firmware_update", "humidity", "mute_once", + "outdoor_temperature", "power_watts", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json b/tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json index 9cbc8d2..1264892 100644 --- a/tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json +++ b/tests/fixtures/golden/airconditioner_lnx_rac_heatpump.json @@ -22,6 +22,7 @@ "mute_once", "odor_controller_active", "odor_controller_progress", + "outdoor_temperature", "power_watts", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json b/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json index b0d26cd..1fbc07b 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json +++ b/tests/fixtures/golden/airconditioner_tp1x_da_ac_rac_01011.json @@ -23,6 +23,7 @@ "last_air_sensing_level", "last_air_sensing_time", "mute_once", + "outdoor_temperature", "periodic_air_sensing", "periodic_sensing_skip_status", "selfcheck_error", diff --git a/tests/fixtures/golden/airconditioner_tp1x_rac.json b/tests/fixtures/golden/airconditioner_tp1x_rac.json index 445a6f9..84b5dd9 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_rac.json +++ b/tests/fixtures/golden/airconditioner_tp1x_rac.json @@ -19,6 +19,7 @@ "energy_saved_kwh", "firmware_update", "mute_once", + "outdoor_temperature", "selfcheck_error", "selfcheck_result", "selfcheck_status", diff --git a/tests/fixtures/golden/airconditioner_tp1x_rac_01001.json b/tests/fixtures/golden/airconditioner_tp1x_rac_01001.json index 10810e8..a6979b6 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_rac_01001.json +++ b/tests/fixtures/golden/airconditioner_tp1x_rac_01001.json @@ -18,6 +18,7 @@ "mute_once", "odor_controller_active", "odor_controller_progress", + "outdoor_temperature", "power_watts", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json b/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json index 9fd5d96..de360bf 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json +++ b/tests/fixtures/golden/airconditioner_tp1x_rac_coolonly.json @@ -16,6 +16,7 @@ "firmware_update", "humidity", "mute_once", + "outdoor_temperature", "power_watts", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_tp1x_rac_odor_controller.json b/tests/fixtures/golden/airconditioner_tp1x_rac_odor_controller.json index 99e808b..a4f5938 100644 --- a/tests/fixtures/golden/airconditioner_tp1x_rac_odor_controller.json +++ b/tests/fixtures/golden/airconditioner_tp1x_rac_odor_controller.json @@ -20,6 +20,7 @@ "mute_once", "odor_controller_active", "odor_controller_progress", + "outdoor_temperature", "selfcheck_error", "selfcheck_result", "selfcheck_status", diff --git a/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json b/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json index c419541..b1adb33 100644 --- a/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json +++ b/tests/fixtures/golden/airconditioner_tp2x_rac_20k.json @@ -15,6 +15,7 @@ "firmware_update", "humidity", "mute_once", + "outdoor_temperature", "power_watts", "tropical_night_mode" ] diff --git a/tests/fixtures/golden/airconditioner_windfree.json b/tests/fixtures/golden/airconditioner_windfree.json index 95f4c8d..53f1a45 100644 --- a/tests/fixtures/golden/airconditioner_windfree.json +++ b/tests/fixtures/golden/airconditioner_windfree.json @@ -19,6 +19,7 @@ "fine_dust", "humidity", "odor", + "outdoor_temperature", "power_watts", "super_fine_dust", "tropical_night_mode" diff --git a/tests/fixtures/golden/airconditioner_window_ac.json b/tests/fixtures/golden/airconditioner_window_ac.json index 58ac32d..40d121f 100644 --- a/tests/fixtures/golden/airconditioner_window_ac.json +++ b/tests/fixtures/golden/airconditioner_window_ac.json @@ -16,6 +16,7 @@ "firmware_update", "humidity", "mute_once", + "outdoor_temperature", "power_watts", "selfcheck_error", "selfcheck_result", diff --git a/tests/test_airconditioner_artik051_krac.py b/tests/test_airconditioner_artik051_krac.py index 7ab0154..4f887d2 100644 --- a/tests/test_airconditioner_artik051_krac.py +++ b/tests/test_airconditioner_artik051_krac.py @@ -129,22 +129,35 @@ def test_token_entities_present_with_calibrated_values(): def test_token_entities_stay_off_newer_boards(): - """Newer families carry Volume/Sleep/OutdoorTemp/Autoclean tokens too, - while also exposing those settings as dedicated resources -- ungated, the - token entities would duplicate them (auto clean) or apply a scale - calibrated on another board generation (outdoor temperature).""" + """Newer families carry Volume/Sleep/Autoclean tokens too, while also + exposing those settings as dedicated resources -- ungated, the token + entities would duplicate them.""" state = _state("airconditioner_tp1x_rac") for key in ( "spi", "auto_clean_legacy", "air_monitoring", "good_sleep", - "outdoor_temperature", "filter_time", ): assert key not in state, key +def test_outdoor_temperature_is_not_gated_to_legacy_boards(): + """issue #367: OutdoorTemp_ is not paired with any dedicated resource on + newer boards -- /temperatures/vs/0 carries indoor temperature only, no + outdoor equivalent exists anywhere in this fixture's 23 hrefs -- so unlike + auto_clean_legacy et al. above, gating it to is_legacy_board only dropped + a real reading. A 48h field capture correlated the token against + weather.forecast_home at r=0.92 across several non-legacy boards, + confirming it is live per-site data rather than a firmware constant.""" + resources = _load_device("airconditioner_tp1x_rac") + assert is_legacy_board(resources) is False + bound, _ = _discover(resources) + state = flatten(bound, resources) + assert state["outdoor_temperature"] == 37.0 # OutdoorTemp_92, the fixture's own value + + 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