Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add water heater support to Airzone #98401

Merged
merged 10 commits into from
Sep 10, 2023
1 change: 1 addition & 0 deletions homeassistant/components/airzone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Platform.CLIMATE,
Platform.SELECT,
Platform.SENSOR,
Platform.WATER_HEATER,
]

_LOGGER = logging.getLogger(__name__)
Expand Down
16 changes: 16 additions & 0 deletions homeassistant/components/airzone/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ def get_airzone_value(self, key: str) -> Any:
"""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."""
Expand Down
136 changes: 136 additions & 0 deletions homeassistant/components/airzone/water_heater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""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."""

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_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.ON_OFF
| WaterHeaterEntityFeature.OPERATION_MODE
)
Noltari marked this conversation as resolved.
Show resolved Hide resolved
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."""
params: dict[str, Any] = {
API_ACS_ON: 0,
}
await self._async_update_dhw_params(params)
Noltari marked this conversation as resolved.
Show resolved Hide resolved

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the water heater off."""
params: dict[str, Any] = {
API_ACS_ON: 1,
}
await self._async_update_dhw_params(params)
Noltari marked this conversation as resolved.
Show resolved Hide resolved

async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new target operation mode."""
params: dict[str, Any] = 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)
228 changes: 228 additions & 0 deletions tests/components/airzone/test_water_heater.py
Original file line number Diff line number Diff line change
@@ -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.get(ATTR_CURRENT_TEMPERATURE) == 43
assert state.attributes.get(ATTR_MAX_TEMP) == 75
assert state.attributes.get(ATTR_MIN_TEMP) == 30
assert state.attributes.get(ATTR_TEMPERATURE) == 45
Noltari marked this conversation as resolved.
Show resolved Hide resolved


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.get(ATTR_TEMPERATURE) == 35
Noltari marked this conversation as resolved.
Show resolved Hide resolved


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.get(ATTR_TEMPERATURE) == 45
Noltari marked this conversation as resolved.
Show resolved Hide resolved