forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add water heater support to Airzone (home-assistant#98401)
Co-authored-by: J. Nick Koston <[email protected]>
- Loading branch information
Showing
4 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[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 |