diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index c71857697..41fc41d52 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -4,7 +4,7 @@ name: Latest commit env: - CACHE_VERSION: 3 + CACHE_VERSION: 4 DEFAULT_PYTHON: "3.9" PRE_COMMIT_HOME: ~/.cache/pre-commit diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eccf1d72..e05fe8c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +# v0.22.1: Improve solution for issue #213 + # v0.22.0: Smile P1 - add a P1 smartmeter device - Change all gateway model names to Gateway - Change Anna Smile name to Smile Anna, Anna model name to ThermoTouch diff --git a/plugwise/__init__.py b/plugwise/__init__.py index 02f2b9c1b..9894ca3c4 100644 --- a/plugwise/__init__.py +++ b/plugwise/__init__.py @@ -1,6 +1,6 @@ """Plugwise module.""" -__version__ = "0.22.0" +__version__ = "0.22.1" from plugwise.smile import Smile from plugwise.stick import Stick diff --git a/plugwise/helper.py b/plugwise/helper.py index 7f585752d..17f5cc02d 100644 --- a/plugwise/helper.py +++ b/plugwise/helper.py @@ -329,7 +329,7 @@ def __init__(self) -> None: self._on_off_device = False self._opentherm_device = False self._outdoor_temp: float - self._schedule_present_state: str + self._schedule_old_states: dict[str, dict[str, str]] = {} self._sched_setpoints: list[float] | None = None self._smile_legacy = False self._stretch_v2 = False diff --git a/plugwise/smile.py b/plugwise/smile.py index de9f5a62b..1fc343a4f 100644 --- a/plugwise/smile.py +++ b/plugwise/smile.py @@ -216,9 +216,14 @@ def _device_data_climate( if self._adam_cooling_enabled or self._lortherm_cooling_enabled: device_data["mode"] = "cool" - self._schedule_present_state = "off" - if device_data["mode"] == "auto": - self._schedule_present_state = "on" + if "None" not in avail_schedules: + loc_schedule_states = {} + for schedule in avail_schedules: + loc_schedule_states[schedule] = "off" + if device_data["mode"] == "auto": + loc_schedule_states[sel_schedule] = "on" + + self._schedule_old_states[loc_id] = loc_schedule_states return device_data @@ -508,7 +513,9 @@ async def async_update(self) -> list[GatewayData | dict[str, DeviceData]]: return [self.gw_data, self.gw_devices] - async def _set_schedule_state_legacy(self, name: str, status: str) -> None: + async def _set_schedule_state_legacy( + self, loc_id: str, name: str, status: str + ) -> None: """Helper-function for set_schedule_state().""" schedule_rule_id: str | None = None for rule in self._domain_objects.findall("rule"): @@ -518,9 +525,13 @@ async def _set_schedule_state_legacy(self, name: str, status: str) -> None: if schedule_rule_id is None: raise PlugwiseError("Plugwise: no schedule with this name available.") - state = "false" + new_state = "false" if status == "on": - state = "true" + new_state = "true" + # If no state change is requested, do nothing + if new_state == self._schedule_old_states[loc_id][name]: + return + locator = f'.//*[@id="{schedule_rule_id}"]/template' for rule in self._domain_objects.findall(locator): template_id = rule.attrib["id"] @@ -529,33 +540,29 @@ async def _set_schedule_state_legacy(self, name: str, status: str) -> None: data = ( "{state}' + f' id="{template_id}" />{new_state}' ) await self._request(uri, method="put", data=data) + self._schedule_old_states[loc_id][name] = new_state async def set_schedule_state( - self, loc_id: str, name: str | None, state: str + self, loc_id: str, name: str | None, new_state: str ) -> None: """Activate/deactivate the Schedule, with the given name, on the relevant Thermostat. Determined from - DOMAIN_OBJECTS. In HA Core used to set the hvac_mode: in practice switch between schedule on - off. """ - if state not in ["on", "off"]: + # Input checking + if new_state not in ["on", "off"]: raise PlugwiseError("Plugwise: invalid schedule state.") - - # Do nothing when name == None and the state does not change. No need to show - # an error, as doing nothing is the correct action in this scenario. if name is None: - if state == self._schedule_present_state: - return - # else, raise an error: raise PlugwiseError( "Plugwise: cannot change schedule-state: no schedule name provided" ) if self._smile_legacy: - await self._set_schedule_state_legacy(name, state) + await self._set_schedule_state_legacy(loc_id, name, new_state) return schedule_rule = self._rule_ids_by_name(name, loc_id) @@ -563,8 +570,8 @@ async def set_schedule_state( if not schedule_rule or schedule_rule is None: raise PlugwiseError("Plugwise: no schedule with this name available.") - # If schedule name is valid but no state change is requested, do nothing - if state == self._schedule_present_state: + # If no state change is requested, do nothing + if new_state == self._schedule_old_states[loc_id][name]: return schedule_rule_id: str = next(iter(schedule_rule)) @@ -584,10 +591,10 @@ async def set_schedule_state( subject = f'' subject = etree.fromstring(subject) - if state == "off": + if new_state == "off": self._last_active[loc_id] = name contexts.remove(subject) - if state == "on": + if new_state == "on": contexts.append(subject) contexts = etree.tostring(contexts, encoding="unicode").rstrip() @@ -598,8 +605,7 @@ async def set_schedule_state( f"{template}{contexts}" ) await self._request(uri, method="put", data=data) - - self._schedule_present_state = state + self._schedule_old_states[loc_id][name] = new_state async def _set_preset_legacy(self, preset: str) -> None: """Set the given Preset on the relevant Thermostat - from DOMAIN_OBJECTS.""" diff --git a/tests/test_smile.py b/tests/test_smile.py index 1ecb892a1..0c7ea7fe5 100644 --- a/tests/test_smile.py +++ b/tests/test_smile.py @@ -584,7 +584,9 @@ async def tinker_thermostat( result_1 = await self.tinker_thermostat_temp(smile, loc_id, unhappy) result_2 = await self.tinker_thermostat_preset(smile, loc_id, unhappy) - smile._schedule_present_state = "off" + if smile._schedule_old_states != {}: + for item in smile._schedule_old_states[loc_id]: + smile._schedule_old_states[loc_id][item] = "off" result_3 = await self.tinker_thermostat_schedule( smile, loc_id, "on", good_schedules, single, unhappy ) @@ -702,7 +704,7 @@ async def test_connect_legacy_anna(self): result = await self.tinker_thermostat( smile, - "c34c6864216446528e95d88985e714cc", + "0000aaaa0000aaaa0000aaaa0000aa00", good_schedules=[ "Thermostat schedule", ], @@ -712,9 +714,10 @@ async def test_connect_legacy_anna(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, - "c34c6864216446528e95d88985e714cc", + "0000aaaa0000aaaa0000aaaa0000aa00", good_schedules=[ "Thermostat schedule", ], @@ -797,24 +800,45 @@ async def test_connect_legacy_anna_2(self): await self.device_test(smile, testdata) assert smile.gateway_id == "be81e3f8275b4129852c4d8d550ae2eb" - assert self.device_items == 43 + # assert self.device_items = 47 assert not self.notifications result = await self.tinker_thermostat( smile, - "c34c6864216446528e95d88985e714cc", + "be81e3f8275b4129852c4d8d550ae2eb", good_schedules=[ "Thermostat schedule", ], ) assert result + + smile._schedule_old_states["be81e3f8275b4129852c4d8d550ae2eb"][ + "Thermostat schedule" + ] = "off" + result_1 = await self.tinker_thermostat_schedule( + smile, + "be81e3f8275b4129852c4d8d550ae2eb", + "on", + good_schedules=["Thermostat schedule"], + single=True, + ) + result_2 = await self.tinker_thermostat_schedule( + smile, + "be81e3f8275b4129852c4d8d550ae2eb", + "on", + good_schedules=["Thermostat schedule"], + single=True, + ) + assert result_1 and result_2 + await smile.close_connection() await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, - "c34c6864216446528e95d88985e714cc", + "be81e3f8275b4129852c4d8d550ae2eb", good_schedules=[ "Thermostat schedule", ], @@ -882,8 +906,6 @@ async def test_connect_smile_p1_v2(self): await smile.close_connection() await self.disconnect(server, client) - server, smile, client = await self.connect_wrapper(raise_timeout=True) - @pytest.mark.asyncio async def test_connect_smile_p1_v2_2(self): """Test another legacy P1 device.""" @@ -1045,6 +1067,7 @@ async def test_connect_anna_v4(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "eb5309212bf5407bb143e5bfa3b18aee", @@ -1155,6 +1178,7 @@ async def test_connect_anna_v4_dhw(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "eb5309212bf5407bb143e5bfa3b18aee", @@ -1201,6 +1225,7 @@ async def test_connect_anna_v4_no_tag(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "eb5309212bf5407bb143e5bfa3b18aee", @@ -1284,6 +1309,7 @@ async def test_connect_anna_without_boiler_fw3(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "c34c6864216446528e95d88985e714cc", @@ -1365,6 +1391,7 @@ async def test_connect_anna_without_boiler_fw4(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "c34c6864216446528e95d88985e714cc", @@ -1446,6 +1473,7 @@ async def test_connect_anna_without_boiler_fw42(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "c34c6864216446528e95d88985e714cc", @@ -1580,6 +1608,7 @@ async def test_connect_adam_plus_anna(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "009490cc2f674ce6b576863fbb64f867", @@ -1827,7 +1856,9 @@ async def test_connect_adam_plus_anna_new(self): ) assert result - smile._schedule_present_state = "off" + smile._schedule_old_states["f2bf9048bef64cc5b6d5110154e33c81"][ + "Badkamer" + ] = "off" result_1 = await self.tinker_thermostat_schedule( smile, "f2bf9048bef64cc5b6d5110154e33c81", @@ -2264,6 +2295,7 @@ async def test_connect_adam_zone_per_device(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, @@ -2681,6 +2713,7 @@ async def test_connect_adam_multiple_devices_per_zone(self): await self.disconnect(server, client) server, smile, client = await self.connect_wrapper(raise_timeout=True) + await self.device_test(smile, testdata) result = await self.tinker_thermostat( smile, "c50f167537524366a5af7aa3942feb1e",