diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index c0d7901981b..7513eec8a75 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -85,6 +85,22 @@ class AirzoneSystemEntity(AirzoneEntity): value = system[key] return value + async def _async_update_sys_params(self, params: dict[str, Any]) -> None: + """Send system parameters to API.""" + _params = { + API_SYSTEM_ID: self.system_id, + **params, + } + _LOGGER.debug("update_sys_params=%s", _params) + try: + await self.coordinator.airzone.set_sys_parameters(_params) + except AirzoneError as error: + raise HomeAssistantError( + f"Failed to set system {self.entity_id}: {error}" + ) from error + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) + class AirzoneHotWaterEntity(AirzoneEntity): """Define an Airzone Hot Water entity.""" diff --git a/homeassistant/components/airzone/select.py b/homeassistant/components/airzone/select.py index 813ead8b6a8..fe259c190ff 100644 --- a/homeassistant/components/airzone/select.py +++ b/homeassistant/components/airzone/select.py @@ -20,6 +20,7 @@ from aioairzone.const import ( AZD_MODES, AZD_Q_ADAPT, AZD_SLEEP, + AZD_SYSTEMS, AZD_ZONES, ) @@ -30,7 +31,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator -from .entity import AirzoneEntity, AirzoneZoneEntity +from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity @dataclass(frozen=True, kw_only=True) @@ -85,14 +86,7 @@ def main_zone_options( return [k for k, v in options.items() if v in modes] -MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( - AirzoneSelectDescription( - api_param=API_MODE, - key=AZD_MODE, - options_dict=MODE_DICT, - options_fn=main_zone_options, - translation_key="modes", - ), +SYSTEM_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( AirzoneSelectDescription( api_param=API_Q_ADAPT, entity_category=EntityCategory.CONFIG, @@ -104,6 +98,17 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( ) +MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( + AirzoneSelectDescription( + api_param=API_MODE, + key=AZD_MODE, + options_dict=MODE_DICT, + options_fn=main_zone_options, + translation_key="modes", + ), +) + + ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( AirzoneSelectDescription( api_param=API_COLD_ANGLE, @@ -140,16 +145,37 @@ async def async_setup_entry( """Add Airzone select from a config_entry.""" coordinator = entry.runtime_data + added_systems: set[str] = set() added_zones: set[str] = set() def _async_entity_listener() -> None: """Handle additions of select.""" + entities: list[AirzoneBaseSelect] = [] + + systems_data = coordinator.data.get(AZD_SYSTEMS, {}) + received_systems = set(systems_data) + new_systems = received_systems - added_systems + if new_systems: + entities.extend( + AirzoneSystemSelect( + coordinator, + description, + entry, + system_id, + systems_data.get(system_id), + ) + for system_id in new_systems + for description in SYSTEM_SELECT_TYPES + if description.key in systems_data.get(system_id) + ) + added_systems.update(new_systems) + zones_data = coordinator.data.get(AZD_ZONES, {}) received_zones = set(zones_data) new_zones = received_zones - added_zones if new_zones: - entities: list[AirzoneZoneSelect] = [ + entities.extend( AirzoneZoneSelect( coordinator, description, @@ -161,8 +187,8 @@ async def async_setup_entry( for description in MAIN_ZONE_SELECT_TYPES if description.key in zones_data.get(system_zone_id) and zones_data.get(system_zone_id).get(AZD_MASTER) is True - ] - entities += [ + ) + entities.extend( AirzoneZoneSelect( coordinator, description, @@ -173,10 +199,11 @@ async def async_setup_entry( for system_zone_id in new_zones for description in ZONE_SELECT_TYPES if description.key in zones_data.get(system_zone_id) - ] - async_add_entities(entities) + ) added_zones.update(new_zones) + async_add_entities(entities) + entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener)) _async_entity_listener() @@ -203,6 +230,38 @@ class AirzoneBaseSelect(AirzoneEntity, SelectEntity): self._attr_current_option = self._get_current_option() +class AirzoneSystemSelect(AirzoneSystemEntity, AirzoneBaseSelect): + """Define an Airzone System select.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneSelectDescription, + entry: ConfigEntry, + system_id: str, + system_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_data) + + self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}" + self.entity_description = description + + self._attr_options = self.entity_description.options_fn( + system_data, description.options_dict + ) + + self.values_dict = {v: k for k, v in description.options_dict.items()} + + self._async_update_attrs() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + param = self.entity_description.api_param + value = self.entity_description.options_dict[option] + await self._async_update_sys_params({param: value}) + + class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect): """Define an Airzone Zone select.""" diff --git a/tests/components/airzone/test_select.py b/tests/components/airzone/test_select.py index 343c033728a..4421822f1c8 100644 --- a/tests/components/airzone/test_select.py +++ b/tests/components/airzone/test_select.py @@ -2,12 +2,13 @@ from unittest.mock import patch -from aioairzone.common import OperationMode +from aioairzone.common import OperationMode, QAdapt from aioairzone.const import ( API_COLD_ANGLE, API_DATA, API_HEAT_ANGLE, API_MODE, + API_Q_ADAPT, API_SLEEP, API_SYSTEM_ID, API_ZONE_ID, @@ -17,7 +18,7 @@ import pytest from homeassistant.components.select import ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ServiceValidationError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from .util import async_init_integration @@ -27,6 +28,11 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None: await async_init_integration(hass) + # Systems + state = hass.states.get("select.system_1_q_adapt") + assert state.state == "standard" + + # Zones state = hass.states.get("select.despacho_cold_angle") assert state.state == "90deg" @@ -95,6 +101,71 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None: assert state.state == "off" +async def test_airzone_select_sys_qadapt(hass: HomeAssistant) -> None: + """Test select system Q-Adapt.""" + + await async_init_integration(hass) + + put_q_adapt = { + API_DATA: { + API_SYSTEM_ID: 1, + API_Q_ADAPT: QAdapt.SILENCE, + } + } + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.system_1_q_adapt", + ATTR_OPTION: "Invalid", + }, + blocking=True, + ) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_q_adapt, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.system_1_q_adapt", + ATTR_OPTION: "silence", + }, + blocking=True, + ) + + state = hass.states.get("select.system_1_q_adapt") + assert state.state == "silence" + + put_q_adapt = { + API_DATA: { + API_SYSTEM_ID: 2, + API_Q_ADAPT: QAdapt.SILENCE, + } + } + + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_q_adapt, + ), + pytest.raises(HomeAssistantError), + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.system_1_q_adapt", + ATTR_OPTION: "silence", + }, + blocking=True, + ) + + async def test_airzone_select_sleep(hass: HomeAssistant) -> None: """Test select sleep."""