Skip to content
Draft
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
df62109
Link to updated backend library
bouwew Jun 21, 2026
8ee46a9
Refresh test-fixtures
bouwew Jun 21, 2026
50158c3
Rework water_heater to support the updated backend library
bouwew Jun 21, 2026
4f348c9
Update constants and deploy
bouwew Jun 21, 2026
8e99519
Updates
bouwew Jun 21, 2026
2443e9b
Update water_heater testcase
bouwew Jun 21, 2026
05d1b5d
Fix len asserts in test_update_device()
bouwew Jun 21, 2026
2221ea4
Update translations
bouwew Jun 21, 2026
2f2ae78
Save updated snapshot files
bouwew Jun 21, 2026
65e1cf3
Ruffed
bouwew Jun 21, 2026
f123d46
More updates
bouwew Jun 21, 2026
b7be49e
Remove boiler_temperature as number, moved to water_heater
bouwew Jun 22, 2026
97ca5b7
Water_heater updates
bouwew Jun 22, 2026
e858cef
More fixture updates
bouwew Jun 22, 2026
ddf2097
More water_heater updates
bouwew Jun 22, 2026
01baa7d
Update water_heater unique_id
bouwew Jun 22, 2026
6be6a06
Update translation strings
bouwew Jun 22, 2026
5788f72
Updates
bouwew Jun 22, 2026
6227c92
Water_heater test updates
bouwew Jun 22, 2026
65e3be3
Test-number: remove test case, number removed
bouwew Jun 22, 2026
a7bf7c4
Fix code
bouwew Jun 22, 2026
fd28f4c
Save updated snapshot files
bouwew Jun 23, 2026
7e8bbb2
Clean up
bouwew Jun 23, 2026
77ec601
Correct translation files
bouwew Jun 23, 2026
1b200b9
Fix requirements format
bouwew Jun 23, 2026
d8dde94
Implement suggested fixes
bouwew Jun 23, 2026
2aff0ac
Update dhw-select translations
bouwew Jun 25, 2026
34bf067
Add water_heater state translation string
bouwew Jun 25, 2026
ba83adc
Save upsated test-snapshot
bouwew Jun 25, 2026
cd937d8
Correct water_heater translation string
bouwew Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions custom_components/plugwise/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,12 @@
VENDOR: Final = "vendor"

# Number constants
MAX_BOILER_TEMP: Final = "maximum_boiler_temperature"
MAX_DHW_TEMP: Final = "max_dhw_temperature"
LOWER_BOUND: Final = "lower_bound"
RESOLUTION: Final = "resolution"
TEMPERATURE_OFFSET: Final = "temperature_offset"
UPPER_BOUND: Final = "upper_bound"

# Sensor constants
DHW_TEMP: Final = "dhw_temperature"
DHW_SETPOINT: Final = "domestic_hot_water_setpoint"
EL_CONSUMED: Final = "electricity_consumed"
EL_CONS_INTERVAL: Final = "electricity_consumed_interval"
Expand Down Expand Up @@ -150,6 +147,10 @@
COOLING_ENA_SWITCH: Final ="cooling_ena_switch"
SWITCHES: Final = "switches"

# Water_heater constants
BOILER_TEMP: Final = "boiler_temperature"
DHW_TEMP: Final = "dhw_temperature"

# Default directives
DEFAULT_PORT: Final[int] = 80
DEFAULT_TIMEOUT: Final[int] = 30
Expand Down Expand Up @@ -188,11 +189,8 @@
}

type NumberType = Literal[
"maximum_boiler_temperature",
"max_dhw_temperature",
"temperature_offset",
]

type SelectType = Literal[
"select_dhw_mode",
"select_gateway_mode",
Expand All @@ -207,3 +205,11 @@
"regulation_modes",
"zone_profiles",
]
type WaterHeaterType = Literal[
"boiler_temperature",
"dhw_temperature",
]
type WaterHeaterOptionsType = Literal[
"boiler_modes",
"dhw_modes",
]
4 changes: 3 additions & 1 deletion custom_components/plugwise/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["plugwise"],
"requirements": ["plugwise==1.12.0"],
"requirements": [
"plugwise@git+https://github.com/plugwise/python-plugwise.git@water_heater_2"
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"version": "0.65.0",
"zeroconf": ["_plugwise._tcp.local."]
}
10 changes: 0 additions & 10 deletions custom_components/plugwise/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from .const import (
LOGGER,
LOWER_BOUND,
MAX_BOILER_TEMP,
RESOLUTION,
TEMPERATURE_OFFSET,
UPPER_BOUND,
Expand All @@ -39,13 +38,6 @@ class PlugwiseNumberEntityDescription(NumberEntityDescription):

# Upstream + is there a reason we didn't rename this one prefixed?
NUMBER_TYPES = (
PlugwiseNumberEntityDescription(
key=MAX_BOILER_TEMP,
translation_key=MAX_BOILER_TEMP,
device_class=NumberDeviceClass.TEMPERATURE,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
PlugwiseNumberEntityDescription(
key=TEMPERATURE_OFFSET,
translation_key=TEMPERATURE_OFFSET,
Expand Down Expand Up @@ -120,8 +112,6 @@ def __init__(
self._attr_native_min_value = ctrl.get(LOWER_BOUND, 0.0) # Upstream const

native_step = ctrl.get(RESOLUTION, 0.5) # Upstream const
if description.key != TEMPERATURE_OFFSET: # Upstream const
native_step = max(native_step, 0.5)
self._attr_native_step = native_step

@property
Expand Down
19 changes: 5 additions & 14 deletions custom_components/plugwise/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,6 @@
}
},
"number": {
"max_dhw_temperature": {
"name": "Domestic hot water setpoint"
},
"maximum_boiler_temperature": {
"name": "Maximum boiler temperature setpoint"
},
"temperature_offset": {
"name": "Temperature offset"
}
Expand Down Expand Up @@ -299,14 +293,11 @@
}
},
"water_heater": {
"plugwise": {
"state": {
"auto": "Auto",
"boost": "Boost",
"comfort": "Comfort",
"eco": "Eco",
"off": "Off"
}
"boiler_temperature": {
"name": "Boiler temperature"
},
"dhw_temperature": {
"name": "DHW temperature"
}
}
},
Expand Down
19 changes: 5 additions & 14 deletions custom_components/plugwise/translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,6 @@
}
},
"number": {
"max_dhw_temperature": {
"name": "Instelpunt sanitair warm water"
},
"maximum_boiler_temperature": {
"name": "Instelpunt maximale boiler temperatuur"
},
"temperature_offset": {
"name": "Temperatuurcompensatie"
}
Expand Down Expand Up @@ -299,14 +293,11 @@
}
},
"water_heater": {
"plugwise": {
"state": {
"auto": "Auto",
"boost": "Boost",
"comfort": "Comfort",
"eco": "Eco",
"off": "Off"
}
"boiler_temperature": {
"name": "Boiler temperatuur"
},
"dhw_temperature": {
"name": "SWW temperatuur"
}
}
},
Expand Down
107 changes: 73 additions & 34 deletions custom_components/plugwise/water_heater.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
"""Plugwise water heater component for HomeAssistant."""

from typing import Any
from dataclasses import dataclass
from typing import Any, cast

from homeassistant.components.water_heater import (
WaterHeaterEntity,
WaterHeaterEntityDescription,
WaterHeaterEntityFeature,
)
from homeassistant.const import (
ATTR_NAME,
ATTR_TEMPERATURE,
STATE_OFF,
STATE_ON,
EntityCategory,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import (
BOILER_TEMP,
DHW_MODE,
DHW_MODES,
DHW_SETPOINT,
DHW_TEMP,
LOGGER,
LOWER_BOUND,
MAX_DHW_TEMP,
SENSORS,
TARGET_TEMP,
UPPER_BOUND,
WATER_TEMP,
WaterHeaterOptionsType,
WaterHeaterType,
)
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
from .entity import PlugwiseEntity
from .util import plugwise_command


@dataclass(frozen=True, kw_only=True)
class PlugwiseWaterHeaterEntityDescription(WaterHeaterEntityDescription):
"""Class describing Plugwise WaterHeater entities."""

key: WaterHeaterType
options_key: WaterHeaterOptionsType | None

# Upstream + is there a reason we didn't rename this one prefixed?
WATERHEATER_TYPES = (
PlugwiseWaterHeaterEntityDescription(
key=BOILER_TEMP,
translation_key=BOILER_TEMP,
entity_category=EntityCategory.CONFIG,
options_key=None,
),
PlugwiseWaterHeaterEntityDescription(
key=DHW_TEMP,
translation_key=DHW_TEMP,
entity_category=EntityCategory.CONFIG,
options_key=DHW_MODES,
),
)

async def async_setup_entry(
_hass: HomeAssistant,
entry: PlugwiseConfigEntry,
Expand All @@ -50,9 +73,15 @@ def _add_entities() -> None:
entities: list[PlugwiseWaterHeaterEntity] = []
for device_id in coordinator.new_devices:
device = coordinator.data[device_id]
if device.get(MAX_DHW_TEMP) is not None:
entities.append(PlugwiseWaterHeaterEntity(coordinator, device_id))
LOGGER.debug("Add %s water_heater", device[ATTR_NAME])
for description in WATERHEATER_TYPES:
if description.key in device:
entities.append(
PlugwiseWaterHeaterEntity(coordinator, device_id, description)
)
LOGGER.debug(
"Add %s %s water_heater", device["name"], description.translation_key
)

async_add_entities(entities)

_add_entities()
Expand All @@ -62,61 +91,71 @@ def _add_entities() -> None:
class PlugwiseWaterHeaterEntity(PlugwiseEntity, WaterHeaterEntity):
"""Representation of a Plugwise water heater."""

_attr_name = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
entity_description: PlugwiseWaterHeaterEntityDescription

def __init__(
self,
coordinator: PlugwiseDataUpdateCoordinator,
device_id: str,
description: PlugwiseWaterHeaterEntityDescription,
) -> None:
"""Initialise the water_heater."""
super().__init__(coordinator, device_id)
self._attr_unique_id = f"{device_id}-water_heater"

max_dhw_temp_bounds = self.device.get(MAX_DHW_TEMP, {})
if max_dhw_temp_bounds:
self._attr_max_temp = max_dhw_temp_bounds.get(UPPER_BOUND, 75.0)
self._attr_min_temp = max_dhw_temp_bounds.get(LOWER_BOUND, 40.0)
self._attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
self._attr_supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE

self.entity_description = description
temp_data = self.device.get(description.key, {})
if temp_data:
self._attr_max_temp = temp_data.get(UPPER_BOUND, 75.0)
self._attr_min_temp = temp_data.get(LOWER_BOUND, 40.0)
self._attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
if description.options_key is not None:
self._attr_supported_features |= WaterHeaterEntityFeature.OPERATION_MODE
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_unique_id = f"{device_id}-{description.key}"

@property
def current_operation(self) -> str | None:
"""Return current readable operation mode."""
if self.entity_description.options_key is None:
return STATE_ON
return self.device.get(DHW_MODE)

@property
def current_temperature(self) -> float | None:
"""Return the current water temperature."""
boiler_temperature = self.device.get(SENSORS, {}).get(WATER_TEMP)
dhw_temperature = self.device.get(SENSORS, {}).get(DHW_TEMP)
return dhw_temperature or boiler_temperature
return self.device.get(self.entity_description.key, {}).get("current")

@property
def operation_list(self) -> list[str]:
"""Return the list of available operation modes."""
if (op_list := self.device.get(DHW_MODES, [])):
return op_list
return [STATE_OFF] # pragma: no cover
"""Return the list of available operation modes.

When no list is available the water_heater only has an "on" mode.
"""
if self.entity_description.options_key is not None:
op_list = self.device.get(self.entity_description.options_key)
if op_list is not None:
return cast(list[str], op_list)

return [STATE_ON]

@property
def target_temperature(self) -> float | None:
"""Return the water temperature we try to reach."""
return (
self.device.get(MAX_DHW_TEMP, {}).get(TARGET_TEMP)
or self.device.get(SENSORS, {}).get(DHW_SETPOINT)
)
return self.device.get(self.entity_description.key, {}).get("setpoint")

@plugwise_command
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set the operation mode."""
if self.entity_description.options_key is None:
return
list_type: int = len(self.operation_list)
await self.coordinator.api.set_dhw_mode(DHW_MODE, self._dev_id, list_type, operation_mode)

@plugwise_command
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
await self.coordinator.api.set_number(self._dev_id, MAX_DHW_TEMP, float(temperature))
await self.coordinator.api.set_number(
self._dev_id,
self.entity_description.key,
float(temperature),
)
20 changes: 10 additions & 10 deletions tests/components/plugwise/fixtures/adam_plus_anna_new/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
"flame_state": true,
"heating_state": true
},
"dev_class": "heater_central",
"dhw_modes": [
"comfort",
"off"
],
"location": "bc93488efab249e5bc54fd7e175a6f91",
"maximum_boiler_temperature": {
"boiler_temperature": {
"current": 43.0,
"lower_bound": 25.0,
"resolution": 0.01,
"setpoint": 50.0,
"upper_bound": 95.0
},
"dev_class": "heater_central",
"dhw_modes": [
"comfort",
"eco"
],
"location": "bc93488efab249e5bc54fd7e175a6f91",
"model": "Generic heater",
"name": "OpenTherm",
"select_dhw_mode": "off",
"select_dhw_mode": "eco",
"sensors": {
"intended_boiler_temperature": 22.5,
"water_temperature": 43.0
"intended_boiler_temperature": 22.5
}
},
"10016900610d4c7481df78c89606ef22": {
Expand Down
Loading
Loading