From 95fa123a0b519167edaba5323a45a936e2a3744e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= <noltari@gmail.com> Date: Mon, 26 Aug 2024 17:57:31 +0200 Subject: [PATCH] Add Airzone main zone mode select (#124566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * airzone: select: add zone master mode Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone: select: use MAIN instead of MASTER Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone: select: call async_add_entities once Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone: select: add options lambda function Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone: select: implement requested changes Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone: select: options_fn: return list Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> --------- Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> --- homeassistant/components/airzone/select.py | 62 +++++++++++++++- homeassistant/components/airzone/strings.json | 11 +++ tests/components/airzone/test_select.py | 70 ++++++++++++++++++- 3 files changed, 139 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airzone/select.py b/homeassistant/components/airzone/select.py index 493150e5c6a..2bc11bc4228 100644 --- a/homeassistant/components/airzone/select.py +++ b/homeassistant/components/airzone/select.py @@ -2,16 +2,21 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from typing import Any, Final -from aioairzone.common import GrilleAngle, SleepTimeout +from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout from aioairzone.const import ( API_COLD_ANGLE, API_HEAT_ANGLE, + API_MODE, API_SLEEP, AZD_COLD_ANGLE, AZD_HEAT_ANGLE, + AZD_MASTER, + AZD_MODE, + AZD_MODES, AZD_SLEEP, AZD_ZONES, ) @@ -33,6 +38,9 @@ class AirzoneSelectDescription(SelectEntityDescription): api_param: str options_dict: dict[str, int] + options_fn: Callable[[dict[str, Any], dict[str, int]], list[str]] = ( + lambda zone_data, value: list(value) + ) GRILLE_ANGLE_DICT: Final[dict[str, int]] = { @@ -42,6 +50,15 @@ GRILLE_ANGLE_DICT: Final[dict[str, int]] = { "40deg": GrilleAngle.DEG_40, } +MODE_DICT: Final[dict[str, int]] = { + "cool": OperationMode.COOLING, + "dry": OperationMode.DRY, + "fan": OperationMode.FAN, + "heat": OperationMode.HEATING, + "heat_cool": OperationMode.AUTO, + "stop": OperationMode.STOP, +} + SLEEP_DICT: Final[dict[str, int]] = { "off": SleepTimeout.SLEEP_OFF, "30m": SleepTimeout.SLEEP_30, @@ -50,6 +67,26 @@ SLEEP_DICT: Final[dict[str, int]] = { } +def main_zone_options( + zone_data: dict[str, Any], + options: dict[str, int], +) -> list[str]: + """Filter available modes.""" + modes = zone_data.get(AZD_MODES, []) + 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", + ), +) + + ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( AirzoneSelectDescription( api_param=API_COLD_ANGLE, @@ -95,7 +132,20 @@ async def async_setup_entry( received_zones = set(zones_data) new_zones = received_zones - added_zones if new_zones: - async_add_entities( + entities: list[AirzoneZoneSelect] = [ + AirzoneZoneSelect( + coordinator, + description, + entry, + system_zone_id, + zones_data.get(system_zone_id), + ) + for system_zone_id in new_zones + 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 += [ AirzoneZoneSelect( coordinator, description, @@ -106,7 +156,8 @@ 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) entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener)) @@ -153,6 +204,11 @@ class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect): f"{self._attr_unique_id}_{system_zone_id}_{description.key}" ) self.entity_description = description + + self._attr_options = self.entity_description.options_fn( + zone_data, description.options_dict + ) + self.values_dict = {v: k for k, v in description.options_dict.items()} self._async_update_attrs() diff --git a/homeassistant/components/airzone/strings.json b/homeassistant/components/airzone/strings.json index 438304d7f41..cd313b821aa 100644 --- a/homeassistant/components/airzone/strings.json +++ b/homeassistant/components/airzone/strings.json @@ -52,6 +52,17 @@ "40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]" } }, + "modes": { + "name": "Mode", + "state": { + "cool": "[%key:component::climate::entity_component::_::state::cool%]", + "dry": "[%key:component::climate::entity_component::_::state::dry%]", + "fan": "[%key:component::climate::entity_component::_::state::fan_only%]", + "heat": "[%key:component::climate::entity_component::_::state::heat%]", + "heat_cool": "[%key:component::climate::entity_component::_::state::heat_cool%]", + "stop": "Stop" + } + }, "sleep_times": { "name": "Sleep", "state": { diff --git a/tests/components/airzone/test_select.py b/tests/components/airzone/test_select.py index 01617eab175..343c033728a 100644 --- a/tests/components/airzone/test_select.py +++ b/tests/components/airzone/test_select.py @@ -2,17 +2,19 @@ from unittest.mock import patch +from aioairzone.common import OperationMode from aioairzone.const import ( API_COLD_ANGLE, API_DATA, API_HEAT_ANGLE, + API_MODE, API_SLEEP, API_SYSTEM_ID, API_ZONE_ID, ) import pytest -from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +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 @@ -31,6 +33,9 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None: state = hass.states.get("select.despacho_heat_angle") assert state.state == "90deg" + state = hass.states.get("select.despacho_mode") + assert state is None + state = hass.states.get("select.despacho_sleep") assert state.state == "off" @@ -40,6 +45,9 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None: state = hass.states.get("select.dorm_1_heat_angle") assert state.state == "90deg" + state = hass.states.get("select.dorm_1_mode") + assert state is None + state = hass.states.get("select.dorm_1_sleep") assert state.state == "off" @@ -49,6 +57,9 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None: state = hass.states.get("select.dorm_2_heat_angle") assert state.state == "90deg" + state = hass.states.get("select.dorm_2_mode") + assert state is None + state = hass.states.get("select.dorm_2_sleep") assert state.state == "off" @@ -58,6 +69,9 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None: state = hass.states.get("select.dorm_ppal_heat_angle") assert state.state == "50deg" + state = hass.states.get("select.dorm_ppal_mode") + assert state is None + state = hass.states.get("select.dorm_ppal_sleep") assert state.state == "30m" @@ -67,6 +81,16 @@ async def test_airzone_create_selects(hass: HomeAssistant) -> None: state = hass.states.get("select.salon_heat_angle") assert state.state == "90deg" + state = hass.states.get("select.salon_mode") + assert state.state == "heat" + assert state.attributes.get(ATTR_OPTIONS) == [ + "cool", + "dry", + "fan", + "heat", + "stop", + ] + state = hass.states.get("select.salon_sleep") assert state.state == "off" @@ -115,6 +139,50 @@ async def test_airzone_select_sleep(hass: HomeAssistant) -> None: assert state.state == "30m" +async def test_airzone_select_mode(hass: HomeAssistant) -> None: + """Test select HVAC mode.""" + + await async_init_integration(hass) + + put_hvac_mode = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 1, + API_MODE: OperationMode.COOLING, + } + ] + } + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.salon_mode", + ATTR_OPTION: "Invalid", + }, + blocking=True, + ) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_hvac_mode, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.salon_mode", + ATTR_OPTION: "cool", + }, + blocking=True, + ) + + state = hass.states.get("select.salon_mode") + assert state.state == "cool" + + async def test_airzone_select_grille_angle(hass: HomeAssistant) -> None: """Test select sleep.""" -- GitLab