Use Airzone WebServer MAC address as unique ID (#70287)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/70780/head
parent
7d51da1b39
commit
add7103d55
|
@ -1,14 +1,17 @@
|
|||
"""The Airzone integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone.const import (
|
||||
AZD_ID,
|
||||
AZD_MAC,
|
||||
AZD_NAME,
|
||||
AZD_SYSTEM,
|
||||
AZD_THERMOSTAT_FW,
|
||||
AZD_THERMOSTAT_MODEL,
|
||||
AZD_WEBSERVER,
|
||||
AZD_ZONES,
|
||||
DEFAULT_SYSTEM_ID,
|
||||
)
|
||||
|
@ -16,8 +19,12 @@ from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions
|
|||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
|
@ -26,6 +33,8 @@ from .coordinator import AirzoneUpdateCoordinator
|
|||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]):
|
||||
"""Define an Airzone entity."""
|
||||
|
@ -59,6 +68,9 @@ class AirzoneZoneEntity(AirzoneEntity):
|
|||
"name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}",
|
||||
"sw_version": self.get_airzone_value(AZD_THERMOSTAT_FW),
|
||||
}
|
||||
self._attr_unique_id = (
|
||||
entry.entry_id if entry.unique_id is None else entry.unique_id
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key) -> Any:
|
||||
"""Return zone value by key."""
|
||||
|
@ -70,6 +82,46 @@ class AirzoneZoneEntity(AirzoneEntity):
|
|||
return value
|
||||
|
||||
|
||||
async def _async_migrate_unique_ids(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Migrate entities when the mac address gets discovered."""
|
||||
|
||||
@callback
|
||||
def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
|
||||
updates = None
|
||||
|
||||
unique_id = entry.unique_id
|
||||
entry_id = entry.entry_id
|
||||
entity_unique_id = entity_entry.unique_id
|
||||
|
||||
if entity_unique_id.startswith(entry_id):
|
||||
new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}"
|
||||
_LOGGER.info(
|
||||
"Migrating unique_id from [%s] to [%s]",
|
||||
entity_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
updates = {"new_unique_id": new_unique_id}
|
||||
|
||||
return updates
|
||||
|
||||
if (
|
||||
entry.unique_id is None
|
||||
and AZD_WEBSERVER in coordinator.data
|
||||
and AZD_MAC in coordinator.data[AZD_WEBSERVER]
|
||||
and (mac := coordinator.data[AZD_WEBSERVER][AZD_MAC]) is not None
|
||||
):
|
||||
updates: dict[str, Any] = {
|
||||
"unique_id": dr.format_mac(mac),
|
||||
}
|
||||
hass.config_entries.async_update_entry(entry, **updates)
|
||||
|
||||
await er.async_migrate_entries(hass, entry.entry_id, _async_migrator)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Airzone from a config entry."""
|
||||
options = ConnectionOptions(
|
||||
|
@ -79,9 +131,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
)
|
||||
|
||||
airzone = AirzoneLocalApi(aiohttp_client.async_get_clientsession(hass), options)
|
||||
|
||||
coordinator = AirzoneUpdateCoordinator(hass, airzone)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
await _async_migrate_unique_ids(hass, entry, coordinator)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
|
|
|
@ -122,6 +122,10 @@ class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor):
|
|||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, system_zone_id, zone_data)
|
||||
|
||||
self._attr_name = f"{zone_data[AZD_NAME]} {description.name}"
|
||||
self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}"
|
||||
self._attr_unique_id = (
|
||||
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
||||
)
|
||||
self.attributes = description.attributes
|
||||
self.entity_description = description
|
||||
|
|
|
@ -99,8 +99,9 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
|||
) -> None:
|
||||
"""Initialize Airzone climate entity."""
|
||||
super().__init__(coordinator, entry, system_zone_id, zone_data)
|
||||
|
||||
self._attr_name = f"{zone_data[AZD_NAME]}"
|
||||
self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}"
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{system_zone_id}"
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
self._attr_target_temperature_step = API_TEMPERATURE_STEP
|
||||
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX)
|
||||
|
|
|
@ -12,6 +12,7 @@ from homeassistant import config_entries
|
|||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
@ -51,13 +52,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
)
|
||||
|
||||
try:
|
||||
await airzone.validate()
|
||||
mac = await airzone.validate()
|
||||
except InvalidSystem:
|
||||
data_schema = SYSTEM_ID_SCHEMA
|
||||
errors[CONF_ID] = "invalid_system_id"
|
||||
except AirzoneError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
if mac:
|
||||
await self.async_set_unique_id(format_mac(mac))
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
}
|
||||
)
|
||||
|
||||
title = f"Airzone {user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
|
|
|
@ -83,8 +83,11 @@ class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor):
|
|||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, system_zone_id, zone_data)
|
||||
|
||||
self._attr_name = f"{zone_data[AZD_NAME]} {description.name}"
|
||||
self._attr_unique_id = f"{entry.entry_id}_{system_zone_id}_{description.key}"
|
||||
self._attr_unique_id = (
|
||||
f"{self._attr_unique_id}_{system_zone_id}_{description.key}"
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
if description.key == AZD_TEMP:
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
|||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .util import CONFIG, CONFIG_ID1, HVAC_MOCK
|
||||
from .util import CONFIG, CONFIG_ID1, HVAC_MOCK, HVAC_WEBSERVER_MOCK
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
|||
side_effect=SystemOutOfRange,
|
||||
), patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
|
||||
side_effect=InvalidMethod,
|
||||
return_value=HVAC_WEBSERVER_MOCK,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
|
@ -118,8 +118,12 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None:
|
|||
async def test_form_duplicated_id(hass: HomeAssistant) -> None:
|
||||
"""Test setting up duplicated entry."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG,
|
||||
domain=DOMAIN,
|
||||
unique_id="airzone_unique_id",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONFIG
|
||||
|
|
|
@ -18,8 +18,12 @@ from tests.common import MockConfigEntry, async_fire_time_changed
|
|||
async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:
|
||||
"""Test ClientConnectorError on coordinator update."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG,
|
||||
domain=DOMAIN,
|
||||
unique_id="airzone_unique_id",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac",
|
||||
|
@ -31,7 +35,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None:
|
|||
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
|
||||
side_effect=InvalidMethod,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
mock_hvac.assert_called_once()
|
||||
mock_hvac.reset_mock()
|
||||
|
|
|
@ -7,18 +7,19 @@ from aioairzone.exceptions import InvalidMethod, SystemOutOfRange
|
|||
from homeassistant.components.airzone.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .util import CONFIG, HVAC_MOCK
|
||||
from .util import CONFIG, HVAC_MOCK, HVAC_WEBSERVER_MOCK
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant) -> None:
|
||||
"""Test unload."""
|
||||
async def test_unique_id_migrate(hass: HomeAssistant) -> None:
|
||||
"""Test unique id migration."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="airzone_unique_id", data=CONFIG
|
||||
)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
|
@ -30,6 +31,52 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
|
|||
), patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
|
||||
side_effect=InvalidMethod,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not config_entry.unique_id
|
||||
assert (
|
||||
entity_registry.async_get("sensor.salon_temperature").unique_id
|
||||
== f"{config_entry.entry_id}_1:1_temp"
|
||||
)
|
||||
|
||||
with 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_webserver",
|
||||
return_value=HVAC_WEBSERVER_MOCK,
|
||||
):
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.unique_id
|
||||
assert (
|
||||
entity_registry.async_get("sensor.salon_temperature").unique_id
|
||||
== f"{config_entry.unique_id}_1:1_temp"
|
||||
)
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant) -> None:
|
||||
"""Test unload."""
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG,
|
||||
domain=DOMAIN,
|
||||
unique_id="airzone_unique_id",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.validate",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.update",
|
||||
return_value=None,
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
@ -12,6 +12,7 @@ from aioairzone.const import (
|
|||
API_HEAT_STAGE,
|
||||
API_HEAT_STAGES,
|
||||
API_HUMIDITY,
|
||||
API_MAC,
|
||||
API_MAX_TEMP,
|
||||
API_MIN_TEMP,
|
||||
API_MODE,
|
||||
|
@ -26,6 +27,8 @@ from aioairzone.const import (
|
|||
API_THERMOS_RADIO,
|
||||
API_THERMOS_TYPE,
|
||||
API_UNITS,
|
||||
API_WIFI_CHANNEL,
|
||||
API_WIFI_RSSI,
|
||||
API_ZONE_ID,
|
||||
)
|
||||
from aioairzone.exceptions import InvalidMethod, SystemOutOfRange
|
||||
|
@ -175,14 +178,24 @@ HVAC_MOCK = {
|
|||
]
|
||||
}
|
||||
|
||||
HVAC_WEBSERVER_MOCK = {
|
||||
API_MAC: "11:22:33:44:55:66",
|
||||
API_WIFI_CHANNEL: 6,
|
||||
API_WIFI_RSSI: -42,
|
||||
}
|
||||
|
||||
|
||||
async def async_init_integration(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Set up the Airzone integration in Home Assistant."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
config_entry = MockConfigEntry(
|
||||
data=CONFIG,
|
||||
domain=DOMAIN,
|
||||
unique_id="airzone_unique_id",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.airzone.AirzoneLocalApi.get_hvac",
|
||||
|
@ -194,5 +207,5 @@ async def async_init_integration(
|
|||
"homeassistant.components.airzone.AirzoneLocalApi.get_webserver",
|
||||
side_effect=InvalidMethod,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
|
Loading…
Reference in New Issue