Support specifying Airzone System ID (#69751)

This commit is contained in:
Álvaro Fernández Rojas 2022-04-13 19:12:21 +02:00 committed by GitHub
parent 00621617c2
commit c76b21e24e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 122 additions and 32 deletions

View File

@ -15,13 +15,13 @@ from aioairzone.const import (
from aioairzone.localapi import AirzoneLocalApi
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .const import DEFAULT_SYSTEM_ID, DOMAIN, MANUFACTURER
from .coordinator import AirzoneUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR]
@ -67,6 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
options = ConnectionOptions(
entry.data[CONF_HOST],
entry.data[CONF_PORT],
entry.data.get(CONF_ID, DEFAULT_SYSTEM_ID),
)
airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options)

View File

@ -4,17 +4,30 @@ from __future__ import annotations
from typing import Any
from aioairzone.common import ConnectionOptions
from aioairzone.exceptions import InvalidHost
from aioairzone.exceptions import AirzoneError, InvalidSystem
from aioairzone.localapi import AirzoneLocalApi
from aiohttp.client_exceptions import ClientConnectorError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import DEFAULT_LOCAL_API_PORT, DOMAIN
from .const import DEFAULT_LOCAL_API_PORT, DEFAULT_SYSTEM_ID, DOMAIN
CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_LOCAL_API_PORT): int,
}
)
SYSTEM_ID_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_LOCAL_API_PORT): int,
vol.Required(CONF_ID, default=1): int,
}
)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -24,13 +37,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
data_schema = CONFIG_SCHEMA
errors = {}
if user_input is not None:
system_id = user_input.get(CONF_ID, DEFAULT_SYSTEM_ID)
self._async_abort_entries_match(
{
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
CONF_ID: system_id,
}
)
@ -39,12 +56,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
ConnectionOptions(
user_input[CONF_HOST],
user_input[CONF_PORT],
system_id,
),
)
try:
await airzone.validate_airzone()
except (ClientConnectorError, InvalidHost):
except InvalidSystem:
data_schema = SYSTEM_ID_SCHEMA
errors["base"] = "invalid_system_id"
except AirzoneError:
errors["base"] = "cannot_connect"
else:
title = f"Airzone {user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
@ -52,11 +73,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_LOCAL_API_PORT): int,
}
),
data_schema=data_schema,
errors=errors,
)

View File

@ -12,6 +12,7 @@ MANUFACTURER: Final = "Airzone"
AIOAIRZONE_DEVICE_TIMEOUT_SEC: Final = 10
API_TEMPERATURE_STEP: Final = 0.5
DEFAULT_LOCAL_API_PORT: Final = 3000
DEFAULT_SYSTEM_ID: Final = 0
TEMP_UNIT_LIB_TO_HASS: Final[dict[TemperatureUnit, str]] = {
TemperatureUnit.CELSIUS: TEMP_CELSIUS,

View File

@ -4,8 +4,8 @@ from __future__ import annotations
from datetime import timedelta
import logging
from aioairzone.exceptions import AirzoneError
from aioairzone.localapi import AirzoneLocalApi
from aiohttp.client_exceptions import ClientConnectorError
import async_timeout
from homeassistant.core import HomeAssistant
@ -37,6 +37,6 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator):
async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC):
try:
await self.airzone.update_airzone()
except ClientConnectorError as error:
except AirzoneError as error:
raise UpdateFailed(error) from error
return self.airzone.data()

View File

@ -3,7 +3,7 @@
"name": "Airzone",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone",
"requirements": ["aioairzone==0.3.3"],
"requirements": ["aioairzone==0.3.6"],
"codeowners": ["@Noltari"],
"iot_class": "local_polling",
"loggers": ["aioairzone"]

View File

@ -4,7 +4,8 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_system_id": "Invalid Airzone System ID"
},
"step": {
"user": {

View File

@ -4,7 +4,8 @@
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect"
"cannot_connect": "Failed to connect",
"invalid_system_id": "Invalid Airzone System ID"
},
"step": {
"user": {

View File

@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4
aio_georss_gdacs==0.7
# homeassistant.components.airzone
aioairzone==0.3.3
aioairzone==0.3.6
# homeassistant.components.ambient_station
aioambient==2021.11.0

View File

@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4
aio_georss_gdacs==0.7
# homeassistant.components.airzone
aioairzone==0.3.3
aioairzone==0.3.6
# homeassistant.components.ambient_station
aioambient==2021.11.0

View File

@ -1,16 +1,22 @@
"""Define tests for the Airzone config flow."""
from unittest.mock import MagicMock, patch
from unittest.mock import patch
from aiohttp.client_exceptions import ClientConnectorError, ClientResponseError
from aioairzone.const import API_SYSTEMS
from aioairzone.exceptions import (
AirzoneError,
InvalidMethod,
InvalidSystem,
SystemOutOfRange,
)
from homeassistant import data_entry_flow
from homeassistant.components.airzone.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
from homeassistant.core import HomeAssistant
from .util import CONFIG, HVAC_MOCK
from .util import CONFIG, CONFIG_ID1, CONFIG_NO_ID, HVAC_MOCK
from tests.common import MockConfigEntry
@ -26,10 +32,10 @@ async def test_form(hass: HomeAssistant) -> None:
return_value=HVAC_MOCK,
), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems",
side_effect=ClientResponseError(MagicMock(), MagicMock()),
side_effect=SystemOutOfRange,
), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
side_effect=ClientResponseError(MagicMock(), MagicMock()),
side_effect=InvalidMethod,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
@ -40,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], CONFIG
result["flow_id"], CONFIG_NO_ID
)
await hass.async_block_till_done()
@ -53,10 +59,62 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}"
assert result["data"][CONF_HOST] == CONFIG[CONF_HOST]
assert result["data"][CONF_PORT] == CONFIG[CONF_PORT]
assert CONF_ID not in result["data"]
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_system_id(hass: HomeAssistant) -> None:
"""Test Invalid System ID 0."""
with patch(
"homeassistant.components.airzone.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac",
side_effect=InvalidSystem,
) as mock_hvac, patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems",
side_effect=SystemOutOfRange,
), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
side_effect=InvalidMethod,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_NO_ID
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
assert result["errors"] == {"base": "invalid_system_id"}
mock_hvac.return_value = HVAC_MOCK[API_SYSTEMS][0]
mock_hvac.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], CONFIG_ID1
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
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.RESULT_TYPE_CREATE_ENTRY
assert (
result["title"]
== f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}"
)
assert result["data"][CONF_HOST] == CONFIG_ID1[CONF_HOST]
assert result["data"][CONF_PORT] == CONFIG_ID1[CONF_PORT]
assert result["data"][CONF_ID] == CONFIG_ID1[CONF_ID]
mock_setup_entry.assert_called_once()
async def test_form_duplicated_id(hass: HomeAssistant) -> None:
"""Test setting up duplicated entry."""
@ -80,7 +138,7 @@ async def test_connection_error(hass: HomeAssistant):
with patch(
"homeassistant.components.airzone.AirzoneLocalApi.validate_airzone",
side_effect=ClientConnectorError(MagicMock(), MagicMock()),
side_effect=AirzoneError,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG

View File

@ -1,8 +1,8 @@
"""Define tests for the Airzone coordinator."""
from unittest.mock import MagicMock, patch
from unittest.mock import patch
from aiohttp import ClientConnectorError
from aioairzone.exceptions import AirzoneError
from homeassistant.components.airzone.const import DOMAIN
from homeassistant.components.airzone.coordinator import SCAN_INTERVAL
@ -30,7 +30,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:
mock_hvac.assert_called_once()
mock_hvac.reset_mock()
mock_hvac.side_effect = ClientConnectorError(MagicMock(), MagicMock())
mock_hvac.side_effect = AirzoneError
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
mock_hvac.assert_called_once()

View File

@ -27,7 +27,7 @@ from aioairzone.const import (
)
from homeassistant.components.airzone import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@ -35,6 +35,18 @@ from tests.common import MockConfigEntry
CONFIG = {
CONF_HOST: "192.168.1.100",
CONF_PORT: 3000,
CONF_ID: 0,
}
CONFIG_NO_ID = {
CONF_HOST: CONFIG[CONF_HOST],
CONF_PORT: CONFIG[CONF_PORT],
}
CONFIG_ID1 = {
CONF_HOST: CONFIG[CONF_HOST],
CONF_PORT: CONFIG[CONF_PORT],
CONF_ID: 1,
}
HVAC_MOCK = {