From 6aa01e14417702d775654efc1d23dc9a99d9722c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 29 May 2023 21:58:53 +0200 Subject: [PATCH] Add Aidoo sensors to Airzone Cloud (#93541) --- .../components/airzone_cloud/entity.py | 41 ++++++++++++- .../components/airzone_cloud/sensor.py | 61 +++++++++++++++++-- .../airzone_cloud/test_config_flow.py | 6 +- .../airzone_cloud/test_coordinator.py | 6 +- .../airzone_cloud/test_diagnostics.py | 2 + tests/components/airzone_cloud/test_sensor.py | 4 ++ tests/components/airzone_cloud/util.py | 53 +++++++++++++++- 7 files changed, 159 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/airzone_cloud/entity.py b/homeassistant/components/airzone_cloud/entity.py index bf02bf1ccf8..e2a3142f1f5 100644 --- a/homeassistant/components/airzone_cloud/entity.py +++ b/homeassistant/components/airzone_cloud/entity.py @@ -4,7 +4,13 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import Any -from aioairzone_cloud.const import AZD_NAME, AZD_SYSTEM_ID, AZD_ZONES +from aioairzone_cloud.const import ( + AZD_AIDOOS, + AZD_NAME, + AZD_SYSTEM_ID, + AZD_WEBSERVER, + AZD_ZONES, +) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import DeviceInfo @@ -22,6 +28,36 @@ class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator], ABC): """Return Airzone Cloud entity value by key.""" +class AirzoneAidooEntity(AirzoneEntity): + """Define an Airzone Cloud Aidoo entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + aidoo_id: str, + aidoo_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.aidoo_id = aidoo_id + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.unique_id}_{aidoo_id}")}, + manufacturer=MANUFACTURER, + name=aidoo_data[AZD_NAME], + via_device=(DOMAIN, f"{entry.unique_id}_{aidoo_data[AZD_WEBSERVER]}"), + ) + + def get_airzone_value(self, key: str) -> Any: + """Return Aidoo value by key.""" + value = None + if aidoo := self.coordinator.data[AZD_AIDOOS].get(self.aidoo_id): + value = aidoo.get(key) + return value + + class AirzoneZoneEntity(AirzoneEntity): """Define an Airzone Cloud Zone entity.""" @@ -49,6 +85,5 @@ class AirzoneZoneEntity(AirzoneEntity): """Return zone value by key.""" value = None if zone := self.coordinator.data[AZD_ZONES].get(self.zone_id): - if key in zone: - value = zone[key] + value = zone.get(key) return value diff --git a/homeassistant/components/airzone_cloud/sensor.py b/homeassistant/components/airzone_cloud/sensor.py index 0afabfcb715..faeb62576fd 100644 --- a/homeassistant/components/airzone_cloud/sensor.py +++ b/homeassistant/components/airzone_cloud/sensor.py @@ -3,7 +3,13 @@ from __future__ import annotations from typing import Any, Final -from aioairzone_cloud.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_ZONES +from aioairzone_cloud.const import ( + AZD_AIDOOS, + AZD_HUMIDITY, + AZD_NAME, + AZD_TEMP, + AZD_ZONES, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -18,7 +24,17 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator -from .entity import AirzoneEntity, AirzoneZoneEntity +from .entity import AirzoneAidooEntity, AirzoneEntity, AirzoneZoneEntity + +AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + device_class=SensorDeviceClass.TEMPERATURE, + key=AZD_TEMP, + name="Temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), +) ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( @@ -42,9 +58,25 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add Airzone Cloud sensors from a config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - sensors = [] + sensors: list[AirzoneSensor] = [] + + # Aidoos + for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items(): + for description in AIDOO_SENSOR_TYPES: + if description.key in aidoo_data: + sensors.append( + AirzoneAidooSensor( + coordinator, + description, + entry, + aidoo_id, + aidoo_data, + ) + ) + + # Zones for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items(): for description in ZONE_SENSOR_TYPES: if description.key in zone_data: @@ -76,6 +108,27 @@ class AirzoneSensor(AirzoneEntity, SensorEntity): self._attr_native_value = self.get_airzone_value(self.entity_description.key) +class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor): + """Define an Airzone Cloud Aidoo sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: SensorEntityDescription, + entry: ConfigEntry, + aidoo_id: str, + aidoo_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, aidoo_id, aidoo_data) + + self._attr_name = f"{aidoo_data[AZD_NAME]} {description.name}" + self._attr_unique_id = f"{entry.unique_id}_{aidoo_id}_{description.key}" + self.entity_description = description + + self._async_update_attrs() + + class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): """Define an Airzone Cloud Zone sensor.""" diff --git a/tests/components/airzone_cloud/test_config_flow.py b/tests/components/airzone_cloud/test_config_flow.py index 477511b5f20..ec031d4bf25 100644 --- a/tests/components/airzone_cloud/test_config_flow.py +++ b/tests/components/airzone_cloud/test_config_flow.py @@ -14,9 +14,9 @@ from .util import ( CONFIG, GET_INSTALLATION_MOCK, GET_INSTALLATIONS_MOCK, - GET_WEBSERVER_MOCK, WS_ID, mock_get_device_status, + mock_get_webserver, ) @@ -37,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: return_value=GET_INSTALLATIONS_MOCK, ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - return_value=GET_WEBSERVER_MOCK, + side_effect=mock_get_webserver, ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", return_value=None, @@ -98,7 +98,7 @@ async def test_installations_list_error(hass: HomeAssistant) -> None: side_effect=AirzoneCloudError, ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - return_value=GET_WEBSERVER_MOCK, + side_effect=mock_get_webserver, ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", return_value=None, diff --git a/tests/components/airzone_cloud/test_coordinator.py b/tests/components/airzone_cloud/test_coordinator.py index 3bea3869881..40b6c937ed2 100644 --- a/tests/components/airzone_cloud/test_coordinator.py +++ b/tests/components/airzone_cloud/test_coordinator.py @@ -14,8 +14,8 @@ from .util import ( CONFIG, GET_INSTALLATION_MOCK, GET_INSTALLATIONS_MOCK, - GET_WEBSERVER_MOCK, mock_get_device_status, + mock_get_webserver, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -42,7 +42,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: return_value=GET_INSTALLATIONS_MOCK, ) as mock_installations, patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - return_value=GET_WEBSERVER_MOCK, + side_effect=mock_get_webserver, ) as mock_webserver, patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", return_value=None, @@ -53,7 +53,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_device_status.assert_called() mock_installation.assert_awaited_once() mock_installations.assert_called_once() - mock_webserver.assert_called_once() + mock_webserver.assert_called() mock_device_status.reset_mock() mock_installation.reset_mock() diff --git a/tests/components/airzone_cloud/test_diagnostics.py b/tests/components/airzone_cloud/test_diagnostics.py index a007886c955..730ac27325a 100644 --- a/tests/components/airzone_cloud/test_diagnostics.py +++ b/tests/components/airzone_cloud/test_diagnostics.py @@ -7,6 +7,7 @@ from aioairzone_cloud.const import ( API_DEVICES, API_GROUPS, API_WS_ID, + AZD_AIDOOS, AZD_INSTALLATIONS, AZD_SYSTEMS, AZD_WEBSERVERS, @@ -109,6 +110,7 @@ async def test_config_entry_diagnostics( ) assert list(diag["coord_data"]) >= [ + AZD_AIDOOS, AZD_INSTALLATIONS, AZD_SYSTEMS, AZD_WEBSERVERS, diff --git a/tests/components/airzone_cloud/test_sensor.py b/tests/components/airzone_cloud/test_sensor.py index f8cbfbf2fde..9de134d718b 100644 --- a/tests/components/airzone_cloud/test_sensor.py +++ b/tests/components/airzone_cloud/test_sensor.py @@ -12,6 +12,10 @@ async def test_airzone_create_sensors( await async_init_integration(hass) + # Aidoos + state = hass.states.get("sensor.bron_temperature") + assert state.state == "21.0" + # Zones state = hass.states.get("sensor.dormitorio_temperature") assert state.state == "25.0" diff --git a/tests/components/airzone_cloud/util.py b/tests/components/airzone_cloud/util.py index 641a143bfde..044cf880a16 100644 --- a/tests/components/airzone_cloud/util.py +++ b/tests/components/airzone_cloud/util.py @@ -4,6 +4,7 @@ from typing import Any from unittest.mock import patch from aioairzone_cloud.const import ( + API_AZ_AIDOO, API_AZ_SYSTEM, API_AZ_ZONE, API_CELSIUS, @@ -38,6 +39,7 @@ from aioairzone_cloud.const import ( API_ZONE_NUMBER, ) from aioairzone_cloud.device import Device +from aioairzone_cloud.webserver import WebServer from homeassistant.components.airzone_cloud import DOMAIN from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME @@ -46,6 +48,7 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry WS_ID = "11:22:33:44:55:66" +WS_ID_AIDOO = "11:22:33:44:55:67" CONFIG = { CONF_ID: "inst1", @@ -88,6 +91,17 @@ GET_INSTALLATION_MOCK = { }, ], }, + { + API_NAME: "Aidoo Group", + API_DEVICES: [ + { + API_DEVICE_ID: "aidoo1", + API_NAME: "Bron", + API_TYPE: API_AZ_AIDOO, + API_WS_ID: WS_ID_AIDOO, + }, + ], + }, ], } @@ -98,6 +112,7 @@ GET_INSTALLATIONS_MOCK = { API_NAME: "House", API_WS_IDS: [ WS_ID, + WS_ID_AIDOO, ], }, ], @@ -120,10 +135,37 @@ GET_WEBSERVER_MOCK = { }, } +GET_WEBSERVER_MOCK_AIDOO = { + API_WS_TYPE: "ws_aidoo", + API_CONFIG: { + API_WS_FW: "3.13", + API_STAT_SSID: "Wifi", + API_STAT_CHANNEL: 1, + API_STAT_AP_MAC: "00:00:00:00:00:01", + }, + API_STATUS: { + API_IS_CONNECTED: True, + API_STAT_QUALITY: 4, + API_STAT_RSSI: -77, + API_CONNECTION_DATE: "2023-05-24 17:00:52 +0200", + API_DISCONNECTION_DATE: "2023-05-24 17:00:25 +0200", + }, +} + def mock_get_device_status(device: Device) -> dict[str, Any]: """Mock API device status.""" + if device.get_id() == "aidoo1": + return { + API_ERRORS: [], + API_IS_CONNECTED: True, + API_LOCAL_TEMP: { + API_CELSIUS: 21, + API_FAH: 70, + }, + API_WARNINGS: [], + } if device.get_id() == "system1": return { API_ERRORS: [], @@ -151,6 +193,15 @@ def mock_get_device_status(device: Device) -> dict[str, Any]: } +def mock_get_webserver(webserver: WebServer, devices: bool) -> dict[str, Any]: + """Mock API get webserver.""" + + if webserver.get_id() == WS_ID_AIDOO: + return GET_WEBSERVER_MOCK_AIDOO + + return GET_WEBSERVER_MOCK + + async def async_init_integration( hass: HomeAssistant, ) -> None: @@ -174,7 +225,7 @@ async def async_init_integration( return_value=GET_INSTALLATIONS_MOCK, ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver", - return_value=GET_WEBSERVER_MOCK, + side_effect=mock_get_webserver, ), patch( "homeassistant.components.airzone_cloud.AirzoneCloudApi.login", return_value=None,