Add Airzone main zone mode select (#124566)

* 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>
This commit is contained in:
Álvaro Fernández Rojas 2024-08-26 17:57:31 +02:00 committed by GitHub
parent 657ff58500
commit 95fa123a0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 139 additions and 4 deletions

View File

@ -2,16 +2,21 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Final from typing import Any, Final
from aioairzone.common import GrilleAngle, SleepTimeout from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
from aioairzone.const import ( from aioairzone.const import (
API_COLD_ANGLE, API_COLD_ANGLE,
API_HEAT_ANGLE, API_HEAT_ANGLE,
API_MODE,
API_SLEEP, API_SLEEP,
AZD_COLD_ANGLE, AZD_COLD_ANGLE,
AZD_HEAT_ANGLE, AZD_HEAT_ANGLE,
AZD_MASTER,
AZD_MODE,
AZD_MODES,
AZD_SLEEP, AZD_SLEEP,
AZD_ZONES, AZD_ZONES,
) )
@ -33,6 +38,9 @@ class AirzoneSelectDescription(SelectEntityDescription):
api_param: str api_param: str
options_dict: dict[str, int] 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]] = { GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
@ -42,6 +50,15 @@ GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
"40deg": GrilleAngle.DEG_40, "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]] = { SLEEP_DICT: Final[dict[str, int]] = {
"off": SleepTimeout.SLEEP_OFF, "off": SleepTimeout.SLEEP_OFF,
"30m": SleepTimeout.SLEEP_30, "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, ...]] = ( ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription( AirzoneSelectDescription(
api_param=API_COLD_ANGLE, api_param=API_COLD_ANGLE,
@ -95,7 +132,20 @@ async def async_setup_entry(
received_zones = set(zones_data) received_zones = set(zones_data)
new_zones = received_zones - added_zones new_zones = received_zones - added_zones
if new_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( AirzoneZoneSelect(
coordinator, coordinator,
description, description,
@ -106,7 +156,8 @@ async def async_setup_entry(
for system_zone_id in new_zones for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id) if description.key in zones_data.get(system_zone_id)
) ]
async_add_entities(entities)
added_zones.update(new_zones) added_zones.update(new_zones)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener)) 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}" f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
) )
self.entity_description = description 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.values_dict = {v: k for k, v in description.options_dict.items()}
self._async_update_attrs() self._async_update_attrs()

View File

@ -52,6 +52,17 @@
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]" "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": { "sleep_times": {
"name": "Sleep", "name": "Sleep",
"state": { "state": {

View File

@ -2,17 +2,19 @@
from unittest.mock import patch from unittest.mock import patch
from aioairzone.common import OperationMode
from aioairzone.const import ( from aioairzone.const import (
API_COLD_ANGLE, API_COLD_ANGLE,
API_DATA, API_DATA,
API_HEAT_ANGLE, API_HEAT_ANGLE,
API_MODE,
API_SLEEP, API_SLEEP,
API_SYSTEM_ID, API_SYSTEM_ID,
API_ZONE_ID, API_ZONE_ID,
) )
import pytest 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.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError 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") state = hass.states.get("select.despacho_heat_angle")
assert state.state == "90deg" assert state.state == "90deg"
state = hass.states.get("select.despacho_mode")
assert state is None
state = hass.states.get("select.despacho_sleep") state = hass.states.get("select.despacho_sleep")
assert state.state == "off" 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") state = hass.states.get("select.dorm_1_heat_angle")
assert state.state == "90deg" assert state.state == "90deg"
state = hass.states.get("select.dorm_1_mode")
assert state is None
state = hass.states.get("select.dorm_1_sleep") state = hass.states.get("select.dorm_1_sleep")
assert state.state == "off" 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") state = hass.states.get("select.dorm_2_heat_angle")
assert state.state == "90deg" assert state.state == "90deg"
state = hass.states.get("select.dorm_2_mode")
assert state is None
state = hass.states.get("select.dorm_2_sleep") state = hass.states.get("select.dorm_2_sleep")
assert state.state == "off" 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") state = hass.states.get("select.dorm_ppal_heat_angle")
assert state.state == "50deg" assert state.state == "50deg"
state = hass.states.get("select.dorm_ppal_mode")
assert state is None
state = hass.states.get("select.dorm_ppal_sleep") state = hass.states.get("select.dorm_ppal_sleep")
assert state.state == "30m" 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") state = hass.states.get("select.salon_heat_angle")
assert state.state == "90deg" 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") state = hass.states.get("select.salon_sleep")
assert state.state == "off" assert state.state == "off"
@ -115,6 +139,50 @@ async def test_airzone_select_sleep(hass: HomeAssistant) -> None:
assert state.state == "30m" 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: async def test_airzone_select_grille_angle(hass: HomeAssistant) -> None:
"""Test select sleep.""" """Test select sleep."""