Allow multiple Airzone entries with different System IDs (#135397)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Álvaro Fernández Rojas 2025-01-22 18:41:58 +01:00 committed by GitHub
parent 3bbd7daa7f
commit 4e494aa393
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 100 additions and 13 deletions

View File

@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> b
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), entry.data[CONF_ID],
) )
airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options) 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: async def async_unload_entry(hass: HomeAssistant, entry: AirzoneConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 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

View File

@ -44,6 +44,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
_discovered_ip: str | None = None _discovered_ip: str | None = None
_discovered_mac: str | None = None _discovered_mac: str | None = None
MINOR_VERSION = 2
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -53,6 +54,9 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is not None: 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) self._async_abort_entries_match(user_input)
airzone = AirzoneLocalApi( airzone = AirzoneLocalApi(
@ -60,7 +64,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
ConnectionOptions( ConnectionOptions(
user_input[CONF_HOST], user_input[CONF_HOST],
user_input[CONF_PORT], 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]}" 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_create_entry(title=title, data=user_input)
return self.async_show_form( return self.async_show_form(

View File

@ -275,6 +275,7 @@
'config_entry': dict({ 'config_entry': dict({
'data': dict({ 'data': dict({
'host': '192.168.1.100', 'host': '192.168.1.100',
'id': 0,
'port': 3000, 'port': 3000,
}), }),
'disabled_by': None, 'disabled_by': None,
@ -282,7 +283,7 @@
}), }),
'domain': 'airzone', 'domain': 'airzone',
'entry_id': '6e7a0798c1734ba81d26ced0e690eaec', 'entry_id': '6e7a0798c1734ba81d26ced0e690eaec',
'minor_version': 1, 'minor_version': 2,
'options': dict({ 'options': dict({
}), }),
'pref_disable_new_entities': False, 'pref_disable_new_entities': False,

View File

@ -28,6 +28,7 @@ from .util import (
HVAC_MOCK, HVAC_MOCK,
HVAC_VERSION_MOCK, HVAC_VERSION_MOCK,
HVAC_WEBSERVER_MOCK, HVAC_WEBSERVER_MOCK,
USER_INPUT,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -81,7 +82,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"], USER_INPUT
) )
await hass.async_block_till_done() 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["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 result["data"][CONF_ID] == CONFIG[CONF_ID]
assert len(mock_setup_entry.mock_calls) == 1 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( 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 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["type"] is FlowResultType.CREATE_ENTRY
assert ( assert (
result["title"] 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_HOST] == CONFIG_ID1[CONF_HOST]
assert result["data"][CONF_PORT] == CONFIG_ID1[CONF_PORT] 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.""" """Test setting up duplicated entry."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
minor_version=2,
data=CONFIG, data=CONFIG,
domain=DOMAIN, domain=DOMAIN,
unique_id="airzone_unique_id", unique_id="airzone_unique_id",
@ -174,7 +176,7 @@ async def test_form_duplicated_id(hass: HomeAssistant) -> None:
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
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=USER_INPUT
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
@ -189,7 +191,7 @@ async def test_connection_error(hass: HomeAssistant) -> None:
side_effect=AirzoneError, 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=USER_INPUT
) )
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}

View File

@ -25,6 +25,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:
"""Test ClientConnectorError on coordinator update.""" """Test ClientConnectorError on coordinator update."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
minor_version=2,
data=CONFIG, data=CONFIG,
domain=DOMAIN, domain=DOMAIN,
unique_id="airzone_unique_id", unique_id="airzone_unique_id",
@ -74,6 +75,7 @@ async def test_coordinator_new_devices(
"""Test new devices on coordinator update.""" """Test new devices on coordinator update."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
minor_version=2,
data=CONFIG, data=CONFIG,
domain=DOMAIN, domain=DOMAIN,
unique_id="airzone_unique_id", unique_id="airzone_unique_id",

View File

@ -2,14 +2,16 @@
from unittest.mock import patch from unittest.mock import patch
from aioairzone.const import DEFAULT_SYSTEM_ID
from aioairzone.exceptions import HotWaterNotAvailable, InvalidMethod, SystemOutOfRange from aioairzone.exceptions import HotWaterNotAvailable, InvalidMethod, SystemOutOfRange
from homeassistant.components.airzone.const import DOMAIN from homeassistant.components.airzone.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er 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 from tests.common import MockConfigEntry
@ -19,7 +21,11 @@ async def test_unique_id_migrate(
) -> None: ) -> None:
"""Test unique id migration.""" """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) config_entry.add_to_hass(hass)
with ( with (
@ -89,6 +95,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
"""Test unload.""" """Test unload."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
minor_version=2,
data=CONFIG, data=CONFIG,
domain=DOMAIN, domain=DOMAIN,
unique_id="airzone_unique_id", 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.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED 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

View File

@ -55,6 +55,7 @@ from aioairzone.const import (
API_WS_AZ, API_WS_AZ,
API_WS_TYPE, API_WS_TYPE,
API_ZONE_ID, API_ZONE_ID,
DEFAULT_SYSTEM_ID,
) )
from homeassistant.components.airzone.const import DOMAIN from homeassistant.components.airzone.const import DOMAIN
@ -63,13 +64,18 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
CONFIG = { USER_INPUT = {
CONF_HOST: "192.168.1.100", CONF_HOST: "192.168.1.100",
CONF_PORT: 3000, CONF_PORT: 3000,
} }
CONFIG = {
**USER_INPUT,
CONF_ID: DEFAULT_SYSTEM_ID,
}
CONFIG_ID1 = { CONFIG_ID1 = {
**CONFIG, **USER_INPUT,
CONF_ID: 1, CONF_ID: 1,
} }
@ -359,6 +365,7 @@ async def async_init_integration(
"""Set up the Airzone integration in Home Assistant.""" """Set up the Airzone integration in Home Assistant."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
minor_version=2,
data=CONFIG, data=CONFIG,
entry_id="6e7a0798c1734ba81d26ced0e690eaec", entry_id="6e7a0798c1734ba81d26ced0e690eaec",
domain=DOMAIN, domain=DOMAIN,