airconditioner: ungate outdoor_temperature from is_legacy_board

The OutdoorTemp_ options token was only surfaced on legacy boards
(is_legacy_board), even though 14 of 17 fixtures carrying the token are
non-legacy. issue #367 confirmed with a 48h field capture (r=0.92
against weather.forecast_home) that the token tracks real outdoor
temperature independent of board generation, and that no non-legacy
board exposes an alternative outdoor-temperature resource.

Split a token-presence-only exists_fn (_has_option_token_any_board) for
this token, leaving _has_option_token's legacy gate untouched for the
other options[] settings that still need it. Ships enabled_default=False
since multi-split installs report the same token on every indoor head,
which would otherwise create one duplicate active sensor per head.

Updates the golden fixtures for the 14 affected boards and the
artik051_krac test that had asserted outdoor_temperature stays off
newer boards.
This commit is contained in:
Marc Billow
2026-08-15 21:22:09 +00:00
parent 12922909c0
commit 8814fffa4f
16 changed files with 62 additions and 6 deletions
@@ -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
+1
View File
@@ -20,6 +20,7 @@
"fine_dust",
"humidity",
"odor",
"outdoor_temperature",
"power_watts",
"super_fine_dust",
"tropical_night_mode"
+1
View File
@@ -25,6 +25,7 @@
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"outdoor_temperature",
"power_energy_kwh",
"power_watts",
"selfcheck_error",
@@ -18,6 +18,7 @@
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"outdoor_temperature",
"power_watts",
"tropical_night_mode"
]
+1
View File
@@ -36,6 +36,7 @@
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"outdoor_temperature",
"periodic_air_sensing",
"periodic_sensing_skip_status",
"power_watts",
+1
View File
@@ -17,6 +17,7 @@
"firmware_update",
"humidity",
"mute_once",
"outdoor_temperature",
"power_watts",
"tropical_night_mode"
]
@@ -22,6 +22,7 @@
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"outdoor_temperature",
"power_watts",
"tropical_night_mode"
]
@@ -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",
+1
View File
@@ -19,6 +19,7 @@
"energy_saved_kwh",
"firmware_update",
"mute_once",
"outdoor_temperature",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
@@ -18,6 +18,7 @@
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"outdoor_temperature",
"power_watts",
"tropical_night_mode"
]
@@ -16,6 +16,7 @@
"firmware_update",
"humidity",
"mute_once",
"outdoor_temperature",
"power_watts",
"tropical_night_mode"
]
@@ -20,6 +20,7 @@
"mute_once",
"odor_controller_active",
"odor_controller_progress",
"outdoor_temperature",
"selfcheck_error",
"selfcheck_result",
"selfcheck_status",
@@ -15,6 +15,7 @@
"firmware_update",
"humidity",
"mute_once",
"outdoor_temperature",
"power_watts",
"tropical_night_mode"
]
+1
View File
@@ -19,6 +19,7 @@
"fine_dust",
"humidity",
"odor",
"outdoor_temperature",
"power_watts",
"super_fine_dust",
"tropical_night_mode"
+1
View File
@@ -16,6 +16,7 @@
"firmware_update",
"humidity",
"mute_once",
"outdoor_temperature",
"power_watts",
"selfcheck_error",
"selfcheck_result",
+18 -5
View File
@@ -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