Merge pull request #375 from mbillow/claude/issue-367-hnrc8y
airconditioner: ungate outdoor_temperature from is_legacy_board
This commit is contained in:
@@ -372,6 +372,33 @@ 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 _reports_celsius(resources):
|
||||||
|
"""Whether this subdevice's own /temperatures/vs/0 declares Celsius (or
|
||||||
|
says nothing at all, `_temps_vs_unit`'s default).
|
||||||
|
|
||||||
|
Guards `OutdoorTemp_`'s -55 offset below: issue #367's field validation
|
||||||
|
(48h against weather.forecast_home, r=0.92) ran on Celsius-locale boards
|
||||||
|
only -- 13 of the 14 non-legacy fixtures that carry the token declare
|
||||||
|
Celsius, and the offset's own calibration comment is a Celsius reading
|
||||||
|
too. The one Fahrenheit-locale exception on record
|
||||||
|
(`airconditioner_lnx_rac_heatpump`) has nothing to confirm the same
|
||||||
|
additive constant, or degrees C rather than F, still hold -- so this
|
||||||
|
stays off boards that declare anything else, rather than guess."""
|
||||||
|
return _temps_vs_unit(resources.get(HREF_TEMPS_VS) or {}) == "°C"
|
||||||
|
|
||||||
|
|
||||||
def _option_token_on(prefix):
|
def _option_token_on(prefix):
|
||||||
return lambda rep: _option_token(rep, prefix) == "On"
|
return lambda rep: _option_token(rep, prefix) == "On"
|
||||||
|
|
||||||
@@ -782,13 +809,34 @@ CLIMATE = Capability(
|
|||||||
),
|
),
|
||||||
# Outdoor temperature, offset by 55 -- calibrated against an
|
# Outdoor temperature, offset by 55 -- calibrated against an
|
||||||
# independent thermometer (token 75 while it read 20.3°C).
|
# independent thermometer (token 75 while it read 20.3°C).
|
||||||
|
#
|
||||||
|
# exists_fn is token-presence-only (issue #367), not is_legacy_board:
|
||||||
|
# gating it there 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.
|
||||||
|
# `_reports_celsius` keeps that presence check from also claiming
|
||||||
|
# the -55 Celsius offset for board generations it was never
|
||||||
|
# validated on -- see its own docstring.
|
||||||
|
#
|
||||||
|
# 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(
|
SensorDesc(
|
||||||
key="outdoor_temperature",
|
key="outdoor_temperature",
|
||||||
rep_fn=_option_token_num("OutdoorTemp", offset=55),
|
rep_fn=_option_token_num("OutdoorTemp", offset=55),
|
||||||
exists_fn=_has_option_token("OutdoorTemp"),
|
exists_fn=lambda rep, resources: (
|
||||||
|
_has_option_token_any_board("OutdoorTemp")(rep, resources)
|
||||||
|
and _reports_celsius(resources)
|
||||||
|
),
|
||||||
device_class="temperature",
|
device_class="temperature",
|
||||||
state_class="measurement",
|
state_class="measurement",
|
||||||
unit="°C",
|
unit="°C",
|
||||||
|
enabled_default=False,
|
||||||
icon="mdi:home-thermometer-outline",
|
icon="mdi:home-thermometer-outline",
|
||||||
),
|
),
|
||||||
# Filter time in tenths of an hour, counting UP since last filter
|
# Filter time in tenths of an hour, counting UP since last filter
|
||||||
|
|||||||
+1
@@ -20,6 +20,7 @@
|
|||||||
"fine_dust",
|
"fine_dust",
|
||||||
"humidity",
|
"humidity",
|
||||||
"odor",
|
"odor",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"super_fine_dust",
|
"super_fine_dust",
|
||||||
"tropical_night_mode"
|
"tropical_night_mode"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"mute_once",
|
"mute_once",
|
||||||
"odor_controller_active",
|
"odor_controller_active",
|
||||||
"odor_controller_progress",
|
"odor_controller_progress",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_energy_kwh",
|
"power_energy_kwh",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"selfcheck_error",
|
"selfcheck_error",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"mute_once",
|
"mute_once",
|
||||||
"odor_controller_active",
|
"odor_controller_active",
|
||||||
"odor_controller_progress",
|
"odor_controller_progress",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"tropical_night_mode"
|
"tropical_night_mode"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
"mute_once",
|
"mute_once",
|
||||||
"odor_controller_active",
|
"odor_controller_active",
|
||||||
"odor_controller_progress",
|
"odor_controller_progress",
|
||||||
|
"outdoor_temperature",
|
||||||
"periodic_air_sensing",
|
"periodic_air_sensing",
|
||||||
"periodic_sensing_skip_status",
|
"periodic_sensing_skip_status",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"firmware_update",
|
"firmware_update",
|
||||||
"humidity",
|
"humidity",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"tropical_night_mode"
|
"tropical_night_mode"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"last_air_sensing_level",
|
"last_air_sensing_level",
|
||||||
"last_air_sensing_time",
|
"last_air_sensing_time",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
|
"outdoor_temperature",
|
||||||
"periodic_air_sensing",
|
"periodic_air_sensing",
|
||||||
"periodic_sensing_skip_status",
|
"periodic_sensing_skip_status",
|
||||||
"selfcheck_error",
|
"selfcheck_error",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"energy_saved_kwh",
|
"energy_saved_kwh",
|
||||||
"firmware_update",
|
"firmware_update",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
|
"outdoor_temperature",
|
||||||
"selfcheck_error",
|
"selfcheck_error",
|
||||||
"selfcheck_result",
|
"selfcheck_result",
|
||||||
"selfcheck_status",
|
"selfcheck_status",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"mute_once",
|
"mute_once",
|
||||||
"odor_controller_active",
|
"odor_controller_active",
|
||||||
"odor_controller_progress",
|
"odor_controller_progress",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"tropical_night_mode"
|
"tropical_night_mode"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"firmware_update",
|
"firmware_update",
|
||||||
"humidity",
|
"humidity",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"tropical_night_mode"
|
"tropical_night_mode"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"mute_once",
|
"mute_once",
|
||||||
"odor_controller_active",
|
"odor_controller_active",
|
||||||
"odor_controller_progress",
|
"odor_controller_progress",
|
||||||
|
"outdoor_temperature",
|
||||||
"selfcheck_error",
|
"selfcheck_error",
|
||||||
"selfcheck_result",
|
"selfcheck_result",
|
||||||
"selfcheck_status",
|
"selfcheck_status",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"firmware_update",
|
"firmware_update",
|
||||||
"humidity",
|
"humidity",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"tropical_night_mode"
|
"tropical_night_mode"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"fine_dust",
|
"fine_dust",
|
||||||
"humidity",
|
"humidity",
|
||||||
"odor",
|
"odor",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"super_fine_dust",
|
"super_fine_dust",
|
||||||
"tropical_night_mode"
|
"tropical_night_mode"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"firmware_update",
|
"firmware_update",
|
||||||
"humidity",
|
"humidity",
|
||||||
"mute_once",
|
"mute_once",
|
||||||
|
"outdoor_temperature",
|
||||||
"power_watts",
|
"power_watts",
|
||||||
"selfcheck_error",
|
"selfcheck_error",
|
||||||
"selfcheck_result",
|
"selfcheck_result",
|
||||||
|
|||||||
@@ -129,22 +129,35 @@ def test_token_entities_present_with_calibrated_values():
|
|||||||
|
|
||||||
|
|
||||||
def test_token_entities_stay_off_newer_boards():
|
def test_token_entities_stay_off_newer_boards():
|
||||||
"""Newer families carry Volume/Sleep/OutdoorTemp/Autoclean tokens too,
|
"""Newer families carry Volume/Sleep/Autoclean tokens too, while also
|
||||||
while also exposing those settings as dedicated resources -- ungated, the
|
exposing those settings as dedicated resources -- ungated, the token
|
||||||
token entities would duplicate them (auto clean) or apply a scale
|
entities would duplicate them."""
|
||||||
calibrated on another board generation (outdoor temperature)."""
|
|
||||||
state = _state("airconditioner_tp1x_rac")
|
state = _state("airconditioner_tp1x_rac")
|
||||||
for key in (
|
for key in (
|
||||||
"spi",
|
"spi",
|
||||||
"auto_clean_legacy",
|
"auto_clean_legacy",
|
||||||
"air_monitoring",
|
"air_monitoring",
|
||||||
"good_sleep",
|
"good_sleep",
|
||||||
"outdoor_temperature",
|
|
||||||
"filter_time",
|
"filter_time",
|
||||||
):
|
):
|
||||||
assert key not in state, key
|
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():
|
def test_climate_legacy_airflow_gate_agrees_with_is_legacy_board():
|
||||||
"""issue #161: climate.py's _legacy_airflow() delegates to
|
"""issue #161: climate.py's _legacy_airflow() delegates to
|
||||||
capabilities/airconditioner.py's is_legacy_board() instead of
|
capabilities/airconditioner.py's is_legacy_board() instead of
|
||||||
|
|||||||
@@ -654,6 +654,22 @@ def test_lnx_rac_heatpump_no_unbound_hrefs():
|
|||||||
assert unbound == []
|
assert unbound == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_lnx_rac_heatpump_outdoor_temperature_stays_off_fahrenheit_boards():
|
||||||
|
"""issue #367's -55 offset was field-validated on Celsius-locale boards
|
||||||
|
only; this fixture's own /temperatures/vs/0 declares Fahrenheit despite
|
||||||
|
carrying an OutdoorTemp_ token, so the sensor stays off rather than
|
||||||
|
apply an unvalidated offset/unit to it (see _reports_celsius)."""
|
||||||
|
reg, resources = _ac_lnx_rac_heatpump()
|
||||||
|
temps_item = resources["/temperatures/vs/0"]["x.com.samsung.da.items"][0]
|
||||||
|
assert temps_item["x.com.samsung.da.unit"] == "Fahrenheit"
|
||||||
|
options = resources["/mode/vs/0"]["x.com.samsung.da.options"]
|
||||||
|
assert any(o.startswith("OutdoorTemp_") for o in options)
|
||||||
|
|
||||||
|
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||||
|
state = flatten(bound, resources)
|
||||||
|
assert "outdoor_temperature" not in state
|
||||||
|
|
||||||
|
|
||||||
def test_lnx_rac_heatpump_absence_power_saving_state():
|
def test_lnx_rac_heatpump_absence_power_saving_state():
|
||||||
reg, resources = _ac_lnx_rac_heatpump()
|
reg, resources = _ac_lnx_rac_heatpump()
|
||||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||||
|
|||||||
Reference in New Issue
Block a user