From 30f3a42dc90601ec2bea2232ba1485058ceacd65 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 11:31:44 +0200 Subject: [PATCH 01/24] Implement water_heater platform for DHW function --- custom_components/plugwise/water_heater.py | 96 ++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 custom_components/plugwise/water_heater.py diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py new file mode 100644 index 000000000..ec95fee3c --- /dev/null +++ b/custom_components/plugwise/water_heater.py @@ -0,0 +1,96 @@ +"""Plugwise water heater component for HomeAssistant.""" + +from homeassistant.components.water_heater import ( + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ( + ATTR_NAME, + STATE_OFF, + STATE_ON, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import ( + BINARY_SENSORS, + DEV_CLASS, + LOGGER, + LOWER_BOUND, + SENSORS, + TARGET_TEMP, + UPPER_BOUND, +) +from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity + +MODE_HEAT = "heat" +MODE_OFF = "off" +OPERATION_MODES = [MODE_HEAT, MODE_OFF] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PlugwiseConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Plugwise water_heater from a config entry.""" + coordinator = entry.runtime_data + + @callback + def _add_entities() -> None: + """Add Entities.""" + if not coordinator.new_devices: + return + + for device_id in coordinator.new_devices: + device = coordinator.data[device_id] + if device[DEV_CLASS] == "heater_central" and device.get("max_dhw_temperature"): + async_add_entities([PlugwiseWaterHeaterEntity(coordinator, device_id)]) + LOGGER.debug("Add %s water_heater", device[ATTR_NAME]) + + _add_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_entities)) + + +class PlugwiseWaterHeaterEntity(PlugwiseEntity, WaterHeaterEntity): + """Representation of a Plugwise water heater.""" + + _attr_name = None + _attr_operation_list = OPERATION_MODES + _attr_temperature_unit = UnitOfTemperature.CELSIUS + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + ) -> None: + """Initialise the water_heater.""" + super().__init__(coordinator, device_id) + self._attr_unique_id = f"{device_id}-water_heater" + + self._attr_max_temp = self.device.get("max_dhw_temperature", {}).get(UPPER_BOUND, 75.0) + self._attr_min_temp = self.device.get("max_dhw_temperature", {}).get(LOWER_BOUND, 40.0) + self._attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE + self._attr_supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE + + @property + def current_operation(self) -> str | None: + """Return current readable operation mode.""" + if (state := self.device.get(BINARY_SENSORS, {}).get("dhw_state")) is not None: + if state: + return STATE_ON + else: + return STATE_OFF + return None + + @property + def current_temperature(self) -> float | None: + """Return the current water temperature.""" + return self.device.get(SENSORS, {}).get("water_temperature") + + @property + def target_temperature(self) -> float | None: + """Return the water temperature we try to reach.""" + return self.device.get("max_dhw_temperature", {}).get(TARGET_TEMP) From c55595d1d4757cf88cdc7c6250b45b550c7bca1a Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 11:50:31 +0200 Subject: [PATCH 02/24] Ruffed --- custom_components/plugwise/water_heater.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index ec95fee3c..be158b257 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -4,12 +4,7 @@ WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.const import ( - ATTR_NAME, - STATE_OFF, - STATE_ON, - UnitOfTemperature, -) +from homeassistant.const import ATTR_NAME, STATE_OFF, STATE_ON, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -81,8 +76,7 @@ def current_operation(self) -> str | None: if (state := self.device.get(BINARY_SENSORS, {}).get("dhw_state")) is not None: if state: return STATE_ON - else: - return STATE_OFF + return STATE_OFF return None @property From 3c4fae78d1d226e0c8a5907796ac482cecc81958 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 12:18:15 +0200 Subject: [PATCH 03/24] Add test_water_heater.py --- .../components/plugwise/test_water_heater.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/components/plugwise/test_water_heater.py diff --git a/tests/components/plugwise/test_water_heater.py b/tests/components/plugwise/test_water_heater.py new file mode 100644 index 000000000..536e4a363 --- /dev/null +++ b/tests/components/plugwise/test_water_heater.py @@ -0,0 +1,29 @@ +"""Tests for the Plugwise water_heater platform.""" + +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from syrupy.assertion import SnapshotAssertion + +from tests.common import MockConfigEntry, snapshot_platform + +HA_PLUGWISE_SMILE_ASYNC_UPDATE = ( + "homeassistant.components.plugwise.coordinator.Smile.async_update" +) + + +@pytest.mark.parametrize("platforms", [(WATER_HEATER_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_adam_water_heater_snapshot( + hass: HomeAssistant, + mock_smile_adam: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, +) -> None: + """Test Adam water_heater snapshot.""" + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) From c033fa0f61a91f7fa5ba2cdca30e43b4a8ad8f00 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 12:20:31 +0200 Subject: [PATCH 04/24] Improve water_heater and supported_features detection --- custom_components/plugwise/water_heater.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index be158b257..c462202fa 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -41,7 +41,7 @@ def _add_entities() -> None: for device_id in coordinator.new_devices: device = coordinator.data[device_id] - if device[DEV_CLASS] == "heater_central" and device.get("max_dhw_temperature"): + if device[DEV_CLASS] == "heater_central" and device.get(BINARY_SENSORS, {}).get("dhw_state"): async_add_entities([PlugwiseWaterHeaterEntity(coordinator, device_id)]) LOGGER.debug("Add %s water_heater", device[ATTR_NAME]) @@ -68,7 +68,8 @@ def __init__( self._attr_max_temp = self.device.get("max_dhw_temperature", {}).get(UPPER_BOUND, 75.0) self._attr_min_temp = self.device.get("max_dhw_temperature", {}).get(LOWER_BOUND, 40.0) self._attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE - self._attr_supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE + if self.device.get("max_dhw_temperature"): + self._attr_supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE @property def current_operation(self) -> str | None: From 015898aa74796620eb71c3f60d4578b865cceaff Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 12:26:03 +0200 Subject: [PATCH 05/24] Correct mocked_adam to fixture with water_heater --- custom_components/plugwise/const.py | 1 + tests/components/plugwise/test_water_heater.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/plugwise/const.py b/custom_components/plugwise/const.py index d4e25a26f..611138431 100644 --- a/custom_components/plugwise/const.py +++ b/custom_components/plugwise/const.py @@ -165,6 +165,7 @@ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.WATER_HEATER, ] SERVICE_DELETE: Final = "delete_notification" SEVERITIES: Final[list[str]] = ["other", "info", "message", "warning", "error"] diff --git a/tests/components/plugwise/test_water_heater.py b/tests/components/plugwise/test_water_heater.py index 536e4a363..35ec3a268 100644 --- a/tests/components/plugwise/test_water_heater.py +++ b/tests/components/plugwise/test_water_heater.py @@ -20,7 +20,7 @@ @pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_adam_water_heater_snapshot( hass: HomeAssistant, - mock_smile_adam: MagicMock, + mock_smile_adam_jip: MagicMock, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, setup_platform: MockConfigEntry, From b63fa99a1962b3e4fbba0f8f0e8bc1c9d5f74bed Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 12:53:13 +0200 Subject: [PATCH 06/24] Fixes --- custom_components/plugwise/water_heater.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index c462202fa..fb0d266f0 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -39,11 +39,13 @@ def _add_entities() -> None: if not coordinator.new_devices: return + entities: list[PlugwiseWaterHeaterEntity] = [] for device_id in coordinator.new_devices: device = coordinator.data[device_id] - if device[DEV_CLASS] == "heater_central" and device.get(BINARY_SENSORS, {}).get("dhw_state"): - async_add_entities([PlugwiseWaterHeaterEntity(coordinator, device_id)]) + if device[DEV_CLASS] == "heater_central" and device.get(BINARY_SENSORS, {}).get("dhw_state") is not None: + entities.append(PlugwiseWaterHeaterEntity(coordinator, device_id)) LOGGER.debug("Add %s water_heater", device[ATTR_NAME]) + async_add_entities(entities) _add_entities() entry.async_on_unload(coordinator.async_add_listener(_add_entities)) From 57fb1444c232a962dbbef9160fa43fe9d69ca370 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 13:01:24 +0200 Subject: [PATCH 07/24] Save new test_water_heater snapshot --- .../plugwise/snapshots/test_water_heater.ambr | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/components/plugwise/snapshots/test_water_heater.ambr diff --git a/tests/components/plugwise/snapshots/test_water_heater.ambr b/tests/components/plugwise/snapshots/test_water_heater.ambr new file mode 100644 index 000000000..6851c43da --- /dev/null +++ b/tests/components/plugwise/snapshots/test_water_heater.ambr @@ -0,0 +1,70 @@ +# serializer version: 1 +# name: test_adam_water_heater_snapshot[platforms0][water_heater.opentherm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 60.0, + 'min_temp': 40.0, + 'operation_list': list([ + 'heat', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.opentherm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'e4684553153b44afbef2200885f379dc-water_heater', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_water_heater_snapshot[platforms0][water_heater.opentherm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 37.3, + 'friendly_name': 'OpenTherm', + 'max_temp': 60.0, + 'min_temp': 40.0, + 'operation_list': list([ + 'heat', + 'off', + ]), + 'operation_mode': 'off', + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 60.0, + }), + 'context': , + 'entity_id': 'water_heater.opentherm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- From deb9b166b9477b445cd70f33a6987d339e446372 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 13:09:29 +0200 Subject: [PATCH 08/24] Add anna_v4_dhw fixture --- .../plugwise/fixtures/anna_v4_dhw/data.json | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/components/plugwise/fixtures/anna_v4_dhw/data.json diff --git a/tests/components/plugwise/fixtures/anna_v4_dhw/data.json b/tests/components/plugwise/fixtures/anna_v4_dhw/data.json new file mode 100644 index 000000000..677d0a622 --- /dev/null +++ b/tests/components/plugwise/fixtures/anna_v4_dhw/data.json @@ -0,0 +1,98 @@ +{ + "01b85360fdd243d0aaad4d6ac2a5ba7e": { + "active_preset": "home", + "available_schedules": [ + "Standaard", + "Thuiswerken", + "off" + ], + "climate_mode": "heat", + "control_state": "idle", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", + "location": "eb5309212bf5407bb143e5bfa3b18aee", + "model": "ThermoTouch", + "name": "Anna", + "preset_modes": [ + "vacation", + "no_frost", + "away", + "asleep", + "home" + ], + "select_schedule": "off", + "sensors": { + "illuminance": 60.0, + "setpoint": 20.5, + "temperature": 20.6 + }, + "temperature_offset": { + "lower_bound": -2.0, + "resolution": 0.1, + "setpoint": 0.0, + "upper_bound": 2.0 + }, + "thermostat": { + "lower_bound": 4.0, + "resolution": 0.1, + "setpoint": 20.5, + "upper_bound": 30.0 + }, + "vendor": "Plugwise" + }, + "0466eae8520144c78afb29628384edeb": { + "binary_sensors": { + "plugwise_notification": false + }, + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", + "location": "94c107dc6ac84ed98e9f68c0dd06bf71", + "mac_address": "012345670001", + "model": "Gateway", + "model_id": "smile_thermo", + "name": "Smile Anna", + "notifications": {}, + "sensors": { + "outdoor_temperature": 7.44 + }, + "vendor": "Plugwise" + }, + "cd0e6156b1f04d5f952349ffbe397481": { + "available": true, + "binary_sensors": { + "dhw_state": true, + "flame_state": true, + "heating_state": false + }, + "dev_class": "heater_central", + "location": "94c107dc6ac84ed98e9f68c0dd06bf71", + "max_dhw_temperature": { + "lower_bound": 30.0, + "resolution": 0.01, + "setpoint": 60.0, + "upper_bound": 60.0 + }, + "maximum_boiler_temperature": { + "lower_bound": 0.0, + "resolution": 1.0, + "setpoint": 70.0, + "upper_bound": 100.0 + }, + "model": "Generic heater", + "model_id": "2.32", + "name": "OpenTherm", + "sensors": { + "intended_boiler_temperature": 39.9, + "modulation_level": 0.0, + "return_temperature": 32.0, + "water_pressure": 2.2, + "water_temperature": 45.0 + }, + "switches": { + "dhw_cm_switch": false + }, + "vendor": "Bosch Thermotechniek B.V." + } +} From 7c64edc0ec09946d59bb2c97de7e069301a585c5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 13:12:30 +0200 Subject: [PATCH 09/24] Add 2nd water_heater testcase --- tests/components/plugwise/test_water_heater.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/components/plugwise/test_water_heater.py b/tests/components/plugwise/test_water_heater.py index 35ec3a268..7a9d3971b 100644 --- a/tests/components/plugwise/test_water_heater.py +++ b/tests/components/plugwise/test_water_heater.py @@ -25,5 +25,20 @@ async def test_adam_water_heater_snapshot( entity_registry: er.EntityRegistry, setup_platform: MockConfigEntry, ) -> None: - """Test Adam water_heater snapshot.""" + """Test Adam water_heater snapshot with dhw_state off.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("chosen_env", ["anna_v4_dhw"], indirect=True) +@pytest.mark.parametrize("cooling_present", [False], indirect=True) +@pytest.mark.parametrize("platforms", [(WATER_HEATER_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_anna_water_heater_snapshot( + hass: HomeAssistant, + mock_smile_anna: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, +) -> None: + """Test Anna water_heater snapshot with dhw_state on.""" + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) \ No newline at end of file From c680b3630ac00e36e9164927a1f26aa472bea930 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 13:16:12 +0200 Subject: [PATCH 10/24] Save updates: snapshot, ruffed --- .../plugwise/snapshots/test_water_heater.ambr | 69 +++++++++++++++++++ .../components/plugwise/test_water_heater.py | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/tests/components/plugwise/snapshots/test_water_heater.ambr b/tests/components/plugwise/snapshots/test_water_heater.ambr index 6851c43da..d4ce0e05c 100644 --- a/tests/components/plugwise/snapshots/test_water_heater.ambr +++ b/tests/components/plugwise/snapshots/test_water_heater.ambr @@ -68,3 +68,72 @@ 'state': 'off', }) # --- +# name: test_anna_water_heater_snapshot[platforms0-False-anna_v4_dhw][water_heater.opentherm-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 60.0, + 'min_temp': 30.0, + 'operation_list': list([ + 'heat', + 'off', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.opentherm', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'cd0e6156b1f04d5f952349ffbe397481-water_heater', + 'unit_of_measurement': None, + }) +# --- +# name: test_anna_water_heater_snapshot[platforms0-False-anna_v4_dhw][water_heater.opentherm-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 45.0, + 'friendly_name': 'OpenTherm', + 'max_temp': 60.0, + 'min_temp': 30.0, + 'operation_list': list([ + 'heat', + 'off', + ]), + 'operation_mode': 'on', + 'supported_features': , + 'target_temp_high': None, + 'target_temp_low': None, + 'temperature': 60.0, + }), + 'context': , + 'entity_id': 'water_heater.opentherm', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/plugwise/test_water_heater.py b/tests/components/plugwise/test_water_heater.py index 7a9d3971b..0a9f404e3 100644 --- a/tests/components/plugwise/test_water_heater.py +++ b/tests/components/plugwise/test_water_heater.py @@ -41,4 +41,4 @@ async def test_anna_water_heater_snapshot( setup_platform: MockConfigEntry, ) -> None: """Test Anna water_heater snapshot with dhw_state on.""" - await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) \ No newline at end of file + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) From fbbe9063ddb7f06b49c0862b96d26fa26f0bc2ed Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 13:20:05 +0200 Subject: [PATCH 11/24] Update related test asserts --- tests/components/plugwise/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index bd5dbf89f..078cd6319 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -287,7 +287,7 @@ async def test_update_device( assert ( len(er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)) - == 53 + == 54 ) assert ( len(dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id)) @@ -311,7 +311,7 @@ async def test_update_device( assert ( len(er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)) - == 60 + == 61 ) assert ( len(dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id)) @@ -338,7 +338,7 @@ async def test_update_device( assert ( len(er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)) - == 53 + == 54 ) assert ( len(dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id)) From a35f004035d2eefd057340c29e2b5c5747da1e3d Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 13:42:50 +0200 Subject: [PATCH 12/24] Add water_heater async_set_temperature() --- custom_components/plugwise/water_heater.py | 26 +++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index fb0d266f0..a1a4b9170 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -4,7 +4,13 @@ WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.const import ATTR_NAME, STATE_OFF, STATE_ON, UnitOfTemperature +from homeassistant.const import ( + ATTR_NAME, + ATTR_TEMPERATURE, + STATE_OFF, + STATE_ON, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -13,12 +19,14 @@ DEV_CLASS, LOGGER, LOWER_BOUND, + MAX_DHW_TEMP, SENSORS, TARGET_TEMP, UPPER_BOUND, ) from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity +from .util import plugwise_command MODE_HEAT = "heat" MODE_OFF = "off" @@ -65,13 +73,17 @@ def __init__( ) -> None: """Initialise the water_heater.""" super().__init__(coordinator, device_id) + self.device_id = device_id self._attr_unique_id = f"{device_id}-water_heater" self._attr_max_temp = self.device.get("max_dhw_temperature", {}).get(UPPER_BOUND, 75.0) self._attr_min_temp = self.device.get("max_dhw_temperature", {}).get(LOWER_BOUND, 40.0) self._attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE + self._supports_temperature_control = False if self.device.get("max_dhw_temperature"): self._attr_supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE + self._supports_temperature_control = True + @property def current_operation(self) -> str | None: @@ -91,3 +103,15 @@ def current_temperature(self) -> float | None: def target_temperature(self) -> float | None: """Return the water temperature we try to reach.""" return self.device.get("max_dhw_temperature", {}).get(TARGET_TEMP) + + @plugwise_command + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if not self._supports_temperature_control or temperature is None: + return + + await self.coordinator.api.set_number(self.device_id, MAX_DHW_TEMP, temperature) + LOGGER.debug( + "Setting %s to %s was successful", MAX_DHW_TEMP, temperature + ) From 3c3ee4832792fcaaa46cf2db62ef8d8616c4ceaa Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 13:55:06 +0200 Subject: [PATCH 13/24] Add test case --- .../components/plugwise/test_water_heater.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/components/plugwise/test_water_heater.py b/tests/components/plugwise/test_water_heater.py index 0a9f404e3..8e5dabee4 100644 --- a/tests/components/plugwise/test_water_heater.py +++ b/tests/components/plugwise/test_water_heater.py @@ -4,7 +4,8 @@ import pytest -from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN +from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN, SERVICE_SET_TEMPERATURE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from syrupy.assertion import SnapshotAssertion @@ -29,6 +30,22 @@ async def test_adam_water_heater_snapshot( await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) +async def test_adam_water_heater_setpoint_change( + hass: HomeAssistant, mock_smile_adam_jip: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test Adam water_heater setpoint-change.""" + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "water_heater.opentherm", ATTR_TEMPERATURE: 65}, + blocking=True, + ) + assert mock_smile_adam_jip.set_number.call_count == 1 + mock_smile_adam_jip.set_number.assert_called_with( + "e4684553153b44afbef2200885f379dc", "max_dhw_temperature", 65.0, + ) + + @pytest.mark.parametrize("chosen_env", ["anna_v4_dhw"], indirect=True) @pytest.mark.parametrize("cooling_present", [False], indirect=True) @pytest.mark.parametrize("platforms", [(WATER_HEATER_DOMAIN,)]) From a0632619ebfb72555da6dd1b114b5bca9b79c320 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:07:50 +0200 Subject: [PATCH 14/24] Add missing import --- custom_components/plugwise/water_heater.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index a1a4b9170..b302477f7 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -1,5 +1,7 @@ """Plugwise water heater component for HomeAssistant.""" +from typing import Any + from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, From 6d2cf0bd599f3c2b465c7d6b529a57242a9e9d38 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:09:24 +0200 Subject: [PATCH 15/24] Ruffed --- tests/components/plugwise/test_water_heater.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/components/plugwise/test_water_heater.py b/tests/components/plugwise/test_water_heater.py index 8e5dabee4..9a5b580c1 100644 --- a/tests/components/plugwise/test_water_heater.py +++ b/tests/components/plugwise/test_water_heater.py @@ -4,7 +4,10 @@ import pytest -from homeassistant.components.water_heater import DOMAIN as WATER_HEATER_DOMAIN, SERVICE_SET_TEMPERATURE +from homeassistant.components.water_heater import ( + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er From a452dc78417fde6ef21ce1569731cd0cb18927a4 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:10:35 +0200 Subject: [PATCH 16/24] Number: remove max_dhw-temperature, handled by water_heater --- custom_components/plugwise/number.py | 8 ------- tests/components/plugwise/test_number.py | 28 ------------------------ 2 files changed, 36 deletions(-) diff --git a/custom_components/plugwise/number.py b/custom_components/plugwise/number.py index cc3f79242..29122649e 100644 --- a/custom_components/plugwise/number.py +++ b/custom_components/plugwise/number.py @@ -16,7 +16,6 @@ LOGGER, LOWER_BOUND, MAX_BOILER_TEMP, - MAX_DHW_TEMP, RESOLUTION, TEMPERATURE_OFFSET, UPPER_BOUND, @@ -47,13 +46,6 @@ class PlugwiseNumberEntityDescription(NumberEntityDescription): entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - PlugwiseNumberEntityDescription( - key=MAX_DHW_TEMP, - translation_key=MAX_DHW_TEMP, - device_class=NumberDeviceClass.TEMPERATURE, - entity_category=EntityCategory.CONFIG, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - ), PlugwiseNumberEntityDescription( key=TEMPERATURE_OFFSET, translation_key=TEMPERATURE_OFFSET, diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py index aaf496d2a..87ea6f1f9 100644 --- a/tests/components/plugwise/test_number.py +++ b/tests/components/plugwise/test_number.py @@ -67,34 +67,6 @@ async def test_adam_temperature_offset_out_of_bounds_change( ) -@pytest.mark.parametrize("chosen_env", ["m_adam_heating"], indirect=True) -@pytest.mark.parametrize("cooling_present", [False], indirect=True) -async def test_adam_dhw_setpoint_change( - hass: HomeAssistant, - mock_smile_adam_heat_cool: MagicMock, - init_integration: MockConfigEntry, -) -> None: - """Test changing of number entities.""" - state = hass.states.get("number.opentherm_domestic_hot_water_setpoint") - assert state - assert float(state.state) == 60.0 - - await hass.services.async_call( - NUMBER_DOMAIN, - SERVICE_SET_VALUE, - { - ATTR_ENTITY_ID: "number.opentherm_domestic_hot_water_setpoint", - ATTR_VALUE: 55, - }, - blocking=True, - ) - - assert mock_smile_adam_heat_cool.set_number.call_count == 1 - mock_smile_adam_heat_cool.set_number.assert_called_with( - "056ee145a816487eaa69243c3280f8bf", "max_dhw_temperature", 55.0 - ) - - @pytest.mark.parametrize("chosen_env", ["anna_heatpump_heating"], indirect=True) @pytest.mark.parametrize("cooling_present", [True], indirect=True) @pytest.mark.parametrize("platforms", [(NUMBER_DOMAIN,)]) From 2afef295b8a48bdad24015451ddf25d6ef04185b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:14:04 +0200 Subject: [PATCH 17/24] Revert assert update after remove double number entity --- tests/components/plugwise/test_init.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 078cd6319..bd5dbf89f 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -287,7 +287,7 @@ async def test_update_device( assert ( len(er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)) - == 54 + == 53 ) assert ( len(dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id)) @@ -311,7 +311,7 @@ async def test_update_device( assert ( len(er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)) - == 61 + == 60 ) assert ( len(dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id)) @@ -338,7 +338,7 @@ async def test_update_device( assert ( len(er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)) - == 54 + == 53 ) assert ( len(dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id)) From 1a061f48da402cd96e7f675bcf32c03d530b4712 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:16:05 +0200 Subject: [PATCH 18/24] Save updated number snapshot --- .../plugwise/snapshots/test_number.ambr | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/tests/components/plugwise/snapshots/test_number.ambr b/tests/components/plugwise/snapshots/test_number.ambr index b033751e2..0026ef47c 100644 --- a/tests/components/plugwise/snapshots/test_number.ambr +++ b/tests/components/plugwise/snapshots/test_number.ambr @@ -609,67 +609,6 @@ 'state': '-0.5', }) # --- -# name: test_anna_number_entities[platforms0-True-anna_heatpump_heating][number.opentherm_domestic_hot_water_setpoint-entry] - EntityRegistryEntrySnapshot({ - 'aliases': list([ - None, - ]), - 'area_id': None, - 'capabilities': dict({ - 'max': 60.0, - 'min': 35.0, - 'mode': , - 'step': 0.5, - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'number', - 'entity_category': , - 'entity_id': 'number.opentherm_domestic_hot_water_setpoint', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'object_id_base': 'Domestic hot water setpoint', - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Domestic hot water setpoint', - 'platform': 'plugwise', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'max_dhw_temperature', - 'unique_id': '1cbf783bb11e4a7c8a6843dee3a86927-max_dhw_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_anna_number_entities[platforms0-True-anna_heatpump_heating][number.opentherm_domestic_hot_water_setpoint-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'temperature', - 'friendly_name': 'OpenTherm Domestic hot water setpoint', - 'max': 60.0, - 'min': 35.0, - 'mode': , - 'step': 0.5, - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'number.opentherm_domestic_hot_water_setpoint', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '53.0', - }) -# --- # name: test_anna_number_entities[platforms0-True-anna_heatpump_heating][number.opentherm_maximum_boiler_temperature_setpoint-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ From 45236bc93a6140ca7f5a251efe016062661e8ba6 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:20:31 +0200 Subject: [PATCH 19/24] hass -> _hass --- custom_components/plugwise/water_heater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index b302477f7..acdfa6fd2 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -36,7 +36,7 @@ async def async_setup_entry( - hass: HomeAssistant, + _hass: HomeAssistant, entry: PlugwiseConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: From 37623676f753a10eab203f4eb9b43912bc471422 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:32:32 +0200 Subject: [PATCH 20/24] Correct current_operation modes, as suggested --- custom_components/plugwise/water_heater.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index acdfa6fd2..bb8736eb4 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -9,8 +9,6 @@ from homeassistant.const import ( ATTR_NAME, ATTR_TEMPERATURE, - STATE_OFF, - STATE_ON, UnitOfTemperature, ) from homeassistant.core import HomeAssistant, callback @@ -92,8 +90,8 @@ def current_operation(self) -> str | None: """Return current readable operation mode.""" if (state := self.device.get(BINARY_SENSORS, {}).get("dhw_state")) is not None: if state: - return STATE_ON - return STATE_OFF + return MODE_HEAT + return MODE_OFF return None @property From 79a4bbcee0bb4d8b7d31f3f6e09b082011429bc5 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:35:18 +0200 Subject: [PATCH 21/24] Re-ruffed --- custom_components/plugwise/water_heater.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index bb8736eb4..22f2c96aa 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -6,11 +6,7 @@ WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.const import ( - ATTR_NAME, - ATTR_TEMPERATURE, - UnitOfTemperature, -) +from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback From fb7d486534eed2a75c080bedf1f75bfd574c5b30 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 14:42:21 +0200 Subject: [PATCH 22/24] Improve, as suggested --- custom_components/plugwise/water_heater.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index 22f2c96aa..20d14a592 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -69,7 +69,6 @@ def __init__( ) -> None: """Initialise the water_heater.""" super().__init__(coordinator, device_id) - self.device_id = device_id self._attr_unique_id = f"{device_id}-water_heater" self._attr_max_temp = self.device.get("max_dhw_temperature", {}).get(UPPER_BOUND, 75.0) @@ -107,7 +106,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: if not self._supports_temperature_control or temperature is None: return - await self.coordinator.api.set_number(self.device_id, MAX_DHW_TEMP, temperature) + await self.coordinator.api.set_number(self._dev_id, MAX_DHW_TEMP, temperature) LOGGER.debug( "Setting %s to %s was successful", MAX_DHW_TEMP, temperature ) From c425ba4946afa2e6a78560b526524251b7f08a79 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 20:21:35 +0200 Subject: [PATCH 23/24] Change modes, add set_operation_mode(), and more --- custom_components/plugwise/water_heater.py | 36 +++++++++++++++++----- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index 20d14a592..f315c10a5 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -6,13 +6,21 @@ WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.const import ( + ATTR_NAME, + ATTR_TEMPERATURE, + STATE_OFF, + STATE_ON, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import ( BINARY_SENSORS, DEV_CLASS, + DHW_CM_SWITCH, + DHW_SETPOINT, LOGGER, LOWER_BOUND, MAX_DHW_TEMP, @@ -24,9 +32,9 @@ from .entity import PlugwiseEntity from .util import plugwise_command -MODE_HEAT = "heat" -MODE_OFF = "off" -OPERATION_MODES = [MODE_HEAT, MODE_OFF] +MODE_DHW_COMFORT = "Dhw comfort" +MODE_DHW_NORMAL = "Dhw normal" +OPERATION_MODES = [MODE_DHW_COMFORT, MODE_DHW_NORMAL] async def async_setup_entry( @@ -85,8 +93,8 @@ def current_operation(self) -> str | None: """Return current readable operation mode.""" if (state := self.device.get(BINARY_SENSORS, {}).get("dhw_state")) is not None: if state: - return MODE_HEAT - return MODE_OFF + return MODE_DHW_COMFORT + return MODE_DHW_NORMAL return None @property @@ -97,7 +105,21 @@ def current_temperature(self) -> float | None: @property def target_temperature(self) -> float | None: """Return the water temperature we try to reach.""" - return self.device.get("max_dhw_temperature", {}).get(TARGET_TEMP) + return ( + self.device.get("max_dhw_temperature", {}).get(TARGET_TEMP) + or self.device.get(SENSORS, {}).get(DHW_SETPOINT) + ) + + @plugwise_command + async def async_set_operation_mode(self, mode: str) -> None: + """Set the operation mode.""" + state = STATE_ON if mode == MODE_DHW_COMFORT else STATE_OFF + await self.coordinator.api.set_switch_state( + self._dev_id, + None, + DHW_CM_SWITCH, + state, + ) @plugwise_command async def async_set_temperature(self, **kwargs: Any) -> None: From b64f79b4dd36c946b3a27e3f3d4cff53edb23114 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk Date: Sun, 7 Jun 2026 20:34:37 +0200 Subject: [PATCH 24/24] Correct W0237 --- custom_components/plugwise/water_heater.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/plugwise/water_heater.py b/custom_components/plugwise/water_heater.py index f315c10a5..dbdd26810 100644 --- a/custom_components/plugwise/water_heater.py +++ b/custom_components/plugwise/water_heater.py @@ -111,9 +111,9 @@ def target_temperature(self) -> float | None: ) @plugwise_command - async def async_set_operation_mode(self, mode: str) -> None: + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set the operation mode.""" - state = STATE_ON if mode == MODE_DHW_COMFORT else STATE_OFF + state = STATE_ON if operation_mode == MODE_DHW_COMFORT else STATE_OFF await self.coordinator.api.set_switch_state( self._dev_id, None,