Add Airzone Cloud integration (#93238)
* airzone-cloud: add new integration Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Add missing aioairzone-cloud to test requirements Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Update aioairzone-cloud to v0.0.4 Allows to handle TooManyRequests exception on coordinator. Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * aioairzone_cloud: reduce API requests Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: remove system_zone_id As opposed to the Local API of Airzone devices, the Cloud API provides unique IDs for both systems and zones, so we can remove the system_zone_id copied from the Local API integration. Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: minor improvements Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * tests: airzone_cloud: simplify mock_get_webserver Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Update aioairzone to v0.0.5 - Add token refresh and relogin support. - Improve fetching installation devices. Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: add to strict typing Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Update aioairzone to v0.0.7 Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * trigger CI * airzone_cloud: remove unneeded api_get_user call Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Add Airzone brand Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Update aioairzone to v0.1.1 Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: use unique_id instead of entry_id Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: remove special handling of TooManyRequests Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: bump coordinator timeout to 30s Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: make AirzoneEntity an ABC Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * airzone_cloud: fix strings typo Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * tests: airzone_cloud: simplify webserver mock Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com> * Update aioairzone-cloud to v0.1.2 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:
parent
8bf22014ce
commit
8edb253ace
22 changed files with 892 additions and 3 deletions
|
@ -49,6 +49,7 @@ homeassistant.components.air_quality.*
|
|||
homeassistant.components.airly.*
|
||||
homeassistant.components.airvisual.*
|
||||
homeassistant.components.airzone.*
|
||||
homeassistant.components.airzone_cloud.*
|
||||
homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
|
|
|
@ -59,6 +59,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/airvisual_pro/ @bachya
|
||||
/homeassistant/components/airzone/ @Noltari
|
||||
/tests/components/airzone/ @Noltari
|
||||
/homeassistant/components/airzone_cloud/ @Noltari
|
||||
/tests/components/airzone_cloud/ @Noltari
|
||||
/homeassistant/components/aladdin_connect/ @mkmer
|
||||
/tests/components/aladdin_connect/ @mkmer
|
||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||
|
|
5
homeassistant/brands/airzone.json
Normal file
5
homeassistant/brands/airzone.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"domain": "airzone",
|
||||
"name": "Airzone",
|
||||
"integrations": ["airzone", "airzone_cloud"]
|
||||
}
|
48
homeassistant/components/airzone_cloud/__init__.py
Normal file
48
homeassistant/components/airzone_cloud/__init__.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
"""The Airzone Cloud integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.common import ConnectionOptions
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Airzone Cloud from a config entry."""
|
||||
options = ConnectionOptions(
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
airzone = AirzoneCloudApi(aiohttp_client.async_get_clientsession(hass), options)
|
||||
await airzone.login()
|
||||
inst_list = await airzone.list_installations()
|
||||
for inst in inst_list:
|
||||
if inst.get_id() == entry.data[CONF_ID]:
|
||||
airzone.select_installation(inst)
|
||||
await airzone.update_installation(inst)
|
||||
|
||||
coordinator = AirzoneUpdateCoordinator(hass, airzone)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
116
homeassistant/components/airzone_cloud/config_flow.py
Normal file
116
homeassistant/components/airzone_cloud/config_flow.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
"""Config flow for Airzone Cloud."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.common import ConnectionOptions
|
||||
from aioairzone_cloud.const import AZD_ID, AZD_NAME, AZD_WEBSERVERS
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError, LoginError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow for an Airzone Cloud device."""
|
||||
|
||||
airzone: AirzoneCloudApi
|
||||
|
||||
async def async_step_inst_pick(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the installation selection."""
|
||||
errors = {}
|
||||
options: dict[str, str] = {}
|
||||
|
||||
inst_desc = None
|
||||
inst_id = None
|
||||
if user_input is not None:
|
||||
inst_id = user_input[CONF_ID]
|
||||
|
||||
try:
|
||||
inst_list = await self.airzone.list_installations()
|
||||
except AirzoneCloudError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
for inst in inst_list:
|
||||
_data = inst.data()
|
||||
_id = _data[AZD_ID]
|
||||
options[_id] = f"{_data[AZD_NAME]} {_data[AZD_WEBSERVERS][0]} ({_id})"
|
||||
if _id is not None and _id == inst_id:
|
||||
inst_desc = options[_id]
|
||||
|
||||
if user_input is not None and inst_desc is not None:
|
||||
await self.async_set_unique_id(inst_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
user_input[CONF_USERNAME] = self.airzone.options.username
|
||||
user_input[CONF_PASSWORD] = self.airzone.options.password
|
||||
|
||||
return self.async_create_entry(title=inst_desc, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ID): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(value=k, label=v)
|
||||
for k, v in options.items()
|
||||
],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
if CONF_ID in user_input:
|
||||
return await self.async_step_inst_pick(user_input)
|
||||
|
||||
self.airzone = AirzoneCloudApi(
|
||||
aiohttp_client.async_get_clientsession(self.hass),
|
||||
ConnectionOptions(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
await self.airzone.login()
|
||||
except (AirzoneCloudError, LoginError):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return await self.async_step_inst_pick()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
8
homeassistant/components/airzone_cloud/const.py
Normal file
8
homeassistant/components/airzone_cloud/const.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
"""Constants for the Airzone Cloud integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final[str] = "airzone_cloud"
|
||||
MANUFACTURER: Final[str] = "Airzone"
|
||||
|
||||
AIOAIRZONE_CLOUD_TIMEOUT_SEC: Final[int] = 30
|
43
homeassistant/components/airzone_cloud/coordinator.py
Normal file
43
homeassistant/components/airzone_cloud/coordinator.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
"""The Airzone Cloud integration coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import AIOAIRZONE_CLOUD_TIMEOUT_SEC, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Class to manage fetching data from the Airzone Cloud device."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airzone: AirzoneCloudApi) -> None:
|
||||
"""Initialize."""
|
||||
self.airzone = airzone
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
|
||||
try:
|
||||
await self.airzone.update()
|
||||
except AirzoneCloudError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
return self.airzone.data()
|
54
homeassistant/components/airzone_cloud/entity.py
Normal file
54
homeassistant/components/airzone_cloud/entity.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
"""Entity classes for the Airzone Cloud integration."""
|
||||
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 homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
|
||||
|
||||
class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator], ABC):
|
||||
"""Define an Airzone Cloud entity."""
|
||||
|
||||
@abstractmethod
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return Airzone Cloud entity value by key."""
|
||||
|
||||
|
||||
class AirzoneZoneEntity(AirzoneEntity):
|
||||
"""Define an Airzone Cloud Zone entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.system_id = zone_data[AZD_SYSTEM_ID]
|
||||
self.zone_id = zone_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{entry.unique_id}_{zone_id}")},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=zone_data[AZD_NAME],
|
||||
via_device=(DOMAIN, f"{entry.unique_id}_{self.system_id}"),
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""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]
|
||||
return value
|
10
homeassistant/components/airzone_cloud/manifest.json
Normal file
10
homeassistant/components/airzone_cloud/manifest.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"domain": "airzone_cloud",
|
||||
"name": "Airzone Cloud",
|
||||
"codeowners": ["@Noltari"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.1.2"]
|
||||
}
|
97
homeassistant/components/airzone_cloud/sensor.py
Normal file
97
homeassistant/components/airzone_cloud/sensor.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
"""Support for the Airzone Cloud sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone_cloud.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_ZONES
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneEntity, AirzoneZoneEntity
|
||||
|
||||
ZONE_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,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key=AZD_HUMIDITY,
|
||||
name="Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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]
|
||||
|
||||
sensors = []
|
||||
for zone_id, zone_data in coordinator.data[AZD_ZONES].items():
|
||||
for description in ZONE_SENSOR_TYPES:
|
||||
if description.key in zone_data:
|
||||
sensors.append(
|
||||
AirzoneZoneSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
zone_id,
|
||||
zone_data,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class AirzoneSensor(AirzoneEntity, SensorEntity):
|
||||
"""Define an Airzone Cloud sensor."""
|
||||
|
||||
@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 sensor attributes."""
|
||||
self._attr_native_value = self.get_airzone_value(self.entity_description.key)
|
||||
|
||||
|
||||
class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor):
|
||||
"""Define an Airzone Cloud Zone sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, zone_id, zone_data)
|
||||
|
||||
self._attr_name = f"{zone_data[AZD_NAME]} {description.name}"
|
||||
self._attr_unique_id = f"{entry.unique_id}_{zone_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
19
homeassistant/components/airzone_cloud/strings.json
Normal file
19
homeassistant/components/airzone_cloud/strings.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"id": "Installation",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ FLOWS = {
|
|||
"airvisual",
|
||||
"airvisual_pro",
|
||||
"airzone",
|
||||
"airzone_cloud",
|
||||
"aladdin_connect",
|
||||
"alarmdecoder",
|
||||
"amberelectric",
|
||||
|
|
|
@ -137,9 +137,20 @@
|
|||
},
|
||||
"airzone": {
|
||||
"name": "Airzone",
|
||||
"integrations": {
|
||||
"airzone": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
"iot_class": "local_polling",
|
||||
"name": "Airzone"
|
||||
},
|
||||
"airzone_cloud": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Airzone Cloud"
|
||||
}
|
||||
}
|
||||
},
|
||||
"aladdin_connect": {
|
||||
"name": "Aladdin Connect",
|
||||
|
|
10
mypy.ini
10
mypy.ini
|
@ -251,6 +251,16 @@ disallow_untyped_defs = true
|
|||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.airzone_cloud.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.aladdin_connect.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
|
|
@ -115,6 +115,9 @@ aio_georss_gdacs==0.8
|
|||
# homeassistant.components.airq
|
||||
aioairq==0.2.4
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.1.2
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.5.6
|
||||
|
||||
|
|
|
@ -105,6 +105,9 @@ aio_georss_gdacs==0.8
|
|||
# homeassistant.components.airq
|
||||
aioairq==0.2.4
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.1.2
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.5.6
|
||||
|
||||
|
|
1
tests/components/airzone_cloud/__init__.py
Normal file
1
tests/components/airzone_cloud/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Airzone Cloud integration."""
|
143
tests/components/airzone_cloud/test_config_flow.py
Normal file
143
tests/components/airzone_cloud/test_config_flow.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
"""Define tests for the Airzone Cloud config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError, LoginError
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.airzone_cloud.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .util import (
|
||||
CONFIG,
|
||||
GET_INSTALLATION_MOCK,
|
||||
GET_INSTALLATIONS_MOCK,
|
||||
GET_WEBSERVER_MOCK,
|
||||
WS_ID,
|
||||
mock_get_device_status,
|
||||
)
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the form is served with valid input."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone_cloud.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry, patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status",
|
||||
side_effect=mock_get_device_status,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation",
|
||||
return_value=GET_INSTALLATION_MOCK,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations",
|
||||
return_value=GET_INSTALLATIONS_MOCK,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver",
|
||||
return_value=GET_WEBSERVER_MOCK,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONFIG[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONFIG[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ID: CONFIG[CONF_ID],
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
conf_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
entry = conf_entries[0]
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"House {WS_ID} ({CONFIG[CONF_ID]})"
|
||||
assert result["data"][CONF_ID] == CONFIG[CONF_ID]
|
||||
assert result["data"][CONF_USERNAME] == CONFIG[CONF_USERNAME]
|
||||
assert result["data"][CONF_PASSWORD] == CONFIG[CONF_PASSWORD]
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_installations_list_error(hass: HomeAssistant) -> None:
|
||||
"""Test connection error."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone_cloud.async_setup_entry",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status",
|
||||
side_effect=mock_get_device_status,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations",
|
||||
side_effect=AirzoneCloudError,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver",
|
||||
return_value=GET_WEBSERVER_MOCK,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: CONFIG[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONFIG[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_login_error(hass: HomeAssistant) -> None:
|
||||
"""Test login error."""
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
|
||||
side_effect=LoginError,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={
|
||||
CONF_USERNAME: CONFIG[CONF_USERNAME],
|
||||
CONF_PASSWORD: CONFIG[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
70
tests/components/airzone_cloud/test_coordinator.py
Normal file
70
tests/components/airzone_cloud/test_coordinator.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
"""Define tests for the Airzone Cloud coordinator."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError
|
||||
|
||||
from homeassistant.components.airzone_cloud.const import DOMAIN
|
||||
from homeassistant.components.airzone_cloud.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .util import (
|
||||
CONFIG,
|
||||
GET_INSTALLATION_MOCK,
|
||||
GET_INSTALLATIONS_MOCK,
|
||||
GET_WEBSERVER_MOCK,
|
||||
mock_get_device_status,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:
|
||||
"""Test ClientConnectorError on coordinator update."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG,
|
||||
domain=DOMAIN,
|
||||
unique_id="airzone_cloud_unique_id",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status",
|
||||
side_effect=mock_get_device_status,
|
||||
) as mock_device_status, patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation",
|
||||
return_value=GET_INSTALLATION_MOCK,
|
||||
) as mock_installation, patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations",
|
||||
return_value=GET_INSTALLATIONS_MOCK,
|
||||
) as mock_installations, patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver",
|
||||
return_value=GET_WEBSERVER_MOCK,
|
||||
) as mock_webserver, patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
|
||||
return_value=None,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_device_status.assert_called()
|
||||
mock_installation.assert_awaited_once()
|
||||
mock_installations.assert_called_once()
|
||||
mock_webserver.assert_called_once()
|
||||
|
||||
mock_device_status.reset_mock()
|
||||
mock_installation.reset_mock()
|
||||
mock_installations.reset_mock()
|
||||
mock_webserver.reset_mock()
|
||||
|
||||
mock_device_status.side_effect = AirzoneCloudError
|
||||
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_device_status.assert_called()
|
||||
|
||||
state = hass.states.get("sensor.salon_temperature")
|
||||
assert state.state == STATE_UNAVAILABLE
|
43
tests/components/airzone_cloud/test_init.py
Normal file
43
tests/components/airzone_cloud/test_init.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
"""Define tests for the Airzone Cloud init."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.airzone_cloud.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .util import CONFIG
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant) -> None:
|
||||
"""Test unload."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG,
|
||||
domain=DOMAIN,
|
||||
unique_id="airzone_cloud_unique_id",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.list_installations",
|
||||
return_value=[],
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.update_installation",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.update",
|
||||
return_value=None,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
26
tests/components/airzone_cloud/test_sensor.py
Normal file
26
tests/components/airzone_cloud/test_sensor.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
"""The sensor tests for the Airzone Cloud platform."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .util import async_init_integration
|
||||
|
||||
|
||||
async def test_airzone_create_sensors(
|
||||
hass: HomeAssistant, entity_registry_enabled_by_default: None
|
||||
) -> None:
|
||||
"""Test creation of sensors."""
|
||||
|
||||
await async_init_integration(hass)
|
||||
|
||||
# Zones
|
||||
state = hass.states.get("sensor.dormitorio_temperature")
|
||||
assert state.state == "25.0"
|
||||
|
||||
state = hass.states.get("sensor.dormitorio_humidity")
|
||||
assert state.state == "24"
|
||||
|
||||
state = hass.states.get("sensor.salon_temperature")
|
||||
assert state.state == "20.0"
|
||||
|
||||
state = hass.states.get("sensor.salon_humidity")
|
||||
assert state.state == "30"
|
175
tests/components/airzone_cloud/util.py
Normal file
175
tests/components/airzone_cloud/util.py
Normal file
|
@ -0,0 +1,175 @@
|
|||
"""Tests for the Airzone integration."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioairzone_cloud.const import (
|
||||
API_AZ_SYSTEM,
|
||||
API_AZ_ZONE,
|
||||
API_CELSIUS,
|
||||
API_CONFIG,
|
||||
API_CONNECTION_DATE,
|
||||
API_DEVICE_ID,
|
||||
API_DEVICES,
|
||||
API_DISCONNECTION_DATE,
|
||||
API_FAH,
|
||||
API_GROUPS,
|
||||
API_HUMIDITY,
|
||||
API_INSTALLATION_ID,
|
||||
API_INSTALLATIONS,
|
||||
API_IS_CONNECTED,
|
||||
API_LOCAL_TEMP,
|
||||
API_META,
|
||||
API_NAME,
|
||||
API_STAT_AP_MAC,
|
||||
API_STAT_CHANNEL,
|
||||
API_STAT_QUALITY,
|
||||
API_STAT_SSID,
|
||||
API_STATUS,
|
||||
API_SYSTEM_NUMBER,
|
||||
API_TYPE,
|
||||
API_WS_FW,
|
||||
API_WS_ID,
|
||||
API_WS_IDS,
|
||||
API_WS_TYPE,
|
||||
API_ZONE_NUMBER,
|
||||
)
|
||||
from aioairzone_cloud.device import Device
|
||||
|
||||
from homeassistant.components.airzone_cloud import DOMAIN
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
WS_ID = "11:22:33:44:55:66"
|
||||
|
||||
CONFIG = {
|
||||
CONF_ID: "inst1",
|
||||
CONF_USERNAME: "user",
|
||||
CONF_PASSWORD: "pass",
|
||||
}
|
||||
|
||||
GET_INSTALLATION_MOCK = {
|
||||
API_GROUPS: [
|
||||
{
|
||||
API_NAME: "Group",
|
||||
API_DEVICES: [
|
||||
{
|
||||
API_DEVICE_ID: "system1",
|
||||
API_TYPE: API_AZ_SYSTEM,
|
||||
API_META: {
|
||||
API_SYSTEM_NUMBER: 1,
|
||||
},
|
||||
API_WS_ID: WS_ID,
|
||||
},
|
||||
{
|
||||
API_DEVICE_ID: "zone1",
|
||||
API_NAME: "Salon",
|
||||
API_TYPE: API_AZ_ZONE,
|
||||
API_META: {
|
||||
API_SYSTEM_NUMBER: 1,
|
||||
API_ZONE_NUMBER: 1,
|
||||
},
|
||||
API_WS_ID: WS_ID,
|
||||
},
|
||||
{
|
||||
API_DEVICE_ID: "zone2",
|
||||
API_NAME: "Dormitorio",
|
||||
API_TYPE: API_AZ_ZONE,
|
||||
API_META: {
|
||||
API_SYSTEM_NUMBER: 1,
|
||||
API_ZONE_NUMBER: 2,
|
||||
},
|
||||
API_WS_ID: WS_ID,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
GET_INSTALLATIONS_MOCK = {
|
||||
API_INSTALLATIONS: [
|
||||
{
|
||||
API_INSTALLATION_ID: CONFIG[CONF_ID],
|
||||
API_NAME: "House",
|
||||
API_WS_IDS: [
|
||||
WS_ID,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
GET_WEBSERVER_MOCK = {
|
||||
API_WS_TYPE: "ws_az",
|
||||
API_CONFIG: {
|
||||
API_WS_FW: "3.44",
|
||||
API_STAT_SSID: "Wifi",
|
||||
API_STAT_CHANNEL: 36,
|
||||
API_STAT_AP_MAC: "00:00:00:00:00:00",
|
||||
},
|
||||
API_STATUS: {
|
||||
API_IS_CONNECTED: True,
|
||||
API_STAT_QUALITY: 4,
|
||||
API_CONNECTION_DATE: "2023-05-07T12:55:51.000Z",
|
||||
API_DISCONNECTION_DATE: "2023-01-01T22:26:55.376Z",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def mock_get_device_status(device: Device) -> dict[str, Any]:
|
||||
"""Mock API device status."""
|
||||
|
||||
if device.get_id() == "system1":
|
||||
return {
|
||||
API_IS_CONNECTED: True,
|
||||
}
|
||||
if device.get_id() == "zone2":
|
||||
return {
|
||||
API_HUMIDITY: 24,
|
||||
API_IS_CONNECTED: True,
|
||||
API_LOCAL_TEMP: {
|
||||
API_FAH: 77,
|
||||
API_CELSIUS: 25,
|
||||
},
|
||||
}
|
||||
return {
|
||||
API_HUMIDITY: 30,
|
||||
API_IS_CONNECTED: True,
|
||||
API_LOCAL_TEMP: {
|
||||
API_FAH: 68,
|
||||
API_CELSIUS: 20,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_init_integration(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Set up the Airzone integration in Home Assistant."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG,
|
||||
domain=DOMAIN,
|
||||
unique_id="airzone_cloud_unique_id",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_device_status",
|
||||
side_effect=mock_get_device_status,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installation",
|
||||
return_value=GET_INSTALLATION_MOCK,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_installations",
|
||||
return_value=GET_INSTALLATIONS_MOCK,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.api_get_webserver",
|
||||
return_value=GET_WEBSERVER_MOCK,
|
||||
), patch(
|
||||
"homeassistant.components.airzone_cloud.AirzoneCloudApi.login",
|
||||
return_value=None,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
Loading…
Add table
Add a link
Reference in a new issue