From 4e494aa393b93e433739ac69b3982343c8d8478b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Wed, 22 Jan 2025 18:41:58 +0100 Subject: [PATCH] Allow multiple Airzone entries with different System IDs (#135397) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/airzone/__init__.py | 24 ++++++++- .../components/airzone/config_flow.py | 9 +++- .../airzone/snapshots/test_diagnostics.ambr | 3 +- tests/components/airzone/test_config_flow.py | 14 +++--- tests/components/airzone/test_coordinator.py | 2 + tests/components/airzone/test_init.py | 50 ++++++++++++++++++- tests/components/airzone/util.py | 11 +++- 7 files changed, 100 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 39e4f73aa38..aa168dce858 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> b options = ConnectionOptions( entry.data[CONF_HOST], entry.data[CONF_PORT], - entry.data.get(CONF_ID, DEFAULT_SYSTEM_ID), + entry.data[CONF_ID], ) airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options) @@ -120,3 +120,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool: + """Migrate an old entry.""" + if entry.version == 1 and entry.minor_version < 2: + # Add missing CONF_ID + system_id = entry.data.get(CONF_ID, DEFAULT_SYSTEM_ID) + new_data = entry.data.copy() + new_data[CONF_ID] = system_id + hass.config_entries.async_update_entry( + entry, + data=new_data, + minor_version=2, + ) + + _LOGGER.info( + "Migration to configuration version %s.%s successful", + entry.version, + entry.minor_version, + ) + + return True diff --git a/homeassistant/components/airzone/config_flow.py b/homeassistant/components/airzone/config_flow.py index b0a87dd4e57..c4088e950e9 100644 --- a/homeassistant/components/airzone/config_flow.py +++ b/homeassistant/components/airzone/config_flow.py @@ -44,6 +44,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN): _discovered_ip: str | None = None _discovered_mac: str | None = None + MINOR_VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -53,6 +54,9 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: + if CONF_ID not in user_input: + user_input[CONF_ID] = DEFAULT_SYSTEM_ID + self._async_abort_entries_match(user_input) airzone = AirzoneLocalApi( @@ -60,7 +64,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN): ConnectionOptions( user_input[CONF_HOST], user_input[CONF_PORT], - user_input.get(CONF_ID, DEFAULT_SYSTEM_ID), + user_input[CONF_ID], ), ) @@ -84,6 +88,9 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN): ) title = f"Airzone {user_input[CONF_HOST]}:{user_input[CONF_PORT]}" + if user_input[CONF_ID] != DEFAULT_SYSTEM_ID: + title += f" #{user_input[CONF_ID]}" + return self.async_create_entry(title=title, data=user_input) return self.async_show_form( diff --git a/tests/components/airzone/snapshots/test_diagnostics.ambr b/tests/components/airzone/snapshots/test_diagnostics.ambr index bb44a0abeb1..0c3c0ba7c7a 100644 --- a/tests/components/airzone/snapshots/test_diagnostics.ambr +++ b/tests/components/airzone/snapshots/test_diagnostics.ambr @@ -275,6 +275,7 @@ 'config_entry': dict({ 'data': dict({ 'host': '192.168.1.100', + 'id': 0, 'port': 3000, }), 'disabled_by': None, @@ -282,7 +283,7 @@ }), 'domain': 'airzone', 'entry_id': '6e7a0798c1734ba81d26ced0e690eaec', - 'minor_version': 1, + 'minor_version': 2, 'options': dict({ }), 'pref_disable_new_entities': False, diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 9bc0a8cedbd..65897c6da7e 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -28,6 +28,7 @@ from .util import ( HVAC_MOCK, HVAC_VERSION_MOCK, HVAC_WEBSERVER_MOCK, + USER_INPUT, ) from tests.common import MockConfigEntry @@ -81,7 +82,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"], USER_INPUT ) await hass.async_block_till_done() @@ -94,7 +95,7 @@ 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 result["data"][CONF_ID] == CONFIG[CONF_ID] assert len(mock_setup_entry.mock_calls) == 1 @@ -129,7 +130,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: ), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) assert result["type"] is FlowResultType.FORM @@ -154,7 +155,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert ( result["title"] - == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}" + == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]} #{CONFIG_ID1[CONF_ID]}" ) assert result["data"][CONF_HOST] == CONFIG_ID1[CONF_HOST] assert result["data"][CONF_PORT] == CONFIG_ID1[CONF_PORT] @@ -167,6 +168,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: """Test setting up duplicated entry.""" config_entry = MockConfigEntry( + minor_version=2, data=CONFIG, domain=DOMAIN, unique_id="airzone_unique_id", @@ -174,7 +176,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) assert result["type"] is FlowResultType.ABORT @@ -189,7 +191,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: side_effect=AirzoneError, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/airzone/test_coordinator.py b/tests/components/airzone/test_coordinator.py index 583758a6bee..fcdcad6a32a 100644 --- a/tests/components/airzone/test_coordinator.py +++ b/tests/components/airzone/test_coordinator.py @@ -25,6 +25,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: """Test ClientConnectorError on coordinator update.""" config_entry = MockConfigEntry( + minor_version=2, data=CONFIG, domain=DOMAIN, unique_id="airzone_unique_id", @@ -74,6 +75,7 @@ async def test_coordinator_new_devices( """Test new devices on coordinator update.""" config_entry = MockConfigEntry( + minor_version=2, data=CONFIG, domain=DOMAIN, unique_id="airzone_unique_id", diff --git a/tests/components/airzone/test_init.py b/tests/components/airzone/test_init.py index 293fc75acb5..a2783cb7c2f 100644 --- a/tests/components/airzone/test_init.py +++ b/tests/components/airzone/test_init.py @@ -2,14 +2,16 @@ from unittest.mock import patch +from aioairzone.const import DEFAULT_SYSTEM_ID from aioairzone.exceptions import HotWaterNotAvailable, InvalidMethod, SystemOutOfRange from homeassistant.components.airzone.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .util import CONFIG, HVAC_MOCK, HVAC_VERSION_MOCK, HVAC_WEBSERVER_MOCK +from .util import CONFIG, HVAC_MOCK, HVAC_VERSION_MOCK, HVAC_WEBSERVER_MOCK, USER_INPUT from tests.common import MockConfigEntry @@ -19,7 +21,11 @@ async def test_unique_id_migrate( ) -> None: """Test unique id migration.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG) + config_entry = MockConfigEntry( + minor_version=2, + domain=DOMAIN, + data=CONFIG, + ) config_entry.add_to_hass(hass) with ( @@ -89,6 +95,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: """Test unload.""" config_entry = MockConfigEntry( + minor_version=2, data=CONFIG, domain=DOMAIN, unique_id="airzone_unique_id", @@ -112,3 +119,42 @@ async def test_unload_entry(hass: HomeAssistant) -> None: await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_migrate_entry_v2(hass: HomeAssistant) -> None: + """Test entry migration to v2.""" + + config_entry = MockConfigEntry( + minor_version=1, + data=USER_INPUT, + domain=DOMAIN, + ) + config_entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_dhw", + side_effect=HotWaterNotAvailable, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac", + return_value=HVAC_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", + side_effect=SystemOutOfRange, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_version", + return_value=HVAC_VERSION_MOCK, + ), + patch( + "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", + side_effect=InvalidMethod, + ), + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.minor_version == 2 + assert config_entry.data.get(CONF_ID) == DEFAULT_SYSTEM_ID diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index b51dfb890e4..50d1964924d 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -55,6 +55,7 @@ from aioairzone.const import ( API_WS_AZ, API_WS_TYPE, API_ZONE_ID, + DEFAULT_SYSTEM_ID, ) from homeassistant.components.airzone.const import DOMAIN @@ -63,13 +64,18 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -CONFIG = { +USER_INPUT = { CONF_HOST: "192.168.1.100", CONF_PORT: 3000, } +CONFIG = { + **USER_INPUT, + CONF_ID: DEFAULT_SYSTEM_ID, +} + CONFIG_ID1 = { - **CONFIG, + **USER_INPUT, CONF_ID: 1, } @@ -359,6 +365,7 @@ async def async_init_integration( """Set up the Airzone integration in Home Assistant.""" config_entry = MockConfigEntry( + minor_version=2, data=CONFIG, entry_id="6e7a0798c1734ba81d26ced0e690eaec", domain=DOMAIN,