diff --git a/CHANGELOG.md b/CHANGELOG.md index 381b4c96d..fff8ac6f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Ongoing + +- A zone shows the cooling setpoint of each preset only when the gateway has cooling and cooling is allowed for that zone. A zone that may not cool shows the heating setpoint alone, and, when no schedule is selected, reports heating as its climate mode. +- Setting the heating setpoint of a preset no longer fails in a zone that may not cool when the value is above the cooling setpoint stored for that zone. + ## v1.15.0 - Contribution PR's [#936](https://github.com/plugwise/python-plugwise/pull/936) and [#938](https://github.com/plugwise/python-plugwise/pull/938) by @MartvanMale: diff --git a/fixtures/adam_heatpump_cooling/data.json b/fixtures/adam_heatpump_cooling/data.json index 005e19dca..f8f13e5c0 100644 --- a/fixtures/adam_heatpump_cooling/data.json +++ b/fixtures/adam_heatpump_cooling/data.json @@ -178,8 +178,7 @@ "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "available_schedules": [ @@ -191,18 +190,16 @@ "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 18.5, + "setpoint": 18.5, "upper_bound": 30.0 }, - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "model": "ThermoZone", @@ -210,8 +207,7 @@ "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "preset_modes": [ @@ -243,8 +239,7 @@ "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "vendor": "Plugwise", @@ -341,8 +336,7 @@ "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 23.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "available_schedules": [ @@ -354,8 +348,7 @@ "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "climate_mode": "auto", @@ -364,8 +357,7 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "model": "ThermoZone", @@ -373,8 +365,7 @@ "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "preset_modes": [ @@ -405,8 +396,7 @@ "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "vendor": "Plugwise", @@ -536,8 +526,7 @@ "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 23.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "available_schedules": [ @@ -549,8 +538,7 @@ "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "climate_mode": "auto", @@ -559,8 +547,7 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "model": "ThermoZone", @@ -568,8 +555,7 @@ "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "preset_modes": [ @@ -601,8 +587,7 @@ "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "vendor": "Plugwise", @@ -617,8 +602,7 @@ "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "available_schedules": [ @@ -630,18 +614,16 @@ "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 18.5, + "setpoint": 18.5, "upper_bound": 30.0 }, - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 20.0, + "setpoint": 20.0, "upper_bound": 30.0 }, "model": "ThermoZone", @@ -649,8 +631,7 @@ "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "preset_modes": [ @@ -681,8 +662,7 @@ "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "vendor": "Plugwise", @@ -899,8 +879,7 @@ "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, "available_schedules": [ @@ -912,18 +891,16 @@ "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "model": "ThermoZone", @@ -931,8 +908,7 @@ "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "preset_modes": [ @@ -964,8 +940,7 @@ "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "vendor": "Plugwise", @@ -1078,8 +1053,7 @@ "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 21.5, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, "available_schedules": [ @@ -1091,18 +1065,16 @@ "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, "model": "ThermoZone", @@ -1110,8 +1082,7 @@ "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "preset_modes": [ @@ -1143,8 +1114,7 @@ "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "vendor": "Plugwise", diff --git a/plugwise/data.py b/plugwise/data.py index e5989d2ae..56de13064 100644 --- a/plugwise/data.py +++ b/plugwise/data.py @@ -268,6 +268,8 @@ def _climate_data(self, location_id: str, entity: GwEntityData) -> None: if entity.get("location") is not None: loc_id = entity["location"] + zone_cools = self._cooling_present and self._cooling_allowed(loc_id) + # Presets entity["preset_modes"] = None entity["active_preset"] = None @@ -277,7 +279,7 @@ def _climate_data(self, location_id: str, entity: GwEntityData) -> None: entity["active_preset"] = self._preset(loc_id) # Only a zone's own rule can be edited without touching other zones if self._preset_rule_id(loc_id) is not None: - self._preset_temperatures(presets, entity) + self._preset_temperatures(zone_cools, presets, entity) # Schedule entity["available_schedules"] = [OFF] @@ -293,7 +295,7 @@ def _climate_data(self, location_id: str, entity: GwEntityData) -> None: self._count += 1 if sel_schedule == OFF: entity["climate_mode"] = "heat" - if self._cooling_present: + if zone_cools: entity["climate_mode"] = ( "cool" if self.check_reg_mode("cooling") else "heat_cool" ) @@ -306,17 +308,17 @@ def _climate_data(self, location_id: str, entity: GwEntityData) -> None: ) def _preset_temperatures( - self, presets: dict[str, list[float]], entity: GwEntityData + self, zone_cools: bool, presets: dict[str, list[float]], entity: GwEntityData ) -> None: """Helper-function for _climate_data(). Add the setpoints of each preset as an adjustable value: the heating setpoint, - or with cooling present the heating (low) and cooling (high) setpoints. + or where the zone may cool the heating (low) and cooling (high) setpoints. """ for key, preset in PRESET_TEMPERATURES.items(): if preset in presets: heating, cooling = presets[preset] - if self._cooling_present: + if zone_cools: entity[key] = ActuatorData( lower_bound=MIN_SETPOINT, resolution=PRESET_TEMPERATURE_RESOLUTION, diff --git a/plugwise/helper.py b/plugwise/helper.py index 4a2645fa2..5ea6b73e5 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -920,6 +920,22 @@ def _preset(self, loc_id: str) -> str | None: return None # pragma: no cover + def _cooling_allowed(self, loc_id: str) -> bool: + """Helper-function for data.py: _climate_data() and smile.py: set_preset_temperature(). + + Collect whether cooling is allowed in a zone, based on Location ID. + Assume it is where the zone says nothing, leaving the gateway-wide + capability to decide. + """ + locator = ( + f'./location[@id="{loc_id}"]/actuator_functionalities' + "/thermostat_functionality/cooling_allowed" + ) + if (allowed := self._domain_objects.find(locator)) is not None: + return str(allowed.text) == "true" + + return True + def _presets(self, loc_id: str) -> dict[str, list[float]]: """Collect Presets for a Thermostat based on location_id.""" presets: dict[str, list[float]] = {} diff --git a/plugwise/smile.py b/plugwise/smile.py index 5e44d86b6..3337b0602 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -265,8 +265,12 @@ async def set_preset_temperature( heating = temperature else: cooling = temperature - # Without cooling the stored cooling setpoint is a default nobody can change - if self._cooling_present and heating > cooling: + # Where a zone may not cool, the stored cooling setpoint is a default nobody can change + if ( + self._cooling_present + and self._cooling_allowed(loc_id) + and heating > cooling + ): raise PlugwiseError( f"Plugwise: the {preset} heating setpoint {heating} is above its cooling setpoint {cooling}. Lower the heating setpoint or raise the cooling setpoint." ) diff --git a/tests/data/adam/adam_heatpump_cooling.json b/tests/data/adam/adam_heatpump_cooling.json index 01313001e..cb7de51bd 100644 --- a/tests/data/adam/adam_heatpump_cooling.json +++ b/tests/data/adam/adam_heatpump_cooling.json @@ -181,7 +181,7 @@ "Werkdag schema", "Weekend" ], - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "model": "ThermoZone", @@ -196,36 +196,31 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 18.5, + "setpoint": 18.5, "upper_bound": 30.0 }, "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "select_schedule": "off", @@ -359,36 +354,31 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 23.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "select_schedule": "Werkdag schema", @@ -554,36 +544,31 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 23.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 20.5, + "setpoint": 20.5, "upper_bound": 30.0 }, "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "select_schedule": "Werkdag schema", @@ -620,7 +605,7 @@ "Werkdag schema", "Weekend" ], - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "model": "ThermoZone", @@ -635,36 +620,31 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 20.0, + "setpoint": 20.0, "upper_bound": 30.0 }, "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 18.5, + "setpoint": 18.5, "upper_bound": 30.0 }, "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "select_schedule": "off", @@ -902,7 +882,7 @@ "Werkdag schema", "Weekend" ], - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "model": "ThermoZone", @@ -917,36 +897,31 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 18.0, + "setpoint": 18.0, "upper_bound": 30.0 }, "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "select_schedule": "off", @@ -1081,7 +1056,7 @@ "Werkdag schema", "Weekend" ], - "climate_mode": "cool", + "climate_mode": "heat", "control_state": "idle", "dev_class": "climate", "model": "ThermoZone", @@ -1096,36 +1071,31 @@ "home_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 22.0, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, "asleep_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 21.5, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, "away_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 25.0, - "setpoint_low": 17.5, + "setpoint": 17.5, "upper_bound": 30.0 }, "vacation_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 28.0, - "setpoint_low": 16.0, + "setpoint": 16.0, "upper_bound": 30.0 }, "no_frost_preset_temperature": { "lower_bound": 4.0, "resolution": 0.5, - "setpoint_high": 30.0, - "setpoint_low": 10.0, + "setpoint": 10.0, "upper_bound": 30.0 }, "select_schedule": "off", diff --git a/tests/test_adam.py b/tests/test_adam.py index d6f972e4c..d3c630991 100644 --- a/tests/test_adam.py +++ b/tests/test_adam.py @@ -470,7 +470,7 @@ async def test_adam_heatpump_cooling(self): server, api, client = await self.connect_wrapper() test_items = await self.device_test(api, "2022-01-02 00:00:01", testdata) - assert self.entity_items == 793 + assert self.entity_items == 763 assert test_items == self.entity_items assert self.cooling_present assert self._cooling_enabled @@ -480,6 +480,20 @@ async def test_adam_heatpump_cooling(self): assert "setpoint" in zone["thermostat"] assert zone["home_preset_temperature"]["setpoint_low"] == 19.0 assert zone["home_preset_temperature"]["setpoint_high"] == 22.0 + # 20e73585 has cooling_allowed false in the fixture + # pylint: disable-next=protected-access + no_cool = api._smile_api.gw_entities["20e735858f8146cead98b873177a4f99"] + assert no_cool["home_preset_temperature"]["setpoint"] == 18.0 + assert "setpoint_high" not in no_cool["home_preset_temperature"] + # Its stored cooling setpoint is a default, so it must not block a write + await api.set_number( + "20e735858f8146cead98b873177a4f99", "home_preset_temperature", 22.5 + ) + # pylint: disable-next=protected-access + assert api._smile_api._presets("20e735858f8146cead98b873177a4f99")["home"] == [ + 22.5, + 22.0, + ] await api.close_connection() await self.disconnect(server, client)