diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index de75bf03d45..1a54be0ac41 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -24,6 +24,7 @@ PLATFORMS: list[Platform] = [ Platform.CLIMATE, Platform.SELECT, Platform.SENSOR, + Platform.WATER_HEATER, ] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index 267cd210ff0..2310d5fb5a4 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -106,6 +106,22 @@ class AirzoneHotWaterEntity(AirzoneEntity): """Return DHW value by key.""" return self.coordinator.data[AZD_HOT_WATER].get(key) + async def _async_update_dhw_params(self, params: dict[str, Any]) -> None: + """Send DHW parameters to API.""" + _params = { + API_SYSTEM_ID: 0, + **params, + } + _LOGGER.debug("update_dhw_params=%s", _params) + try: + await self.coordinator.airzone.set_dhw_parameters(_params) + except AirzoneError as error: + raise HomeAssistantError( + f"Failed to set dhw {self.name}: {error}" + ) from error + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + class AirzoneWebServerEntity(AirzoneEntity): """Define an Airzone WebServer entity.""" diff --git a/homeassistant/components/airzone/water_heater.py b/homeassistant/components/airzone/water_heater.py new file mode 100644 index 00000000000..b19aa36449c --- /dev/null +++ b/homeassistant/components/airzone/water_heater.py @@ -0,0 +1,131 @@ +"""Support for the Airzone water heater.""" +from __future__ import annotations + +from typing import Any, Final + +from aioairzone.common import HotWaterOperation +from aioairzone.const import ( + API_ACS_ON, + API_ACS_POWER_MODE, + API_ACS_SET_POINT, + AZD_HOT_WATER, + AZD_NAME, + AZD_OPERATION, + AZD_OPERATIONS, + AZD_TEMP, + AZD_TEMP_MAX, + AZD_TEMP_MIN, + AZD_TEMP_SET, + AZD_TEMP_UNIT, +) + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS +from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneHotWaterEntity + +OPERATION_LIB_TO_HASS: Final[dict[HotWaterOperation, str]] = { + HotWaterOperation.Off: STATE_OFF, + HotWaterOperation.On: STATE_ECO, + HotWaterOperation.Powerful: STATE_PERFORMANCE, +} + +OPERATION_MODE_TO_DHW_PARAMS: Final[dict[str, dict[str, Any]]] = { + STATE_OFF: { + API_ACS_ON: 0, + }, + STATE_ECO: { + API_ACS_ON: 1, + API_ACS_POWER_MODE: 0, + }, + STATE_PERFORMANCE: { + API_ACS_ON: 1, + API_ACS_POWER_MODE: 1, + }, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + if AZD_HOT_WATER in coordinator.data: + async_add_entities([AirzoneWaterHeater(coordinator, entry)]) + + +class AirzoneWaterHeater(AirzoneHotWaterEntity, WaterHeaterEntity): + """Define an Airzone Water Heater.""" + + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.ON_OFF + | WaterHeaterEntityFeature.OPERATION_MODE + ) + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize Airzone water heater entity.""" + super().__init__(coordinator, entry) + + self._attr_name = self.get_airzone_value(AZD_NAME) + self._attr_unique_id = f"{self._attr_unique_id}_dhw" + self._attr_operation_list = [ + OPERATION_LIB_TO_HASS[operation] + for operation in self.get_airzone_value(AZD_OPERATIONS) + ] + self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[ + self.get_airzone_value(AZD_TEMP_UNIT) + ] + + self._async_update_attrs() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + await self._async_update_dhw_params({API_ACS_ON: 0}) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the water heater off.""" + await self._async_update_dhw_params({API_ACS_ON: 1}) + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new target operation mode.""" + params = OPERATION_MODE_TO_DHW_PARAMS.get(operation_mode, {}) + await self._async_update_dhw_params(params) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + params: dict[str, Any] = {} + if ATTR_TEMPERATURE in kwargs: + params[API_ACS_SET_POINT] = kwargs[ATTR_TEMPERATURE] + await self._async_update_dhw_params(params) + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update water heater attributes.""" + self._attr_current_temperature = self.get_airzone_value(AZD_TEMP) + self._attr_current_operation = OPERATION_LIB_TO_HASS[ + self.get_airzone_value(AZD_OPERATION) + ] + self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX) + self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN) + self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) diff --git a/tests/components/airzone/test_water_heater.py b/tests/components/airzone/test_water_heater.py new file mode 100644 index 00000000000..a1157192f23 --- /dev/null +++ b/tests/components/airzone/test_water_heater.py @@ -0,0 +1,228 @@ +"""The water heater tests for the Airzone platform.""" +from unittest.mock import patch + +from aioairzone.const import ( + API_ACS_ON, + API_ACS_POWER_MODE, + API_ACS_SET_POINT, + API_DATA, + API_SYSTEM_ID, +) +from aioairzone.exceptions import AirzoneError +import pytest + +from homeassistant.components.water_heater import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_OPERATION_MODE, + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_PERFORMANCE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .util import async_init_integration + + +async def test_airzone_create_water_heater(hass: HomeAssistant) -> None: + """Test creation of water heater.""" + + await async_init_integration(hass) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.state == STATE_ECO + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 43 + assert state.attributes[ATTR_MAX_TEMP] == 75 + assert state.attributes[ATTR_MIN_TEMP] == 30 + assert state.attributes[ATTR_TEMPERATURE] == 45 + + +async def test_airzone_water_heater_turn_on_off(hass: HomeAssistant) -> None: + """Test turning on/off.""" + + await async_init_integration(hass) + + HVAC_MOCK = { + API_DATA: { + API_SYSTEM_ID: 0, + API_ACS_ON: 0, + } + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "water_heater.airzone_dhw", + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.state == STATE_OFF + + HVAC_MOCK = { + API_DATA: { + API_SYSTEM_ID: 0, + API_ACS_ON: 1, + } + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "water_heater.airzone_dhw", + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.state == STATE_ECO + + +async def test_airzone_water_heater_set_operation(hass: HomeAssistant) -> None: + """Test setting the Operation mode.""" + + await async_init_integration(hass) + + HVAC_MOCK_1 = { + API_DATA: { + API_SYSTEM_ID: 0, + API_ACS_ON: 0, + } + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK_1, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_dhw", + ATTR_OPERATION_MODE: STATE_OFF, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.state == STATE_OFF + + HVAC_MOCK_2 = { + API_DATA: { + API_SYSTEM_ID: 0, + API_ACS_ON: 1, + API_ACS_POWER_MODE: 1, + } + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK_2, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_dhw", + ATTR_OPERATION_MODE: STATE_PERFORMANCE, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.state == STATE_PERFORMANCE + + HVAC_MOCK_3 = { + API_DATA: { + API_SYSTEM_ID: 0, + API_ACS_ON: 1, + API_ACS_POWER_MODE: 0, + } + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK_3, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_OPERATION_MODE, + { + ATTR_ENTITY_ID: "water_heater.airzone_dhw", + ATTR_OPERATION_MODE: STATE_ECO, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.state == STATE_ECO + + +async def test_airzone_water_heater_set_temp(hass: HomeAssistant) -> None: + """Test setting the target temperature.""" + + HVAC_MOCK = { + API_DATA: { + API_SYSTEM_ID: 0, + API_ACS_SET_POINT: 35, + } + } + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=HVAC_MOCK, + ): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "water_heater.airzone_dhw", + ATTR_TEMPERATURE: 35, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.attributes[ATTR_TEMPERATURE] == 35 + + +async def test_airzone_water_heater_set_temp_error(hass: HomeAssistant) -> None: + """Test error when setting the target temperature.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + side_effect=AirzoneError, + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "water_heater.airzone_dhw", + ATTR_TEMPERATURE: 80, + }, + blocking=True, + ) + + state = hass.states.get("water_heater.airzone_dhw") + assert state.attributes[ATTR_TEMPERATURE] == 45