mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 14:29:21 -04:00
Fix orphaned HomeWizard external devices after unique ID migration (#172657)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Final, override
|
||||
|
||||
from homewizard_energy.const import Model
|
||||
@@ -30,6 +31,7 @@ from homeassistant.const import (
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import (
|
||||
DeviceInfo,
|
||||
async_get_device_id_by_identifier,
|
||||
@@ -43,6 +45,7 @@ from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
|
||||
from .entity import HomeWizardEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -774,20 +777,73 @@ async def async_setup_entry(
|
||||
# Initialize external devices
|
||||
measurement = entry.runtime_data.data.measurement
|
||||
if measurement.external_devices is not None:
|
||||
for unique_id, device in measurement.external_devices.items():
|
||||
dev_reg = dr.async_get(hass)
|
||||
parent_serial = entry.runtime_data.data.device.serial
|
||||
|
||||
# This cleanup must run at setup because the external device mapping
|
||||
# needed for old->new identifier conversion is only available in runtime
|
||||
# measurement data.
|
||||
for new_unique_id, device in measurement.external_devices.items():
|
||||
_async_migrate_external_device_identifier(
|
||||
dev_reg,
|
||||
entry,
|
||||
str(device.unique_id),
|
||||
new_unique_id,
|
||||
parent_serial,
|
||||
)
|
||||
|
||||
if device.type is not None and (
|
||||
description := EXTERNAL_SENSORS.get(device.type)
|
||||
):
|
||||
# Add external device
|
||||
entities.append(
|
||||
HomeWizardExternalSensorEntity(
|
||||
entry.runtime_data, description, unique_id
|
||||
entry.runtime_data, description, new_unique_id
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
def _async_migrate_external_device_identifier(
|
||||
dev_reg: dr.DeviceRegistry,
|
||||
entry: HomeWizardConfigEntry,
|
||||
old_unique_id: str,
|
||||
new_unique_id: str,
|
||||
parent_serial: str | None,
|
||||
) -> None:
|
||||
"""Migrate a HomeWizard external device identifier when needed."""
|
||||
old_device = dev_reg.async_get_device_by_identifier(
|
||||
(DOMAIN, old_unique_id), entry.entry_id
|
||||
)
|
||||
if old_device is None:
|
||||
return
|
||||
|
||||
if old_unique_id == parent_serial:
|
||||
return
|
||||
|
||||
new_device = dev_reg.async_get_device_by_identifier(
|
||||
(DOMAIN, new_unique_id), entry.entry_id
|
||||
)
|
||||
if new_device is None:
|
||||
dev_reg.async_update_device(
|
||||
old_device.id,
|
||||
new_identifiers={(DOMAIN, new_unique_id)},
|
||||
serial_number=new_unique_id,
|
||||
)
|
||||
return
|
||||
|
||||
if old_device.id == new_device.id:
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Removing migrated HomeWizard external device %s in favor of %s",
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
dev_reg.async_remove_device(old_device.id)
|
||||
|
||||
|
||||
class HomeWizardSensorEntity(HomeWizardEntity, SensorEntity):
|
||||
"""Representation of a HomeWizard Sensor."""
|
||||
|
||||
|
||||
@@ -19,6 +19,26 @@ pytestmark = [
|
||||
pytest.mark.usefixtures("init_integration"),
|
||||
]
|
||||
|
||||
EXTERNAL_DEVICE_OLD_UNIQUE_ID = "G001"
|
||||
EXTERNAL_DEVICE_NEW_UNIQUE_ID = f"gas_meter_{EXTERNAL_DEVICE_OLD_UNIQUE_ID}"
|
||||
|
||||
|
||||
def _cleanup_external_migration_devices(
|
||||
device_registry: dr.DeviceRegistry, config_entry_id: str
|
||||
) -> None:
|
||||
"""Remove external devices used by migration tests."""
|
||||
assert (
|
||||
device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, EXTERNAL_DEVICE_OLD_UNIQUE_ID), config_entry_id
|
||||
)
|
||||
is None
|
||||
)
|
||||
existing_new = device_registry.async_get_device_by_identifier(
|
||||
(DOMAIN, EXTERNAL_DEVICE_NEW_UNIQUE_ID), config_entry_id
|
||||
)
|
||||
assert existing_new is not None
|
||||
device_registry.async_remove_device(existing_new.id)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2025-01-28 21:45:00")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@@ -621,6 +641,99 @@ async def test_external_sensors_unreachable(
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_external_device_registry_migration(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test orphaned external devices are removed during setup."""
|
||||
await hass.config_entries.async_unload(init_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
_cleanup_external_migration_devices(device_registry, init_integration.entry_id)
|
||||
|
||||
old_device = device_registry.async_get_or_create(
|
||||
config_entry_id=init_integration.entry_id,
|
||||
identifiers={(DOMAIN, EXTERNAL_DEVICE_OLD_UNIQUE_ID)},
|
||||
manufacturer="HomeWizard",
|
||||
model="HWE-P1",
|
||||
name="Gas meter",
|
||||
serial_number=EXTERNAL_DEVICE_OLD_UNIQUE_ID,
|
||||
)
|
||||
new_device = device_registry.async_get_or_create(
|
||||
config_entry_id=init_integration.entry_id,
|
||||
identifiers={(DOMAIN, EXTERNAL_DEVICE_NEW_UNIQUE_ID)},
|
||||
manufacturer="HomeWizard",
|
||||
model="HWE-P1",
|
||||
name="Gas meter",
|
||||
serial_number=EXTERNAL_DEVICE_NEW_UNIQUE_ID,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(init_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert device_registry.async_get(old_device.id) is None
|
||||
assert device_registry.async_get(new_device.id) is not None
|
||||
|
||||
|
||||
async def test_external_device_registry_migration_old_only(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test old-format external device identifiers are migrated during setup."""
|
||||
await hass.config_entries.async_unload(init_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
_cleanup_external_migration_devices(device_registry, init_integration.entry_id)
|
||||
|
||||
old_device = device_registry.async_get_or_create(
|
||||
config_entry_id=init_integration.entry_id,
|
||||
identifiers={(DOMAIN, EXTERNAL_DEVICE_OLD_UNIQUE_ID)},
|
||||
manufacturer="HomeWizard",
|
||||
model="HWE-P1",
|
||||
name="Gas meter",
|
||||
serial_number=EXTERNAL_DEVICE_OLD_UNIQUE_ID,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(init_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
migrated_device = device_registry.async_get(old_device.id)
|
||||
assert migrated_device is not None
|
||||
assert (DOMAIN, EXTERNAL_DEVICE_OLD_UNIQUE_ID) not in migrated_device.identifiers
|
||||
assert (DOMAIN, EXTERNAL_DEVICE_NEW_UNIQUE_ID) in migrated_device.identifiers
|
||||
|
||||
|
||||
async def test_external_device_registry_migration_new_only_noop(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
init_integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test already-migrated external device identifiers remain untouched."""
|
||||
await hass.config_entries.async_unload(init_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
_cleanup_external_migration_devices(device_registry, init_integration.entry_id)
|
||||
|
||||
new_device = device_registry.async_get_or_create(
|
||||
config_entry_id=init_integration.entry_id,
|
||||
identifiers={(DOMAIN, EXTERNAL_DEVICE_NEW_UNIQUE_ID)},
|
||||
manufacturer="HomeWizard",
|
||||
model="HWE-P1",
|
||||
name="Gas meter",
|
||||
serial_number=EXTERNAL_DEVICE_NEW_UNIQUE_ID,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(init_integration.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
post_setup_device = device_registry.async_get(new_device.id)
|
||||
assert post_setup_device is not None
|
||||
assert (DOMAIN, EXTERNAL_DEVICE_OLD_UNIQUE_ID) not in post_setup_device.identifiers
|
||||
assert (DOMAIN, EXTERNAL_DEVICE_NEW_UNIQUE_ID) in post_setup_device.identifiers
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_fixture", "entity_ids"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user