Add nibe heat pump water heater entities (#79099)

* Add water heater platform

* Enable water heater platform

* No need to set target temp feature

* Split out preset mode

* Switch to parameters from lib

* Drop presets

* Add missing callback to coordinator update

* Drop support for heatpump activity

- Current entity model does not support it

* Handle s series lack of mappings

* Protect for missing operation modes to set
pull/92678/head
Joakim Plate 2023-05-06 11:41:11 +02:00 committed by GitHub
parent e680ec6247
commit f9fe3f4af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 190 additions and 0 deletions

View File

@ -788,6 +788,7 @@ omit =
homeassistant/components/nibe_heatpump/select.py
homeassistant/components/nibe_heatpump/sensor.py
homeassistant/components/nibe_heatpump/switch.py
homeassistant/components/nibe_heatpump/water_heater.py
homeassistant/components/niko_home_control/light.py
homeassistant/components/nilu/air_quality.py
homeassistant/components/nissan_leaf/*

View File

@ -54,6 +54,7 @@ PLATFORMS: list[Platform] = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.WATER_HEATER,
]
COIL_READ_RETRIES = 5

View File

@ -17,4 +17,7 @@ CONF_MODBUS_UNIT = "modbus_unit"
VALUES_MIXING_VALVE_CLOSED_STATE = (30, "CLOSED", "SHUNT CLOSED")
VALUES_PRIORITY_HEATING = (30, "HEAT")
VALUES_PRIORITY_COOLING = (60, "COOLING")
VALUES_PRIORITY_HOT_WATER = (20, "HOT WATER")
VALUES_TEMPORARY_LUX_INACTIVE = "OFF"
VALUES_TEMPORARY_LUX_ONE_TIME_INCREASE = "ONE TIME INCREASE"
VALUES_COOL_WITH_ROOM_SENSOR_OFF = (0, "OFF")

View File

@ -0,0 +1,185 @@
"""The Nibe Heat Pump sensors."""
from __future__ import annotations
from nibe.coil import Coil
from nibe.coil_groups import WATER_HEATER_COILGROUPS, WaterHeaterCoilGroup
from nibe.exceptions import CoilNotFoundException
from homeassistant.components.water_heater import (
STATE_HEAT_PUMP,
STATE_HIGH_DEMAND,
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, LOGGER, Coordinator
from .const import VALUES_TEMPORARY_LUX_INACTIVE, VALUES_TEMPORARY_LUX_ONE_TIME_INCREASE
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up platform."""
coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id]
def water_heaters():
for key, group in WATER_HEATER_COILGROUPS.get(coordinator.series, ()).items():
try:
yield WaterHeater(coordinator, key, group)
except CoilNotFoundException as exception:
LOGGER.debug("Skipping water heater: %r", exception)
async_add_entities(water_heaters())
class WaterHeater(CoordinatorEntity[Coordinator], WaterHeaterEntity):
"""Sensor entity."""
_attr_entity_category = None
_attr_has_entity_name = True
_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
_attr_max_temp = 35.0
_attr_min_temp = 5.0
def __init__(
self,
coordinator: Coordinator,
key: str,
desc: WaterHeaterCoilGroup,
) -> None:
"""Initialize entity."""
super().__init__(
coordinator,
{
desc.hot_water_load,
desc.hot_water_comfort_mode,
*set(desc.start_temperature.values()),
*set(desc.stop_temperature.values()),
desc.active_accessory,
desc.temporary_lux,
},
)
self._attr_entity_registry_enabled_default = desc.active_accessory is None
self._attr_available = False
self._attr_name = desc.name
self._attr_unique_id = f"{coordinator.unique_id}-{key}"
self._attr_device_info = coordinator.device_info
self._attr_current_operation = None
self._attr_target_temperature_high = None
self._attr_target_temperature_low = None
self._attr_operation_list = []
self._operation_mode_to_lux: dict[str, str] = {}
def _get(address: int) -> Coil:
return coordinator.heatpump.get_coil_by_address(address)
def _map(data: dict[str, int]) -> dict[str, Coil]:
return {key: _get(address) for key, address in data.items()}
self._coil_current = _get(desc.hot_water_load)
self._coil_start_temperature = _map(desc.start_temperature)
self._coil_stop_temperature = _map(desc.stop_temperature)
self._coil_temporary_lux: Coil | None = None
if desc.temporary_lux:
self._coil_temporary_lux = _get(desc.temporary_lux)
self._coil_active_accessory: Coil | None = None
if address := desc.active_accessory:
self._coil_active_accessory = _get(address)
self._coil_hot_water_comfort_mode = _get(desc.hot_water_comfort_mode)
def _add_lux_mode(temporary_lux: str, operation_mode: str) -> None:
assert self._attr_operation_list is not None
if (
not self._coil_temporary_lux
or not self._coil_temporary_lux.reverse_mappings
):
return
if temporary_lux not in self._coil_temporary_lux.reverse_mappings:
return
self._attr_operation_list.append(operation_mode)
self._operation_mode_to_lux[operation_mode] = temporary_lux
_add_lux_mode(VALUES_TEMPORARY_LUX_ONE_TIME_INCREASE, STATE_HIGH_DEMAND)
_add_lux_mode(VALUES_TEMPORARY_LUX_INACTIVE, STATE_HEAT_PUMP)
self._attr_temperature_unit = self._coil_current.unit
@callback
def _handle_coordinator_update(self) -> None:
if not self.coordinator.data:
return
def _get_float(coil: Coil | None) -> float | None:
if coil is None:
return None
return self.coordinator.get_coil_float(coil)
def _get_value(coil: Coil | None) -> int | str | float | None:
if coil is None:
return None
return self.coordinator.get_coil_value(coil)
self._attr_current_temperature = _get_float(self._coil_current)
if (mode := _get_value(self._coil_hot_water_comfort_mode)) and isinstance(
mode, str
):
self._attr_target_temperature_low = _get_float(
self._coil_start_temperature.get(mode)
)
self._attr_target_temperature_high = _get_float(
self._coil_stop_temperature.get(mode)
)
else:
self._attr_target_temperature_low = None
self._attr_target_temperature_high = None
if (
_get_value(self._coil_temporary_lux)
== VALUES_TEMPORARY_LUX_ONE_TIME_INCREASE
):
self._attr_current_operation = STATE_HIGH_DEMAND
else:
self._attr_current_operation = STATE_HEAT_PUMP
super()._handle_coordinator_update()
@property
def available(self) -> bool:
"""Return if entity is available."""
if not self.coordinator.last_update_success:
return False
if not self._coil_active_accessory:
return True
if active_accessory := self.coordinator.get_coil_value(
self._coil_active_accessory
):
return active_accessory == "ON"
return False
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new target operation mode."""
if not self._coil_temporary_lux:
raise HomeAssistantError("Not supported")
lux = self._operation_mode_to_lux.get(operation_mode)
if not lux:
raise ValueError(f"Unsupported operation mode {operation_mode}")
await self.coordinator.async_write_coil(self._coil_temporary_lux, lux)