From 7b7e635eeae2f71b0440a640a0998e689d82cadf Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Fri, 22 May 2026 19:42:26 +0200 Subject: [PATCH 01/26] Add manual-to-auto hvac_mode testcase --- tests/components/plugwise/test_climate.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 56af94292..20ee8f010 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -527,6 +527,24 @@ async def test_anna_climate_entity_climate_changes( "c784ee9fdab44e1395b8dee7d7a497d5", STATE_OFF, "standaard", ) + data = mock_smile_anna.async_update.return_value + data["3cb70739631c4d17a86b8b12e8a5161b"]["climate_mode"] = "heat" + with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): + freezer.tick(timedelta(minutes=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.anna", ATTR_HVAC_MODE: HVACMode.AUTO}, + blocking=True, + ) + assert mock_smile_anna.set_schedule_state.call_count == 2 + mock_smile_anna.set_schedule_state.assert_called_with( + "c784ee9fdab44e1395b8dee7d7a497d5", STATE_ON, "standaard", + ) + # Mock user deleting last schedule from app or browser data = mock_smile_anna.async_update.return_value data["3cb70739631c4d17a86b8b12e8a5161b"]["available_schedules"] = [] From 74fbe0be98c35d60341aeeccbed4ec6149f59f9c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 23 May 2026 10:32:45 +0200 Subject: [PATCH 02/26] Update typing of from_dict() function --- custom_components/plugwise/climate.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index 71bcf4cbd..a2320f740 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import asdict, dataclass -from typing import Any +from typing import Any, Self from homeassistant.components.climate import ( ATTR_HVAC_MODE, @@ -103,12 +103,15 @@ def as_dict(self) -> dict[str, Any]: return asdict(self) @classmethod - def from_dict(cls, restored: dict[str, Any]) -> PlugwiseClimateExtraStoredData: + def from_dict(cls, restored: dict[str, Any]) -> Self | None: """Initialize a stored data object from a dict.""" - return cls( - last_active_schedule=restored.get("last_active_schedule"), - previous_action_mode=restored.get("previous_action_mode"), - ) + try: + return cls( + restored["last_active_schedule"], + restored["previous_action_mode"], + ) + except KeyError: + return None class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity): @@ -185,8 +188,8 @@ def current_temperature(self) -> float | None: def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData: """Return text specific state data to be restored.""" return PlugwiseClimateExtraStoredData( - last_active_schedule=self._last_active_schedule, - previous_action_mode=self._previous_action_mode, + self._last_active_schedule, + self._previous_action_mode, ) @property From e06f1f4b42af58913a3b46741f53d32228bb5b0b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 23 May 2026 11:21:35 +0200 Subject: [PATCH 03/26] Revert changes --- custom_components/plugwise/climate.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index a2320f740..a79476c14 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -105,13 +105,10 @@ def as_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, restored: dict[str, Any]) -> Self | None: """Initialize a stored data object from a dict.""" - try: - return cls( - restored["last_active_schedule"], - restored["previous_action_mode"], + return cls( + last_active_schedule=restored.get("last_active_schedule"), + previous_action_mode=restored.get("previous_action_mode"), ) - except KeyError: - return None class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity): @@ -188,8 +185,8 @@ def current_temperature(self) -> float | None: def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData: """Return text specific state data to be restored.""" return PlugwiseClimateExtraStoredData( - self._last_active_schedule, - self._previous_action_mode, + last_active_schedule=self._last_active_schedule, + previous_action_mode=self._previous_action_mode, ) @property From 7befc1a37ff8703657c4303a09ee904d33f7e055 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 23 May 2026 11:32:58 +0200 Subject: [PATCH 04/26] Improve/simplify restore_state related code --- custom_components/plugwise/climate.py | 36 +++++++++++---------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index a79476c14..b24abaed7 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -102,14 +102,6 @@ def as_dict(self) -> dict[str, Any]: """Return a dict representation of the text data.""" return asdict(self) - @classmethod - def from_dict(cls, restored: dict[str, Any]) -> Self | None: - """Initialize a stored data object from a dict.""" - return cls( - last_active_schedule=restored.get("last_active_schedule"), - previous_action_mode=restored.get("previous_action_mode"), - ) - class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity): """Representation of a Plugwise thermostat.""" @@ -123,18 +115,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity): _last_active_schedule: str | None = None _previous_action_mode: str | None = HVACAction.HEATING.value - async def async_added_to_hass(self) -> None: - """Run when entity about to be added.""" - await super().async_added_to_hass() - - if extra_data := await self.async_get_last_extra_data(): - plugwise_extra_data = PlugwiseClimateExtraStoredData.from_dict( - extra_data.as_dict() - ) - self._last_active_schedule = plugwise_extra_data.last_active_schedule - self._previous_action_mode = ( - plugwise_extra_data.previous_action_mode or HVACAction.HEATING.value - ) def __init__( self, @@ -181,12 +161,24 @@ def current_temperature(self) -> float | None: """Return the current temperature.""" return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE) + async def async_added_to_hass(self) -> None: + """Run when entity about to be added.""" + + extra_data = await self.async_get_last_extra_data() + if extra_data is not None: + self._last_active_schedule = extra_data.as_dict()["last_active_schedule"] + self._previous_action_mode = ( + extra_data.as_dict()["previous_action_mode"] or HVACAction.HEATING.value + ) + + await super().async_added_to_hass() + @property def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData: """Return text specific state data to be restored.""" return PlugwiseClimateExtraStoredData( - last_active_schedule=self._last_active_schedule, - previous_action_mode=self._previous_action_mode, + self._last_active_schedule, + self._previous_action_mode, ) @property From 6c80a3d40c770f5b4f3c529b063b0936f565edb3 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 23 May 2026 12:07:36 +0200 Subject: [PATCH 05/26] Ruff fixes --- custom_components/plugwise/climate.py | 2 +- tests/components/plugwise/test_climate.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index b24abaed7..b9931c639 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import asdict, dataclass -from typing import Any, Self +from typing import Any from homeassistant.components.climate import ( ATTR_HVAC_MODE, diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 20ee8f010..e7c4895d7 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -532,8 +532,8 @@ async def test_anna_climate_entity_climate_changes( with patch(HA_PLUGWISE_SMILE_ASYNC_UPDATE, return_value=data): freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() - + await hass.async_block_till_done() + await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, From 3384dcea35f2553ebb5d7f82d4b3d228c53ce559 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 23 May 2026 12:23:18 +0200 Subject: [PATCH 06/26] Use specific selfs for restore_state atttributes --- custom_components/plugwise/climate.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index b9931c639..60adf1437 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -112,10 +112,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity, RestoreEntity): _attr_translation_key = DOMAIN _enable_turn_on_off_backwards_compatibility = False - _last_active_schedule: str | None = None - _previous_action_mode: str | None = HVACAction.HEATING.value - - def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, @@ -127,9 +123,11 @@ def __init__( self._api = coordinator.api gateway_id: str = self._api.gateway_id self._gateway_data = coordinator.data[gateway_id] + self._last_active_schedule: str | None = None self._location = device_id if (location := self.device.get(LOCATION)) is not None: self._location = location + self._previous_action_mode = HVACAction.HEATING.value self._attr_max_temp = min(self.device.get(THERMOSTAT, {}).get(UPPER_BOUND, 35.0), 35.0) self._attr_min_temp = self.device.get(THERMOSTAT, {}).get(LOWER_BOUND, 0.0) From 0c7d200dfd661d3de538f1ee0f0af902798b0bd2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sat, 23 May 2026 12:50:27 +0200 Subject: [PATCH 07/26] Update self._last_active_schedule when turning a schedule off --- custom_components/plugwise/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index 60adf1437..072c5ee8f 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -350,6 +350,7 @@ async def _set_manual_hvac_mode(self, mode: HVACMode, schedule: str | None) -> N self.hvac_mode == HVACMode.OFF and schedule not in (None, "off") ) or (self.hvac_mode == HVACMode.AUTO and schedule is not None): await self._api.set_schedule_state(self._location, STATE_OFF, schedule) + self._last_active_schedule = schedule async def _set_auto_hvac_mode(self, schedule: str) -> None: """Execute relevant api-functions based on the requested auto and present mode.""" From 4d613624c7e69e8ec7b0d86f3dd6383876dcc8c0 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 11:16:51 +0200 Subject: [PATCH 08/26] Rework set_hvac_mode() --- custom_components/plugwise/climate.py | 88 +++++++++++---------------- 1 file changed, 35 insertions(+), 53 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index 072c5ee8f..f9f70d054 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -283,81 +283,63 @@ async def async_set_temperature(self, **kwargs: Any) -> None: await self._api.set_temperature(self._location, data) - def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str | None: - """Return the API regulation value for a manual HVAC mode, or None.""" + def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str: + """Return the API regulation value for a manual HVAC mode.""" if hvac_mode == HVACMode.HEAT: - return HVACAction.HEATING.value + mode = HVACAction.HEATING.value if hvac_mode == HVACMode.COOL: - return HVACAction.COOLING.value - return None + mode = HVACAction.COOLING.value + return mode @plugwise_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC mode (off, heat, cool, heat_cool, or auto/schedule).""" + + # Early exit if no mode change if hvac_mode == self.hvac_mode: return - current_schedule = self.device.get("select_schedule") - # OFF: single API call + # Adam only: set to HVACMode.OFF if hvac_mode == HVACMode.OFF: await self._api.set_regulation_mode(hvac_mode.value) return - # Manual mode (heat/cool/heat_cool) without a schedule: set regulation only - if ( - current_schedule is None - and hvac_mode != HVACMode.AUTO - and ( - regulation := self._regulation_mode_for_hvac(hvac_mode) - or self._previous_action_mode - ) - ): + current_schedule = self.device.get("select_schedule") + schedule_is_active = current_schedule not in (None, "off") + # Adam only: transition from HVACMode.OFF + if self.hvac_mode == HVACMode.OFF: + if hvac_mode == HVACMode.AUTO: + if not schedule_is_active: + if self._last_active_schedule is None: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key=ERROR_NO_SCHEDULE, + ) + await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule) + await self._api.set_regulation_mode(self._previous_action_mode) + return + + # Transition to manual mode + if schedule_is_active: + await self._api.set_schedule_state(self._location, STATE_OFF, current_schedule) + self._last_active_schedule = current_schedule + regulation = self._regulation_mode_for_hvac(hvac_mode) await self._api.set_regulation_mode(regulation) return - # Manual mode: ensure regulation and turn off schedule when needed - if hvac_mode in (HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL): - await self._set_manual_hvac_mode(hvac_mode, current_schedule) + # Common - transition from auto = schedule off + if self.hvac_mode == HVACMode.AUTO: + await self._api.set_schedule_state(self._location, STATE_OFF, current_schedule) + self._last_active_schedule = current_schedule return - # AUTO: restore schedule and regulation - desired_schedule = current_schedule - if desired_schedule and desired_schedule != "off": - self._last_active_schedule = desired_schedule - elif desired_schedule == "off": - desired_schedule = self._last_active_schedule - - if not desired_schedule: + # Common - transition to auto = schedule on + if self._last_active_schedule is None: raise HomeAssistantError( translation_domain=DOMAIN, translation_key=ERROR_NO_SCHEDULE, ) - - await self._set_auto_hvac_mode(desired_schedule) - - async def _set_manual_hvac_mode(self, mode: HVACMode, schedule: str | None) -> None: - """Execute relevant api-functions based on the requested manual and present mode.""" - if ( - regulation := self._regulation_mode_for_hvac(mode) or ( - self._previous_action_mode - if self.hvac_mode in (HVACMode.HEAT_COOL, HVACMode.OFF) - else None - ) - ): - await self._api.set_regulation_mode(regulation) - - if ( - self.hvac_mode == HVACMode.OFF and schedule not in (None, "off") - ) or (self.hvac_mode == HVACMode.AUTO and schedule is not None): - await self._api.set_schedule_state(self._location, STATE_OFF, schedule) - self._last_active_schedule = schedule - - async def _set_auto_hvac_mode(self, schedule: str) -> None: - """Execute relevant api-functions based on the requested auto and present mode.""" - if self._previous_action_mode: - if self.hvac_mode == HVACMode.OFF: - await self._api.set_regulation_mode(self._previous_action_mode) - await self._api.set_schedule_state(self._location, STATE_ON, schedule) + await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule) @plugwise_command async def async_set_preset_mode(self, preset_mode: str) -> None: From ee286afb05974ea477cba0591d98efc9b88bc0ad Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 13:51:22 +0200 Subject: [PATCH 09/26] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4805240b..9508b0b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Versions from 0.40 and up ## Ongoing +- Fix for Core Issue #171955 via PR [#1073](https://github.com/plugwise/plugwise-beta/pull/1073) - Update snapshots to the new format via PR [#1070](https://github.com/plugwise/plugwise-beta/pull/1070) - Add local beta brand icons via PR [#1050](https://github.com/plugwise/plugwise-beta/pull/1050) - Lining up strings with Core Plugwise, update translations via PR [#1048](https://github.com/plugwise/plugwise-beta/pull/1048) From c18fea0e54b4e69947e07fb3429eb7d98cfc184a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 13:52:04 +0200 Subject: [PATCH 10/26] Set to v0.64.2a0 test-version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 016bda58a..8cb98fcf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "plugwise-beta" -version = "0.64.1" +version = "0.64.2a0" description = "Plugwise beta custom-component" readme = "README.md" requires-python = ">=3.14" From 18ac12f7fa0557d9df88f7e23bfef24e1e66a960 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 13:53:50 +0200 Subject: [PATCH 11/26] Correct to a2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8cb98fcf8..0947be4ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "plugwise-beta" -version = "0.64.2a0" +version = "0.64.2a2" description = "Plugwise beta custom-component" readme = "README.md" requires-python = ">=3.14" From 98ed6dce966a197b4fac723968d843d1b3aada9e Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 13:57:46 +0200 Subject: [PATCH 12/26] Fix _regulation_mode_for_hvac() as suggested --- custom_components/plugwise/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index f9f70d054..928df3f29 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -286,10 +286,10 @@ async def async_set_temperature(self, **kwargs: Any) -> None: def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str: """Return the API regulation value for a manual HVAC mode.""" if hvac_mode == HVACMode.HEAT: - mode = HVACAction.HEATING.value + return HVACAction.HEATING.value if hvac_mode == HVACMode.COOL: - mode = HVACAction.COOLING.value - return mode + return HVACAction.COOLING.value + raise ValueError(f"Unsupported HVAC mode for regulation: {hvac_mode}") @plugwise_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: From e6e93646127002a59ce67710a1b03a3630d20338 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 14:01:15 +0200 Subject: [PATCH 13/26] Ruff fixes --- custom_components/plugwise/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index 928df3f29..7ea23441f 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -309,12 +309,12 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # Adam only: transition from HVACMode.OFF if self.hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.AUTO: - if not schedule_is_active: - if self._last_active_schedule is None: + if not schedule_is_active: + if self._last_active_schedule is None: raise HomeAssistantError( translation_domain=DOMAIN, translation_key=ERROR_NO_SCHEDULE, - ) + ) await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule) await self._api.set_regulation_mode(self._previous_action_mode) return From d5423ab108507079495433dcbf72d4c7426a27a5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 14:09:10 +0200 Subject: [PATCH 14/26] Reduce complexity --- custom_components/plugwise/climate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index 7ea23441f..11c7c162c 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -309,13 +309,12 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: # Adam only: transition from HVACMode.OFF if self.hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.AUTO: - if not schedule_is_active: - if self._last_active_schedule is None: - raise HomeAssistantError( - translation_domain=DOMAIN, - translation_key=ERROR_NO_SCHEDULE, - ) - await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule) + if not schedule_is_active and self._last_active_schedule is None: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key=ERROR_NO_SCHEDULE, + ) + await self._api.set_schedule_state(self._location, STATE_ON, self._last_active_schedule) await self._api.set_regulation_mode(self._previous_action_mode) return From 146a0bf7fa1a47dee3d37fb45ec0c498ff94114f Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 24 May 2026 14:27:07 +0200 Subject: [PATCH 15/26] Rename requirements_test_all.txt to requirements_test.txt Align with upstream --- scripts/core-testing.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/core-testing.sh b/scripts/core-testing.sh index cf5f3ab13..93b7d0c1f 100755 --- a/scripts/core-testing.sh +++ b/scripts/core-testing.sh @@ -123,7 +123,7 @@ mkdir -p "${coredir}" if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "core_prep" ] ; then # If only dir exists, but not cloned yet - if [ ! -f "${coredir}/requirements_test_all.txt" ]; then + if [ ! -f "${coredir}/requirements_test.txt" ]; then if [ -d "${manualdir}" ]; then echo "" echo -e "${CINFO} ** Reusing copy, rebasing and copy to HA core**${CNORM}" @@ -143,7 +143,7 @@ if [ -z "${GITHUB_ACTIONS}" ] || [ "$1" == "core_prep" ] ; then git clone https://github.com/home-assistant/core.git "${coredir}" cp -a "${coredir}." "${manualdir}" fi - if [ ! -f "${coredir}/requirements_test_all.txt" ]; then + if [ ! -f "${coredir}/requirements_test.txt" ]; then echo "" echo -e "${CFAIL}Cloning failed .. make sure ${coredir} exists and is an empty directory${CNORM}" echo "" From e468113c92193ef6d5bed26fe1f326cc6a9bee73 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 24 May 2026 14:27:29 +0200 Subject: [PATCH 16/26] Update CACHE_VERSION to 4 in core_next.yml --- .github/workflows/core_next.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/core_next.yml b/.github/workflows/core_next.yml index 010cfab1b..d80347019 100644 --- a/.github/workflows/core_next.yml +++ b/.github/workflows/core_next.yml @@ -5,7 +5,7 @@ name: Validate plugwise-beta against HA-core dev env: # Uses a different key/restore key than test.yml - CACHE_VERSION: 3 + CACHE_VERSION: 4 DEFAULT_PYTHON: "3.14" VENV: venv From 7daa269042f7e44eb1c4801599c038797499c6e6 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 24 May 2026 14:27:40 +0200 Subject: [PATCH 17/26] Update CACHE_VERSION to 4 in test workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 578ee9ad6..fb4b35425 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ name: Test PR against HA-core env: - CACHE_VERSION: 3 + CACHE_VERSION: 4 DEFAULT_PYTHON: "3.14" VENV: venv From 4a75cc872e790870c3c663233cd60264c65800d7 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 14:52:49 +0200 Subject: [PATCH 18/26] Remove raise, extend docstring instead --- custom_components/plugwise/climate.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index 11c7c162c..b7403b3d7 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -284,12 +284,15 @@ async def async_set_temperature(self, **kwargs: Any) -> None: await self._api.set_temperature(self._location, data) def _regulation_mode_for_hvac(self, hvac_mode: HVACMode) -> str: - """Return the API regulation value for a manual HVAC mode.""" + """Return the API regulation value for a manual HVAC mode. + + The function inputs are limited to the HVACModes HEAT and COOL. + """ if hvac_mode == HVACMode.HEAT: - return HVACAction.HEATING.value + mode = HVACAction.HEATING.value if hvac_mode == HVACMode.COOL: - return HVACAction.COOLING.value - raise ValueError(f"Unsupported HVAC mode for regulation: {hvac_mode}") + mode = HVACAction.COOLING.value + return mode @plugwise_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: From 40e7142d9c1e9a1a415a820d1f00ee79ddcb5257 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 15:32:11 +0200 Subject: [PATCH 19/26] Add new test fixture --- .../m_adam_heating_off_schedule/data.json | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json diff --git a/tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json b/tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json new file mode 100644 index 000000000..406219f2a --- /dev/null +++ b/tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json @@ -0,0 +1,309 @@ +{ + "056ee145a816487eaa69243c3280f8bf": { + "available": true, + "binary_sensors": { + "dhw_state": false, + "flame_state": false, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "max_dhw_temperature": { + "lower_bound": 40.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 25.0, + "resolution": 0.01, + "setpoint": 50.0, + "upper_bound": 95.0 + }, + "model": "Generic heater", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 0.0, + "water_temperature": 37.0 + }, + "switches": { + "dhw_cm_switch": false + } + }, + "14df5c4dc8cb4ba69f9d1ac0eaf7c5c6": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2025-11-10T01:00:00+01:00", + "hardware": "1", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "Emma Pro", + "model_id": "170-01", + "name": "Emma", + "sensors": { + "battery": 100, + "humidity": 65.0, + "setpoint": 20.0, + "temperature": 19.5 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "60EFABFFFE89CBA0" + }, + "1772a4ea304041adb83f357b751341ff": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "thermostatic_radiator_valve", + "firmware": "2020-11-04T01:00:00+01:00", + "hardware": "1", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Tom", + "model_id": "106-03", + "name": "Tom Badkamer", + "sensors": { + "battery": 60, + "setpoint": 25.0, + "temperature": 18.6, + "temperature_difference": -0.4, + "valve_position": 100.0 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.1, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C8FCBA0" + }, + "ad4838d7d35c4d6ea796ee12ae5aedf8": { + "dev_class": "thermostat", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "ThermoTouch", + "model_id": "143.1", + "name": "Anna", + "sensors": { + "setpoint": 20.0, + "temperature": 19.1 + }, + "vendor": "Plugwise" + }, + "c9293d1d68ee48fc8843c6f0dee2b6be": { + "dev_class": "pumping", + "members": [ + "854f8a9b0e7e425db97f1f110e1ce4b3", + "ad4838d7d35c4d6ea796ee12ae5aedf8" + ], + "model": "Group", + "name": "Vloerverwarming", + "sensors": { + "electricity_consumed": 45.0, + "electricity_produced": 0.0, + "temperature": 20.1 + }, + "vendor": "Plugwise" + }, + "da224107914542988a88561b4452b0f6": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "3.9.0", + "gateway_modes": [ + "away", + "full", + "vacation" + ], + "hardware": "AME Smile 2.0 board", + "location": "bc93488efab249e5bc54fd7e175a6f91", + "mac_address": "D40FB201CBA0", + "model": "Gateway", + "model_id": "smile_open_therm", + "name": "Adam", + "notifications": {}, + "regulation_modes": [ + "bleeding_cold", + "heating", + "off", + "bleeding_hot" + ], + "select_gateway_mode": "full", + "select_regulation_mode": "off", + "sensors": { + "outdoor_temperature": -1.25 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000D5ACBA0" + }, + "da575e9e09b947e281fb6e3ebce3b174": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermometer", + "firmware": "2020-09-01T02:00:00+02:00", + "hardware": "1", + "location": "f2bf9048bef64cc5b6d5110154e33c81", + "model": "Jip", + "model_id": "168-01", + "name": "Jip", + "sensors": { + "battery": 100, + "humidity": 65.8, + "setpoint": 20.0, + "temperature": 19.3 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "70AC08FFFEE1CBA0" + }, + "e2f4322d57924fa090fbbc48b3a140dc": { + "available": true, + "binary_sensors": { + "low_battery": false + }, + "dev_class": "zone_thermostat", + "firmware": "2016-10-10T02:00:00+02:00", + "hardware": "255", + "location": "f871b8c4d63549319221e294e4f88074", + "model": "Lisa", + "model_id": "158-01", + "name": "Lisa Badkamer", + "sensors": { + "battery": 71, + "setpoint": 15.0, + "temperature": 17.9 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "vendor": "Plugwise", + "zigbee_mac_address": "000D6F000C86CBA0" + }, + "e8ef2a01ed3b4139a53bf749204fe6b4": { + "dev_class": "switching", + "members": [ + "2568cc4b9c1e401495d4741a5f89bee1", + "29542b2b6a6a4169acecc15c72a599b8" + ], + "model": "Group", + "name": "Test", + "sensors": { + "electricity_consumed": 16.5, + "electricity_produced": 0.0 + }, + "switches": { + "relay": true + }, + "vendor": "Plugwise" + }, + "f2bf9048bef64cc5b6d5110154e33c81": { + "active_preset": "home", + "available_schedules": [ + "Badkamer", + "Vakantie", + "Weekschema", + "Test", + "off" + ], + "climate_mode": "off", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Living room", + "preset_modes": [ + "vacation", + "no_frost", + "asleep", + "home", + "away" + ], + "select_schedule": "Weekschema", + "select_zone_profile": "active", + "sensors": { + "electricity_consumed": 60.8, + "electricity_produced": 0.0, + "temperature": 19.1 + }, + "thermostat": { + "lower_bound": 1.0, + "resolution": 0.01, + "setpoint": 20.0, + "upper_bound": 35.0 + }, + "thermostats": { + "primary": [ + "ad4838d7d35c4d6ea796ee12ae5aedf8", + "14df5c4dc8cb4ba69f9d1ac0eaf7c5c6", + "da575e9e09b947e281fb6e3ebce3b174" + ], + "secondary": [] + }, + "vendor": "Plugwise", + "zone_profiles": [ + "active", + "off", + "passive" + ] + }, + "f871b8c4d63549319221e294e4f88074": { + "active_preset": "vacation", + "available_schedules": [ + "Badkamer", + "Vakantie", + "Weekschema", + "Test", + "off" + ], + "climate_mode": "off", + "control_state": "idle", + "dev_class": "climate", + "model": "ThermoZone", + "name": "Bathroom", + "preset_modes": [ + "vacation", + "no_frost", + "asleep", + "home", + "away" + ], + "select_schedule": "Badkamer", + "select_zone_profile": "passive", + "sensors": { + "electricity_consumed": 0.0, + "electricity_produced": 0.0, + "temperature": 17.9 + }, + "thermostat": { + "lower_bound": 0.0, + "resolution": 0.01, + "setpoint": 15.0, + "upper_bound": 99.9 + }, + "thermostats": { + "primary": [ + "e2f4322d57924fa090fbbc48b3a140dc" + ], + "secondary": [ + "1772a4ea304041adb83f357b751341ff" + ] + }, + "vendor": "Plugwise", + "zone_profiles": [ + "active", + "off", + "passive" + ] + } +} From 88e66248e57b177960d0630a6ebc604cd2acd544 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 15:39:08 +0200 Subject: [PATCH 20/26] Add test coverage --- tests/components/plugwise/test_climate.py | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index e7c4895d7..1d0672aff 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -269,6 +269,53 @@ async def test_adam_2_climate_snapshot( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +@pytest.mark.parametrize("chosen_env", ["m_adam_heating_off_schedule"], indirect=True) +@pytest.mark.parametrize("cooling_present", [False], indirect=True) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_adam_off_regulation_mode_change( + hass: HomeAssistant, + mock_smile_adam_heat_cool: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test changing from regulation off mode.""" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State("climate.living_room", "heat"), + PlugwiseClimateExtraStoredData( + last_active_schedule=None, + previous_action_mode="heating", + ).as_dict(), + ), + ( + State("climate.bathroom", "heat"), + PlugwiseClimateExtraStoredData( + last_active_schedule="Badkamer", + previous_action_mode="heating", + ).as_dict(), + ), + ], + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert (state := hass.states.get("climate.living_room")) + assert state.state == "off" + + # Verify a HomeAssistantError is raised setting a schedule with last_active_schedule = None + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVACMode.AUTO}, + blocking=True, + ) + + @pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True) @pytest.mark.parametrize("cooling_present", [True], indirect=True) async def test_adam_3_climate_entity_attributes( From d9fcc42e152410e7016577f57bb4c130bd81e495 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 15:44:41 +0200 Subject: [PATCH 21/26] Correct fixture contents --- .../plugwise/fixtures/m_adam_heating_off_schedule/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json b/tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json index 406219f2a..3aeb08f30 100644 --- a/tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json +++ b/tests/components/plugwise/fixtures/m_adam_heating_off_schedule/data.json @@ -229,7 +229,7 @@ "home", "away" ], - "select_schedule": "Weekschema", + "select_schedule": "off", "select_zone_profile": "active", "sensors": { "electricity_consumed": 60.8, From dce133b67cce8a2572412d08abff3b74b219fc89 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 15:52:04 +0200 Subject: [PATCH 22/26] Add more test coverage --- tests/components/plugwise/test_climate.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 1d0672aff..d0a57caf3 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -306,7 +306,7 @@ async def test_adam_off_regulation_mode_change( assert (state := hass.states.get("climate.living_room")) assert state.state == "off" - # Verify a HomeAssistantError is raised setting a schedule with last_active_schedule = None + # Verify a HomeAssistantError is raised setting a schedule from regulation-off-mode with last_active_schedule = None with pytest.raises(HomeAssistantError): await hass.services.async_call( CLIMATE_DOMAIN, @@ -315,6 +315,17 @@ async def test_adam_off_regulation_mode_change( blocking=True, ) + # Verify that the active schedule is turned off when transitioning from regulation-off-mode to a manual mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.bathroom", ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_smile_adam_heat_cool.set_schedule_state.assert_called_with( + "f871b8c4d63549319221e294e4f88074", STATE_OFF, "Badkamer" + ) + @pytest.mark.parametrize("chosen_env", ["m_adam_cooling"], indirect=True) @pytest.mark.parametrize("cooling_present", [True], indirect=True) From 46da1808010ee2f376605d08788b5f591daa3297 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 19:47:05 +0200 Subject: [PATCH 23/26] Sorting --- custom_components/plugwise/climate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index b7403b3d7..d0f2246ab 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -154,11 +154,6 @@ def __init__( self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_preset_modes = presets - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE) - async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" @@ -179,6 +174,11 @@ def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData: self._previous_action_mode, ) + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.device.get(SENSORS, {}).get(ATTR_TEMPERATURE) + @property def target_temperature(self) -> float | None: """Return the temperature we try to reach. From 4c466d9216b901a7b53839211e299cc6cacf9642 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 19:48:00 +0200 Subject: [PATCH 24/26] Set to v0.64.2 release-version --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9508b0b3b..13a2c2687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Versions from 0.40 and up -## Ongoing +## v0.64.2 - Fix for Core Issue #171955 via PR [#1073](https://github.com/plugwise/plugwise-beta/pull/1073) - Update snapshots to the new format via PR [#1070](https://github.com/plugwise/plugwise-beta/pull/1070) diff --git a/pyproject.toml b/pyproject.toml index 0947be4ab..d80585453 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "plugwise-beta" -version = "0.64.2a2" +version = "0.64.2" description = "Plugwise beta custom-component" readme = "README.md" requires-python = ">=3.14" From 2d55cdbad0845936cbf0cb16027d975a8f8d6f26 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 19:49:16 +0200 Subject: [PATCH 25/26] CHANGELOG - link to Core Issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a2c2687..7066d4c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Versions from 0.40 and up ## v0.64.2 -- Fix for Core Issue #171955 via PR [#1073](https://github.com/plugwise/plugwise-beta/pull/1073) +- Fix for Core Issue [#171955](https://github.com/home-assistant/core/issues/171955) via PR [#1073](https://github.com/plugwise/plugwise-beta/pull/1073) - Update snapshots to the new format via PR [#1070](https://github.com/plugwise/plugwise-beta/pull/1070) - Add local beta brand icons via PR [#1050](https://github.com/plugwise/plugwise-beta/pull/1050) - Lining up strings with Core Plugwise, update translations via PR [#1048](https://github.com/plugwise/plugwise-beta/pull/1048) From 4d54ca054ed064564751f158d63af64fb3798ae2 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 24 May 2026 20:14:57 +0200 Subject: [PATCH 26/26] Implement suggested improvement --- custom_components/plugwise/climate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/plugwise/climate.py b/custom_components/plugwise/climate.py index d0f2246ab..ce9104c51 100644 --- a/custom_components/plugwise/climate.py +++ b/custom_components/plugwise/climate.py @@ -159,9 +159,10 @@ async def async_added_to_hass(self) -> None: extra_data = await self.async_get_last_extra_data() if extra_data is not None: - self._last_active_schedule = extra_data.as_dict()["last_active_schedule"] + data = extra_data.as_dict() + self._last_active_schedule = data.get("last_active_schedule") self._previous_action_mode = ( - extra_data.as_dict()["previous_action_mode"] or HVACAction.HEATING.value + data.get("previous_action_mode") or HVACAction.HEATING.value ) await super().async_added_to_hass()