From ce6be9f1dddbb031a196d17aa06a9387dc6923f6 Mon Sep 17 00:00:00 2001 From: Etienne Kemp-Rousseau Date: Wed, 4 Nov 2026 20:47:32 -0500 Subject: [PATCH] Map hvac_action from currentState for 24V thermostats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baseboard thermostats can only heat, so the power-based heuristic (heating > 0 → "heating") has always been sufficient. 24V thermostats report a currentState field ("HEATING", "COOLING", "OFF") that carries the actual running state directly, making the power heuristic both unreliable and incomplete (it cannot express cooling). Branch hvac_action on is_low_voltage: use currentState for 24V devices and keep the existing power logic for baseboard devices. Vocabulary confirmed by live observation against a real Sinopé 24V thermostat (issue dvd-dev/hilo#1122). --- pyhilo/device/climate.py | 19 +++++++++++++------ tests/test_climate.py | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/pyhilo/device/climate.py b/pyhilo/device/climate.py index 3a2b09a..c65a32a 100644 --- a/pyhilo/device/climate.py +++ b/pyhilo/device/climate.py @@ -102,9 +102,20 @@ def hvac_action(self) -> str: """ Gets the current HVAC action of the device. + For 24 V thermostats the action comes from currentState ("HEATING", + "COOLING", "OFF"). For baseboard thermostats it is derived from the + reported heating power. + Returns: - str: 'heating' if heating is active, 'idle' otherwise. + str: one of 'heating', 'cooling', or 'idle'. """ + if self.is_low_voltage: + state = self.current_state + if state == "HEATING": + return "heating" + if state == "COOLING": + return "cooling" + return "idle" attr = self.get_value("heating", 0) return "heating" if attr > 0 else "idle" @@ -170,11 +181,7 @@ def allowed_fan_modes(self) -> list[str]: @property def current_state(self) -> str | None: - """Raw running state reported by a 24 V thermostat. - - Exposed for consumers, but deliberately not mapped to hvac_action yet: - its vocabulary has not been observed in enough operating conditions. - """ + """Raw running state reported by a 24 V thermostat ("HEATING", "COOLING", "OFF").""" return self._optional_str("current_state") @property diff --git a/tests/test_climate.py b/tests/test_climate.py index a6854fd..d331f88 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -41,6 +41,33 @@ def _climate(**readings): return device +class TestHvacAction: + @pytest.mark.parametrize( + "current_state, expected", + [ + pytest.param("HEATING", "heating", id="heating"), + pytest.param("COOLING", "cooling", id="cooling"), + pytest.param("OFF", "idle", id="off-is-idle"), + pytest.param(None, "idle", id="unreported-is-idle"), + ], + ) + def test_low_voltage_maps_current_state(self, current_state, expected): + kwargs = {"Thermostat24VMode": "AUTO"} + if current_state is not None: + kwargs["CurrentState"] = current_state + assert _climate(**kwargs).hvac_action == expected + + @pytest.mark.parametrize( + "heating, expected", + [ + pytest.param(1, "heating", id="power-on-is-heating"), + pytest.param(0, "idle", id="power-off-is-idle"), + ], + ) + def test_baseboard_uses_heating_power(self, heating, expected): + assert _climate(Heating=heating).hvac_action == expected + + class TestAsList: def test_none_is_empty(self): assert as_list(None) == []