From 6025bc4f38687a1bdd17b99b265862b3b0739b2b Mon Sep 17 00:00:00 2001 From: trip-g Date: Tue, 21 Jul 2026 16:50:32 -0400 Subject: [PATCH] Add heatAndCoolDemand support to LyricDevice Resideo's device response includes a heatAndCoolDemand object (equipment stage, demand percentage, active stages, fan/circulation fan request) that aiolyric never parsed at all. Adds a HeatAndCoolDemand class and LyricDevice.heat_and_cool_demand property, following the same pattern as the existing OperationStatus handling. Note the API uses PascalCase keys here (CurrentStage, Demand, Mode, StagesOn, FanRequest, CirculationFanRequest), unlike the camelCase used elsewhere in the same response - that's what Resideo actually returns, not a typo. Co-Authored-By: Claude Sonnet 5 --- aiolyric/objects/device.py | 39 ++++++++++++++++++++++++++++++++++ tests/__init__.py | 16 ++++++++++++++ tests/objects/test_device.py | 24 +++++++++++++++++++++ tests/objects/test_location.py | 26 +++++++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/aiolyric/objects/device.py b/aiolyric/objects/device.py index 113c73e..aea341d 100644 --- a/aiolyric/objects/device.py +++ b/aiolyric/objects/device.py @@ -191,6 +191,40 @@ def circulation_fan_request(self): return self.attributes.get("circulationFanRequest", False) +class HeatAndCoolDemand(LyricBaseObject): + """Heat and cool demand.""" + + @property + def current_stage(self): + """Return the current equipment stage.""" + return self.attributes.get("CurrentStage", None) + + @property + def demand(self): + """Return the demand percentage.""" + return self.attributes.get("Demand", None) + + @property + def mode(self): + """Return the mode.""" + return self.attributes.get("Mode", None) + + @property + def stages_on(self): + """Return which equipment stages are on.""" + return self.attributes.get("StagesOn", []) + + @property + def fan_request(self): + """Return the fan request.""" + return self.attributes.get("FanRequest", False) + + @property + def circulation_fan_request(self): + """Return the circulation fan request.""" + return self.attributes.get("CirculationFanRequest", False) + + class LyricDevice(LyricBaseClient): """Lyric Device.""" @@ -364,6 +398,11 @@ def operation_status(self): """Return the operation status.""" return OperationStatus(self.attributes.get("operationStatus", {})) + @property + def heat_and_cool_demand(self): + """Return the heat and cool demand.""" + return HeatAndCoolDemand(self.attributes.get("heatAndCoolDemand", {})) + @property def device_model(self): """Return the device model.""" diff --git a/tests/__init__.py b/tests/__init__.py index ee93752..5f1290f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -62,6 +62,14 @@ "fanRequest": False, "circulationFanRequest": False, }, + "heatAndCoolDemand": { + "CurrentStage": 1, + "Demand": 33, + "Mode": "Cool", + "StagesOn": [True, False, False, False], + "FanRequest": True, + "CirculationFanRequest": False, + }, "deviceModel": "T5-T6", "fanMode": "Auto", } @@ -127,6 +135,14 @@ "fanRequest": False, "circulationFanRequest": False, }, + "heatAndCoolDemand": { + "CurrentStage": 1, + "Demand": 33, + "Mode": "Cool", + "StagesOn": [True, False, False, False], + "FanRequest": True, + "CirculationFanRequest": False, + }, "deviceModel": "T5-T6", } ], diff --git a/tests/objects/test_device.py b/tests/objects/test_device.py index 5b81664..1389d72 100644 --- a/tests/objects/test_device.py +++ b/tests/objects/test_device.py @@ -142,5 +142,29 @@ def test_device( obj.operation_status.circulation_fan_request == device_fixture_response["operationStatus"]["circulationFanRequest"] ) + assert ( + obj.heat_and_cool_demand.current_stage + == device_fixture_response["heatAndCoolDemand"]["CurrentStage"] + ) + assert ( + obj.heat_and_cool_demand.demand + == device_fixture_response["heatAndCoolDemand"]["Demand"] + ) + assert ( + obj.heat_and_cool_demand.mode + == device_fixture_response["heatAndCoolDemand"]["Mode"] + ) + assert ( + obj.heat_and_cool_demand.stages_on + == device_fixture_response["heatAndCoolDemand"]["StagesOn"] + ) + assert ( + obj.heat_and_cool_demand.fan_request + == device_fixture_response["heatAndCoolDemand"]["FanRequest"] + ) + assert ( + obj.heat_and_cool_demand.circulation_fan_request + == device_fixture_response["heatAndCoolDemand"]["CirculationFanRequest"] + ) assert obj.device_model == device_fixture_response["deviceModel"] assert obj.fan_mode == device_fixture_response["fanMode"] diff --git a/tests/objects/test_location.py b/tests/objects/test_location.py index 095cfcb..d7b701e 100644 --- a/tests/objects/test_location.py +++ b/tests/objects/test_location.py @@ -204,6 +204,32 @@ def test_location( "circulationFanRequest" ] ) + assert ( + obj.devices[0].heat_and_cool_demand.current_stage + == location_fixture_response["devices"][0]["heatAndCoolDemand"]["CurrentStage"] + ) + assert ( + obj.devices[0].heat_and_cool_demand.demand + == location_fixture_response["devices"][0]["heatAndCoolDemand"]["Demand"] + ) + assert ( + obj.devices[0].heat_and_cool_demand.mode + == location_fixture_response["devices"][0]["heatAndCoolDemand"]["Mode"] + ) + assert ( + obj.devices[0].heat_and_cool_demand.stages_on + == location_fixture_response["devices"][0]["heatAndCoolDemand"]["StagesOn"] + ) + assert ( + obj.devices[0].heat_and_cool_demand.fan_request + == location_fixture_response["devices"][0]["heatAndCoolDemand"]["FanRequest"] + ) + assert ( + obj.devices[0].heat_and_cool_demand.circulation_fan_request + == location_fixture_response["devices"][0]["heatAndCoolDemand"][ + "CirculationFanRequest" + ] + ) assert ( obj.devices[0].device_model == location_fixture_response["devices"][0]["deviceModel"]