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 aioairzone.localapi import AirzoneLocalApi
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER from .const import DEFAULT_SYSTEM_ID, DOMAIN, MANUFACTURER
from .coordinator import AirzoneUpdateCoordinator from .coordinator import AirzoneUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] 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( options = ConnectionOptions(
entry.data[CONF_HOST], entry.data[CONF_HOST],
entry.data[CONF_PORT], entry.data[CONF_PORT],
entry.data.get(CONF_ID, DEFAULT_SYSTEM_ID),
) )
airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options) airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options)

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,8 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}, },
"error": { "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": { "step": {
"user": { "user": {

View File

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

View File

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

View File

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

View File

@ -1,16 +1,22 @@
"""Define tests for the Airzone config flow.""" """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 import data_entry_flow
from homeassistant.components.airzone.const import DOMAIN from homeassistant.components.airzone.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState 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 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 from tests.common import MockConfigEntry
@ -26,10 +32,10 @@ async def test_form(hass: HomeAssistant) -> None:
return_value=HVAC_MOCK, return_value=HVAC_MOCK,
), patch( ), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems",
side_effect=ClientResponseError(MagicMock(), MagicMock()), side_effect=SystemOutOfRange,
), patch( ), patch(
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver", "homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
side_effect=ClientResponseError(MagicMock(), MagicMock()), side_effect=InvalidMethod,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
@ -40,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None:
assert result["errors"] == {} assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], CONFIG result["flow_id"], CONFIG_NO_ID
) )
await hass.async_block_till_done() 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["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}"
assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] assert result["data"][CONF_HOST] == CONFIG[CONF_HOST]
assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] assert result["data"][CONF_PORT] == CONFIG[CONF_PORT]
assert CONF_ID not in result["data"]
assert len(mock_setup_entry.mock_calls) == 1 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: async def test_form_duplicated_id(hass: HomeAssistant) -> None:
"""Test setting up duplicated entry.""" """Test setting up duplicated entry."""
@ -80,7 +138,7 @@ async def test_connection_error(hass: HomeAssistant):
with patch( with patch(
"homeassistant.components.airzone.AirzoneLocalApi.validate_airzone", "homeassistant.components.airzone.AirzoneLocalApi.validate_airzone",
side_effect=ClientConnectorError(MagicMock(), MagicMock()), side_effect=AirzoneError,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG DOMAIN, context={"source": SOURCE_USER}, data=CONFIG

View File

@ -1,8 +1,8 @@
"""Define tests for the Airzone coordinator.""" """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.const import DOMAIN
from homeassistant.components.airzone.coordinator import SCAN_INTERVAL 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.assert_called_once()
mock_hvac.reset_mock() mock_hvac.reset_mock()
mock_hvac.side_effect = ClientConnectorError(MagicMock(), MagicMock()) mock_hvac.side_effect = AirzoneError
async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_hvac.assert_called_once() mock_hvac.assert_called_once()

View File

@ -27,7 +27,7 @@ from aioairzone.const import (
) )
from homeassistant.components.airzone import DOMAIN 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 homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -35,6 +35,18 @@ from tests.common import MockConfigEntry
CONFIG = { CONFIG = {
CONF_HOST: "192.168.1.100", CONF_HOST: "192.168.1.100",
CONF_PORT: 3000, 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 = { HVAC_MOCK = {