From c76b21e24ee959c66e9bdec4d626d2d2bf81e2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 13 Apr 2022 19:12:21 +0200 Subject: [PATCH] Support specifying Airzone System ID (#69751) --- homeassistant/components/airzone/__init__.py | 5 +- .../components/airzone/config_flow.py | 38 +++++++--- homeassistant/components/airzone/const.py | 1 + .../components/airzone/coordinator.py | 4 +- .../components/airzone/manifest.json | 2 +- homeassistant/components/airzone/strings.json | 3 +- .../components/airzone/translations/en.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_config_flow.py | 74 +++++++++++++++++-- tests/components/airzone/test_coordinator.py | 6 +- tests/components/airzone/util.py | 14 +++- 12 files changed, 122 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 6952ac2a42d..0070d76cc39 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -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) diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index 57dde088820..7fca83456ee 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -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, ) diff --git a/homeassistant/components/airzone/const.py b/homeassistant/components/airzone/const.py index 094e2319476..a8fcae18cef 100644 --- a/homeassistant/components/airzone/const.py +++ b/homeassistant/components/airzone/const.py @@ -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, diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index f81b75b42bf..2a3af6bc532 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -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() diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 821f564f176..eff6a5af90f 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -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"] diff --git a/homeassistant/components/airzone/strings.json b/homeassistant/components/airzone/strings.json index 7de25789922..855b5615482 100644 --- a/homeassistant/components/airzone/strings.json +++ b/homeassistant/components/airzone/strings.json @@ -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": { diff --git a/homeassistant/components/airzone/translations/en.json b/homeassistant/components/airzone/translations/en.json index b24e62fa34e..85da81afd55 100644 --- a/homeassistant/components/airzone/translations/en.json +++ b/homeassistant/components/airzone/translations/en.json @@ -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": { diff --git a/requirements_all.txt b/requirements_all.txt index 2e18bee8c84..025e6147b28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 739b1eaf5fd..c74ff1b188c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 8ffe10167ea..97103e37f7c 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -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 diff --git a/tests/components/airzone/test_coordinator.py b/tests/components/airzone/test_coordinator.py index 179e94355c0..d9da16a1148 100644 --- a/tests/components/airzone/test_coordinator.py +++ b/tests/components/airzone/test_coordinator.py @@ -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() diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index f533870550d..5ad569a397b 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -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 = {