From 3178eac9ccdebb267c6e1b3d65cf990d6d079012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 27 Sep 2023 17:20:21 +0200 Subject: [PATCH] Implement Airzone Cloud Zone climate support (#100792) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement Airzone Cloud Zone climate support Signed-off-by: Álvaro Fernández Rojas * airzone_cloud: add entity naming Signed-off-by: Álvaro Fernández Rojas * airzone_cloud: implement requested changes Signed-off-by: Álvaro Fernández Rojas --------- Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone_cloud/__init__.py | 1 + .../components/airzone_cloud/climate.py | 208 ++++++++++++++++ .../components/airzone_cloud/entity.py | 21 ++ .../snapshots/test_diagnostics.ambr | 125 ++++++++-- .../components/airzone_cloud/test_climate.py | 224 ++++++++++++++++++ tests/components/airzone_cloud/util.py | 117 ++++++++- 6 files changed, 669 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/airzone_cloud/climate.py create mode 100644 tests/components/airzone_cloud/test_climate.py diff --git a/homeassistant/components/airzone_cloud/__init__.py b/homeassistant/components/airzone_cloud/__init__.py index 732f159c381..38c764d4889 100644 --- a/homeassistant/components/airzone_cloud/__init__.py +++ b/homeassistant/components/airzone_cloud/__init__.py @@ -14,6 +14,7 @@ from .coordinator import AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, + Platform.CLIMATE, Platform.SENSOR, ] diff --git a/homeassistant/components/airzone_cloud/climate.py b/homeassistant/components/airzone_cloud/climate.py new file mode 100644 index 00000000000..18393031ae3 --- /dev/null +++ b/homeassistant/components/airzone_cloud/climate.py @@ -0,0 +1,208 @@ +"""Support for the Airzone Cloud climate.""" +from __future__ import annotations + +from typing import Any, Final + +from aioairzone_cloud.common import OperationAction, OperationMode, TemperatureUnit +from aioairzone_cloud.const import ( + API_MODE, + API_OPTS, + API_POWER, + API_SETPOINT, + API_UNITS, + API_VALUE, + AZD_ACTION, + AZD_HUMIDITY, + AZD_MASTER, + AZD_MODE, + AZD_MODES, + AZD_POWER, + AZD_TEMP, + AZD_TEMP_SET, + AZD_TEMP_SET_MAX, + AZD_TEMP_SET_MIN, + AZD_TEMP_STEP, + AZD_ZONES, +) + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneEntity, AirzoneZoneEntity + +HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationAction, HVACAction]] = { + OperationAction.COOLING: HVACAction.COOLING, + OperationAction.DRYING: HVACAction.DRYING, + OperationAction.FAN: HVACAction.FAN, + OperationAction.HEATING: HVACAction.HEATING, + OperationAction.IDLE: HVACAction.IDLE, + OperationAction.OFF: HVACAction.OFF, +} +HVAC_MODE_LIB_TO_HASS: Final[dict[OperationMode, HVACMode]] = { + OperationMode.STOP: HVACMode.OFF, + OperationMode.COOLING: HVACMode.COOL, + OperationMode.COOLING_AIR: HVACMode.COOL, + OperationMode.COOLING_RADIANT: HVACMode.COOL, + OperationMode.COOLING_COMBINED: HVACMode.COOL, + OperationMode.HEATING: HVACMode.HEAT, + OperationMode.HEAT_AIR: HVACMode.HEAT, + OperationMode.HEAT_RADIANT: HVACMode.HEAT, + OperationMode.HEAT_COMBINED: HVACMode.HEAT, + OperationMode.EMERGENCY_HEAT: HVACMode.HEAT, + OperationMode.VENTILATION: HVACMode.FAN_ONLY, + OperationMode.DRY: HVACMode.DRY, + OperationMode.AUTO: HVACMode.HEAT_COOL, +} +HVAC_MODE_HASS_TO_LIB: Final[dict[HVACMode, OperationMode]] = { + HVACMode.OFF: OperationMode.STOP, + HVACMode.COOL: OperationMode.COOLING, + HVACMode.HEAT: OperationMode.HEATING, + HVACMode.FAN_ONLY: OperationMode.VENTILATION, + HVACMode.DRY: OperationMode.DRY, + HVACMode.HEAT_COOL: OperationMode.AUTO, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone climate from a config_entry.""" + coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[AirzoneClimate] = [] + + # Zones + for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): + entities.append( + AirzoneZoneClimate( + coordinator, + zone_id, + zone_data, + ) + ) + + async_add_entities(entities) + + +class AirzoneClimate(AirzoneEntity, ClimateEntity): + """Define an Airzone Cloud climate.""" + + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_temperature_unit = UnitOfTemperature.CELSIUS + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + params = { + API_POWER: { + API_VALUE: True, + }, + } + await self._async_update_params(params) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + params = { + API_POWER: { + API_VALUE: False, + }, + } + await self._async_update_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_SETPOINT] = { + API_VALUE: kwargs[ATTR_TEMPERATURE], + API_OPTS: { + API_UNITS: TemperatureUnit.CELSIUS.value, + }, + } + await self._async_update_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 climate attributes.""" + self._attr_current_temperature = self.get_airzone_value(AZD_TEMP) + self._attr_current_humidity = self.get_airzone_value(AZD_HUMIDITY) + self._attr_hvac_action = HVAC_ACTION_LIB_TO_HASS[ + self.get_airzone_value(AZD_ACTION) + ] + if self.get_airzone_value(AZD_POWER): + self._attr_hvac_mode = HVAC_MODE_LIB_TO_HASS[ + self.get_airzone_value(AZD_MODE) + ] + else: + self._attr_hvac_mode = HVACMode.OFF + self._attr_max_temp = self.get_airzone_value(AZD_TEMP_SET_MAX) + self._attr_min_temp = self.get_airzone_value(AZD_TEMP_SET_MIN) + self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET) + + +class AirzoneZoneClimate(AirzoneZoneEntity, AirzoneClimate): + """Define an Airzone Cloud Zone climate.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + system_zone_id: str, + zone_data: dict, + ) -> None: + """Initialize Airzone Cloud Zone climate.""" + super().__init__(coordinator, system_zone_id, zone_data) + + self._attr_unique_id = system_zone_id + self._attr_target_temperature_step = self.get_airzone_value(AZD_TEMP_STEP) + self._attr_hvac_modes = [ + HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES) + ] + if HVACMode.OFF not in self._attr_hvac_modes: + self._attr_hvac_modes += [HVACMode.OFF] + + self._async_update_attrs() + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + slave_raise = False + + params: dict[str, Any] = {} + if hvac_mode == HVACMode.OFF: + params[API_POWER] = { + API_VALUE: False, + } + else: + mode = HVAC_MODE_HASS_TO_LIB[hvac_mode] + if mode != self.get_airzone_value(AZD_MODE): + if self.get_airzone_value(AZD_MASTER): + params[API_MODE] = { + API_VALUE: mode.value, + } + else: + slave_raise = True + params[API_POWER] = { + API_VALUE: True, + } + + await self._async_update_params(params) + + if slave_raise: + raise HomeAssistantError(f"Mode can't be changed on slave zone {self.name}") diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index 090e81e4170..3214869aaab 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import ABC, abstractmethod +import logging from typing import Any from aioairzone_cloud.const import ( @@ -15,7 +16,9 @@ from aioairzone_cloud.const import ( AZD_WEBSERVERS, AZD_ZONES, ) +from aioairzone_cloud.exceptions import AirzoneCloudError +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,6 +26,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER from .coordinator import AirzoneUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator], ABC): """Define an Airzone Cloud entity.""" @@ -36,6 +41,10 @@ class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator], ABC): def get_airzone_value(self, key: str) -> Any: """Return Airzone Cloud entity value by key.""" + async def _async_update_params(self, params: dict[str, Any]) -> None: + """Send Airzone parameters to Cloud API.""" + raise NotImplementedError + class AirzoneAidooEntity(AirzoneEntity): """Define an Airzone Cloud Aidoo entity.""" @@ -153,3 +162,15 @@ class AirzoneZoneEntity(AirzoneEntity): if zone := self.coordinator.data[AZD_ZONES].get(self.zone_id): value = zone.get(key) return value + + async def _async_update_params(self, params: dict[str, Any]) -> None: + """Send Zone parameters to Cloud API.""" + _LOGGER.debug("zone=%s: update_params=%s", self.name, params) + try: + await self.coordinator.airzone.api_set_zone_id_params(self.zone_id, params) + except AirzoneCloudError as error: + raise HomeAssistantError( + f"Failed to set {self.name} params: {error}" + ) from error + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) diff --git a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr index 94e602ec03b..fb33323378a 100644 --- a/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone_cloud/snapshots/test_diagnostics.ambr @@ -79,11 +79,29 @@ 'id': 'aidoo1', 'installation': 'installation1', 'is-connected': True, - 'mode': None, + 'mode': 3, + 'modes': list([ + 1, + 2, + 3, + 4, + 5, + ]), 'name': 'Bron', - 'power': None, + 'power': False, 'problems': False, 'temperature': 21.0, + 'temperature-setpoint': 22.0, + 'temperature-setpoint-cool-air': 22.0, + 'temperature-setpoint-hot-air': 22.0, + 'temperature-setpoint-max': 30.0, + 'temperature-setpoint-max-auto-air': 30.0, + 'temperature-setpoint-max-cool-air': 30.0, + 'temperature-setpoint-max-hot-air': 30.0, + 'temperature-setpoint-min': 15.0, + 'temperature-setpoint-min-auto-air': 18.0, + 'temperature-setpoint-min-cool-air': 18.0, + 'temperature-setpoint-min-hot-air': 16.0, 'temperature-step': 0.5, 'web-server': '11:22:33:44:55:67', 'ws-connected': True, @@ -91,19 +109,28 @@ }), 'groups': dict({ 'group1': dict({ - 'action': 6, + 'action': 1, 'active': True, 'available': True, 'humidity': 27, 'installation': 'installation1', - 'mode': 0, + 'mode': 2, + 'modes': list([ + 2, + 3, + 4, + 5, + ]), 'name': 'Group', 'num-devices': 2, - 'power': None, + 'power': True, 'systems': list([ 'system1', ]), 'temperature': 22.5, + 'temperature-setpoint': 24.0, + 'temperature-setpoint-max': 30.0, + 'temperature-setpoint-min': 15.0, 'temperature-step': 0.5, 'zones': list([ 'zone1', @@ -118,11 +145,21 @@ ]), 'available': True, 'installation': 'installation1', - 'mode': 0, + 'mode': 3, + 'modes': list([ + 1, + 2, + 3, + 4, + 5, + ]), 'name': 'Aidoo Group', 'num-devices': 1, - 'power': None, + 'power': False, 'temperature': 21.0, + 'temperature-setpoint': 22.0, + 'temperature-setpoint-max': 30.0, + 'temperature-setpoint-min': 15.0, 'temperature-step': 0.5, }), }), @@ -147,7 +184,13 @@ 'id': 'system1', 'installation': 'installation1', 'is-connected': True, - 'mode': None, + 'mode': 2, + 'modes': list([ + 2, + 3, + 4, + 5, + ]), 'name': 'System 1', 'problems': True, 'system': 1, @@ -189,21 +232,47 @@ }), 'zones': dict({ 'zone1': dict({ - 'action': 6, + 'action': 1, 'active': True, 'available': True, 'humidity': 30, 'id': 'zone1', 'installation': 'installation1', 'is-connected': True, - 'master': None, - 'mode': None, + 'master': True, + 'mode': 2, + 'modes': list([ + 2, + 3, + 4, + 5, + ]), 'name': 'Salon', - 'power': None, + 'power': True, 'problems': False, 'system': 1, 'system-id': 'system1', 'temperature': 20.0, + 'temperature-setpoint': 24.0, + 'temperature-setpoint-cool-air': 24.0, + 'temperature-setpoint-dry-air': 24.0, + 'temperature-setpoint-hot-air': 20.0, + 'temperature-setpoint-max': 30.0, + 'temperature-setpoint-max-cool-air': 30.0, + 'temperature-setpoint-max-dry-air': 30.0, + 'temperature-setpoint-max-emerheat-air': 30.0, + 'temperature-setpoint-max-hot-air': 30.0, + 'temperature-setpoint-max-stop-air': 30.0, + 'temperature-setpoint-max-vent-air': 30.0, + 'temperature-setpoint-min': 15.0, + 'temperature-setpoint-min-cool-air': 18.0, + 'temperature-setpoint-min-dry-air': 18.0, + 'temperature-setpoint-min-emerheat-air': 15.0, + 'temperature-setpoint-min-hot-air': 15.0, + 'temperature-setpoint-min-stop-air': 15.0, + 'temperature-setpoint-min-vent-air': 15.0, + 'temperature-setpoint-stop-air': 24.0, + 'temperature-setpoint-vent-air': 24.0, 'temperature-step': 0.5, 'web-server': 'webserver1', 'ws-connected': True, @@ -217,14 +286,40 @@ 'id': 'zone2', 'installation': 'installation1', 'is-connected': True, - 'master': None, - 'mode': None, + 'master': False, + 'mode': 2, + 'modes': list([ + 2, + 3, + 4, + 5, + ]), 'name': 'Dormitorio', - 'power': None, + 'power': False, 'problems': False, 'system': 1, 'system-id': 'system1', 'temperature': 25.0, + 'temperature-setpoint': 24.0, + 'temperature-setpoint-cool-air': 24.0, + 'temperature-setpoint-dry-air': 24.0, + 'temperature-setpoint-hot-air': 20.0, + 'temperature-setpoint-max': 30.0, + 'temperature-setpoint-max-cool-air': 30.0, + 'temperature-setpoint-max-dry-air': 30.0, + 'temperature-setpoint-max-emerheat-air': 30.0, + 'temperature-setpoint-max-hot-air': 30.0, + 'temperature-setpoint-max-stop-air': 30.0, + 'temperature-setpoint-max-vent-air': 30.0, + 'temperature-setpoint-min': 15.0, + 'temperature-setpoint-min-cool-air': 18.0, + 'temperature-setpoint-min-dry-air': 18.0, + 'temperature-setpoint-min-emerheat-air': 15.0, + 'temperature-setpoint-min-hot-air': 15.0, + 'temperature-setpoint-min-stop-air': 15.0, + 'temperature-setpoint-min-vent-air': 15.0, + 'temperature-setpoint-stop-air': 24.0, + 'temperature-setpoint-vent-air': 24.0, 'temperature-step': 0.5, 'web-server': 'webserver1', 'ws-connected': True, diff --git a/tests/components/airzone_cloud/test_climate.py b/tests/components/airzone_cloud/test_climate.py new file mode 100644 index 00000000000..acf1d082c29 --- /dev/null +++ b/tests/components/airzone_cloud/test_climate.py @@ -0,0 +1,224 @@ +"""The climate tests for the Airzone Cloud platform.""" +from unittest.mock import patch + +from aioairzone_cloud.exceptions import AirzoneCloudError +import pytest + +from homeassistant.components.airzone.const import API_TEMPERATURE_STEP +from homeassistant.components.climate import ( + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + ATTR_TARGET_TEMP_STEP, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + HVACAction, + HVACMode, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .util import async_init_integration + + +async def test_airzone_create_climates(hass: HomeAssistant) -> None: + """Test creation of climates.""" + + await async_init_integration(hass) + + # Zones + state = hass.states.get("climate.dormitorio") + assert state.state == HVACMode.OFF + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 24 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 25.0 + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.OFF + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.FAN_ONLY, + HVACMode.DRY, + HVACMode.OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 24.0 + + state = hass.states.get("climate.salon") + assert state.state == HVACMode.COOL + assert state.attributes.get(ATTR_CURRENT_HUMIDITY) == 30 + assert state.attributes.get(ATTR_CURRENT_TEMPERATURE) == 20.0 + assert state.attributes.get(ATTR_HVAC_ACTION) == HVACAction.COOLING + assert state.attributes.get(ATTR_HVAC_MODES) == [ + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.FAN_ONLY, + HVACMode.DRY, + HVACMode.OFF, + ] + assert state.attributes.get(ATTR_MAX_TEMP) == 30 + assert state.attributes.get(ATTR_MIN_TEMP) == 15 + assert state.attributes.get(ATTR_TARGET_TEMP_STEP) == API_TEMPERATURE_STEP + assert state.attributes.get(ATTR_TEMPERATURE) == 24.0 + + +async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: + """Test turning on/off.""" + + await async_init_integration(hass) + + # Zones + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "climate.dormitorio", + }, + blocking=True, + ) + + state = hass.states.get("climate.dormitorio") + assert state.state == HVACMode.COOL + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "climate.salon", + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVACMode.OFF + + +async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: + """Test setting the HVAC mode.""" + + await async_init_integration(hass) + + # Zones + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_HVAC_MODE: HVACMode.HEAT, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVACMode.HEAT + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_HVAC_MODE: HVACMode.OFF, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.state == HVACMode.OFF + + +async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None: + """Test setting the HVAC mode for a slave zone.""" + + await async_init_integration(hass) + + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.dormitorio", + ATTR_HVAC_MODE: HVACMode.HEAT, + }, + blocking=True, + ) + + state = hass.states.get("climate.dormitorio") + assert state.state == HVACMode.COOL + + +async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None: + """Test setting the target temperature.""" + + await async_init_integration(hass) + + # Zones + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + return_value=None, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.attributes.get(ATTR_TEMPERATURE) == 20.5 + + +async def test_airzone_climate_set_temp_error(hass: HomeAssistant) -> None: + """Test error when setting the target temperature.""" + + await async_init_integration(hass) + + # Zones + with patch( + "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_patch_device", + side_effect=AirzoneCloudError, + ), pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.salon", + ATTR_TEMPERATURE: 20.5, + }, + blocking=True, + ) + + state = hass.states.get("climate.salon") + assert state.attributes.get(ATTR_TEMPERATURE) == 24.0 diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 8fd7da06853..412f0df1337 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -3,6 +3,7 @@ from typing import Any from unittest.mock import patch +from aioairzone_cloud.common import OperationMode from aioairzone_cloud.const import ( API_ACTIVE, API_AZ_AIDOO, @@ -24,8 +25,33 @@ from aioairzone_cloud.const import ( API_IS_CONNECTED, API_LOCAL_TEMP, API_META, + API_MODE, + API_MODE_AVAIL, API_NAME, API_OLD_ID, + API_POWER, + API_RANGE_MAX_AIR, + API_RANGE_MIN_AIR, + API_RANGE_SP_MAX_AUTO_AIR, + API_RANGE_SP_MAX_COOL_AIR, + API_RANGE_SP_MAX_DRY_AIR, + API_RANGE_SP_MAX_EMERHEAT_AIR, + API_RANGE_SP_MAX_HOT_AIR, + API_RANGE_SP_MAX_STOP_AIR, + API_RANGE_SP_MAX_VENT_AIR, + API_RANGE_SP_MIN_AUTO_AIR, + API_RANGE_SP_MIN_COOL_AIR, + API_RANGE_SP_MIN_DRY_AIR, + API_RANGE_SP_MIN_EMERHEAT_AIR, + API_RANGE_SP_MIN_HOT_AIR, + API_RANGE_SP_MIN_STOP_AIR, + API_RANGE_SP_MIN_VENT_AIR, + API_SP_AIR_AUTO, + API_SP_AIR_COOL, + API_SP_AIR_DRY, + API_SP_AIR_HEAT, + API_SP_AIR_STOP, + API_SP_AIR_VENT, API_STAT_AP_MAC, API_STAT_CHANNEL, API_STAT_QUALITY, @@ -166,12 +192,29 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: return { API_ACTIVE: False, API_ERRORS: [], + API_MODE: OperationMode.HEATING.value, + API_MODE_AVAIL: [ + OperationMode.AUTO.value, + OperationMode.COOLING.value, + OperationMode.HEATING.value, + OperationMode.VENTILATION.value, + OperationMode.DRY.value, + ], + API_SP_AIR_AUTO: {API_CELSIUS: 22, API_FAH: 72}, + API_SP_AIR_COOL: {API_CELSIUS: 22, API_FAH: 72}, + API_SP_AIR_HEAT: {API_CELSIUS: 22, API_FAH: 72}, + API_RANGE_MAX_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_AUTO_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_COOL_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_HOT_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_MIN_AIR: {API_CELSIUS: 15, API_FAH: 59}, + API_RANGE_SP_MIN_AUTO_AIR: {API_CELSIUS: 18, API_FAH: 64}, + API_RANGE_SP_MIN_COOL_AIR: {API_CELSIUS: 18, API_FAH: 64}, + API_RANGE_SP_MIN_HOT_AIR: {API_CELSIUS: 16, API_FAH: 61}, + API_POWER: False, API_IS_CONNECTED: True, API_WS_CONNECTED: True, - API_LOCAL_TEMP: { - API_CELSIUS: 21, - API_FAH: 70, - }, + API_LOCAL_TEMP: {API_CELSIUS: 21, API_FAH: 70}, API_WARNINGS: [], } if device.get_id() == "system1": @@ -181,6 +224,13 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: API_OLD_ID: "error-id", }, ], + API_MODE: OperationMode.COOLING.value, + API_MODE_AVAIL: [ + OperationMode.COOLING.value, + OperationMode.HEATING.value, + OperationMode.VENTILATION.value, + OperationMode.DRY.value, + ], API_IS_CONNECTED: True, API_WS_CONNECTED: True, API_WARNINGS: [], @@ -189,24 +239,67 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: return { API_ACTIVE: True, API_HUMIDITY: 30, + API_MODE: OperationMode.COOLING.value, + API_MODE_AVAIL: [ + OperationMode.COOLING.value, + OperationMode.HEATING.value, + OperationMode.VENTILATION.value, + OperationMode.DRY.value, + ], + API_RANGE_MAX_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_COOL_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_SP_MAX_DRY_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_SP_MAX_EMERHEAT_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_HOT_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_STOP_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_SP_MAX_VENT_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_MIN_AIR: {API_CELSIUS: 15, API_FAH: 59}, + API_RANGE_SP_MIN_COOL_AIR: {API_CELSIUS: 18, API_FAH: 64}, + API_RANGE_SP_MIN_DRY_AIR: {API_CELSIUS: 18, API_FAH: 64}, + API_RANGE_SP_MIN_EMERHEAT_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_RANGE_SP_MIN_HOT_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_RANGE_SP_MIN_STOP_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_RANGE_SP_MIN_VENT_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_SP_AIR_COOL: {API_CELSIUS: 24, API_FAH: 75}, + API_SP_AIR_DRY: {API_CELSIUS: 24, API_FAH: 75}, + API_SP_AIR_HEAT: {API_CELSIUS: 20, API_FAH: 68}, + API_SP_AIR_VENT: {API_CELSIUS: 24, API_FAH: 75}, + API_SP_AIR_STOP: {API_CELSIUS: 24, API_FAH: 75}, + API_POWER: True, API_IS_CONNECTED: True, API_WS_CONNECTED: True, - API_LOCAL_TEMP: { - API_FAH: 68, - API_CELSIUS: 20, - }, + API_LOCAL_TEMP: {API_FAH: 68, API_CELSIUS: 20}, API_WARNINGS: [], } if device.get_id() == "zone2": return { API_ACTIVE: False, API_HUMIDITY: 24, + API_MODE: OperationMode.COOLING.value, + API_MODE_AVAIL: [], + API_RANGE_MAX_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_COOL_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_SP_MAX_DRY_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_SP_MAX_EMERHEAT_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_HOT_AIR: {API_CELSIUS: 30, API_FAH: 86}, + API_RANGE_SP_MAX_STOP_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_SP_MAX_VENT_AIR: {API_FAH: 86, API_CELSIUS: 30}, + API_RANGE_MIN_AIR: {API_CELSIUS: 15, API_FAH: 59}, + API_RANGE_SP_MIN_COOL_AIR: {API_CELSIUS: 18, API_FAH: 64}, + API_RANGE_SP_MIN_DRY_AIR: {API_CELSIUS: 18, API_FAH: 64}, + API_RANGE_SP_MIN_EMERHEAT_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_RANGE_SP_MIN_HOT_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_RANGE_SP_MIN_STOP_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_RANGE_SP_MIN_VENT_AIR: {API_FAH: 59, API_CELSIUS: 15}, + API_SP_AIR_COOL: {API_CELSIUS: 24, API_FAH: 75}, + API_SP_AIR_DRY: {API_CELSIUS: 24, API_FAH: 75}, + API_SP_AIR_HEAT: {API_CELSIUS: 20, API_FAH: 68}, + API_SP_AIR_VENT: {API_CELSIUS: 24, API_FAH: 75}, + API_SP_AIR_STOP: {API_CELSIUS: 24, API_FAH: 75}, + API_POWER: False, API_IS_CONNECTED: True, API_WS_CONNECTED: True, - API_LOCAL_TEMP: { - API_FAH: 77, - API_CELSIUS: 25, - }, + API_LOCAL_TEMP: {API_FAH: 77, API_CELSIUS: 25}, API_WARNINGS: [], } return None