1
mirror of https://github.com/home-assistant/core synced 2024-08-02 23:40:32 +02:00

Use Airzone WebServer MAC address as unique ID (#70287)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Álvaro Fernández Rojas 2022-04-26 10:52:55 +02:00 committed by GitHub
parent 7d51da1b39
commit add7103d55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 161 additions and 23 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()