mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Fix migration of entities of Hue integration (#61095)
* fix device name in log * Fix Hue migration for all id versions * fix tests * typo * change to bit more universal approach * fix test again * formatting
This commit is contained in:
parent
dddca8aaeb
commit
bd8bba9e3f
@ -4,6 +4,7 @@ import logging
|
|||||||
|
|
||||||
from aiohue import HueBridgeV2
|
from aiohue import HueBridgeV2
|
||||||
from aiohue.discovery import is_v2_bridge
|
from aiohue.discovery import is_v2_bridge
|
||||||
|
from aiohue.v2.models.device import DeviceArchetypes
|
||||||
from aiohue.v2.models.resource import ResourceTypes
|
from aiohue.v2.models.resource import ResourceTypes
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
@ -18,7 +19,10 @@ from homeassistant.const import (
|
|||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
from homeassistant.helpers.device_registry import async_get as async_get_device_registry
|
from homeassistant.helpers.device_registry import (
|
||||||
|
async_entries_for_config_entry as devices_for_config_entries,
|
||||||
|
async_get as async_get_device_registry,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity_registry import (
|
from homeassistant.helpers.entity_registry import (
|
||||||
async_entries_for_config_entry as entities_for_config_entry,
|
async_entries_for_config_entry as entities_for_config_entry,
|
||||||
async_entries_for_device,
|
async_entries_for_device,
|
||||||
@ -82,6 +86,18 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
|
|||||||
dev_reg = async_get_device_registry(hass)
|
dev_reg = async_get_device_registry(hass)
|
||||||
ent_reg = async_get_entity_registry(hass)
|
ent_reg = async_get_entity_registry(hass)
|
||||||
LOGGER.info("Start of migration of devices and entities to support API schema 2")
|
LOGGER.info("Start of migration of devices and entities to support API schema 2")
|
||||||
|
|
||||||
|
# Create mapping of mac address to HA device id's.
|
||||||
|
# Identifier in dev reg should be mac-address,
|
||||||
|
# but in some cases it has a postfix like `-0b` or `-01`.
|
||||||
|
dev_ids = {}
|
||||||
|
for hass_dev in devices_for_config_entries(dev_reg, entry.entry_id):
|
||||||
|
for domain, mac in hass_dev.identifiers:
|
||||||
|
if domain != DOMAIN:
|
||||||
|
continue
|
||||||
|
normalized_mac = mac.split("-")[0]
|
||||||
|
dev_ids[normalized_mac] = hass_dev.id
|
||||||
|
|
||||||
# initialize bridge connection just for the migration
|
# initialize bridge connection just for the migration
|
||||||
async with HueBridgeV2(host, api_key, websession) as api:
|
async with HueBridgeV2(host, api_key, websession) as api:
|
||||||
|
|
||||||
@ -92,63 +108,63 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
|
|||||||
DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE: ResourceTypes.TEMPERATURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
# handle entities attached to device
|
# migrate entities attached to a device
|
||||||
for hue_dev in api.devices:
|
for hue_dev in api.devices:
|
||||||
zigbee = api.devices.get_zigbee_connectivity(hue_dev.id)
|
zigbee = api.devices.get_zigbee_connectivity(hue_dev.id)
|
||||||
if not zigbee or not zigbee.mac_address:
|
if not zigbee or not zigbee.mac_address:
|
||||||
# not a zigbee device or invalid mac
|
# not a zigbee device or invalid mac
|
||||||
continue
|
continue
|
||||||
# get/update existing device by V1 identifier (mac address)
|
|
||||||
# the device will now have both the old and the new identifier
|
# get existing device by V1 identifier (mac address)
|
||||||
identifiers = {(DOMAIN, hue_dev.id), (DOMAIN, zigbee.mac_address)}
|
if hue_dev.product_data.product_archetype == DeviceArchetypes.BRIDGE_V2:
|
||||||
hass_dev = dev_reg.async_get_or_create(
|
hass_dev_id = dev_ids.get(api.config.bridge_id.upper())
|
||||||
config_entry_id=entry.entry_id, identifiers=identifiers
|
else:
|
||||||
)
|
hass_dev_id = dev_ids.get(zigbee.mac_address)
|
||||||
LOGGER.info("Migrated device %s (%s)", hass_dev.name, hass_dev.id)
|
if hass_dev_id is None:
|
||||||
# loop through al entities for device and find match
|
# can be safely ignored, this device does not exist in current config
|
||||||
for ent in async_entries_for_device(ent_reg, hass_dev.id, True):
|
LOGGER.debug(
|
||||||
# migrate light
|
"Ignoring device %s (%s) as it does not (yet) exist in the device registry",
|
||||||
if ent.entity_id.startswith("light"):
|
hue_dev.metadata.name,
|
||||||
# should always return one lightid here
|
hue_dev.id,
|
||||||
new_unique_id = next(iter(hue_dev.lights))
|
|
||||||
if ent.unique_id == new_unique_id:
|
|
||||||
continue # just in case
|
|
||||||
LOGGER.info(
|
|
||||||
"Migrating %s from unique id %s to %s",
|
|
||||||
ent.entity_id,
|
|
||||||
ent.unique_id,
|
|
||||||
new_unique_id,
|
|
||||||
)
|
|
||||||
ent_reg.async_update_entity(
|
|
||||||
ent.entity_id, new_unique_id=new_unique_id
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
dev_reg.async_update_device(
|
||||||
|
hass_dev_id, new_identifiers={(DOMAIN, hue_dev.id)}
|
||||||
|
)
|
||||||
|
LOGGER.info("Migrated device %s (%s)", hue_dev.metadata.name, hass_dev_id)
|
||||||
|
|
||||||
|
# loop through all entities for device and find match
|
||||||
|
for ent in async_entries_for_device(ent_reg, hass_dev_id, True):
|
||||||
|
|
||||||
|
if ent.entity_id.startswith("light"):
|
||||||
|
# migrate light
|
||||||
|
# should always return one lightid here
|
||||||
|
new_unique_id = next(iter(hue_dev.lights), None)
|
||||||
|
else:
|
||||||
# migrate sensors
|
# migrate sensors
|
||||||
matched_dev_class = sensor_class_mapping.get(
|
matched_dev_class = sensor_class_mapping.get(
|
||||||
ent.original_device_class or "unknown"
|
ent.original_device_class or "unknown"
|
||||||
)
|
)
|
||||||
if matched_dev_class is None:
|
new_unique_id = next(
|
||||||
|
(
|
||||||
|
sensor.id
|
||||||
|
for sensor in api.devices.get_sensors(hue_dev.id)
|
||||||
|
if sensor.type == matched_dev_class
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_unique_id is None:
|
||||||
# this may happen if we're looking at orphaned or unsupported entity
|
# this may happen if we're looking at orphaned or unsupported entity
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Skip migration of %s because it no longer exists on the bridge",
|
"Skip migration of %s because it no longer exists on the bridge",
|
||||||
ent.entity_id,
|
ent.entity_id,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
for sensor in api.devices.get_sensors(hue_dev.id):
|
|
||||||
if sensor.type != matched_dev_class:
|
|
||||||
continue
|
|
||||||
new_unique_id = sensor.id
|
|
||||||
if ent.unique_id == new_unique_id:
|
|
||||||
break # just in case
|
|
||||||
LOGGER.info(
|
|
||||||
"Migrating %s from unique id %s to %s",
|
|
||||||
ent.entity_id,
|
|
||||||
ent.unique_id,
|
|
||||||
new_unique_id,
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
ent_reg.async_update_entity(
|
ent_reg.async_update_entity(
|
||||||
ent.entity_id, new_unique_id=sensor.id
|
ent.entity_id, new_unique_id=new_unique_id
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# assume edge case where the entity was already migrated in a previous run
|
# assume edge case where the entity was already migrated in a previous run
|
||||||
@ -158,17 +174,27 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
|
|||||||
"Skip migration of %s because it already exists",
|
"Skip migration of %s because it already exists",
|
||||||
ent.entity_id,
|
ent.entity_id,
|
||||||
)
|
)
|
||||||
break
|
else:
|
||||||
|
LOGGER.info(
|
||||||
|
"Migrated entity %s from unique id %s to %s",
|
||||||
|
ent.entity_id,
|
||||||
|
ent.unique_id,
|
||||||
|
new_unique_id,
|
||||||
|
)
|
||||||
|
|
||||||
# migrate entities that are not connected to a device (groups)
|
# migrate entities that are not connected to a device (groups)
|
||||||
for ent in entities_for_config_entry(ent_reg, entry.entry_id):
|
for ent in entities_for_config_entry(ent_reg, entry.entry_id):
|
||||||
if ent.device_id is not None:
|
if ent.device_id is not None:
|
||||||
continue
|
continue
|
||||||
|
if "-" in ent.unique_id:
|
||||||
|
# handle case where unique id is v2-id of group/zone
|
||||||
|
hue_group = api.groups.get(ent.unique_id)
|
||||||
|
else:
|
||||||
|
# handle case where the unique id is just the v1 id
|
||||||
v1_id = f"/groups/{ent.unique_id}"
|
v1_id = f"/groups/{ent.unique_id}"
|
||||||
hue_group = api.groups.room.get_by_v1_id(v1_id)
|
hue_group = api.groups.room.get_by_v1_id(
|
||||||
if hue_group is None or hue_group.grouped_light is None:
|
v1_id
|
||||||
# try again with zone
|
) or api.groups.zone.get_by_v1_id(v1_id)
|
||||||
hue_group = api.groups.zone.get_by_v1_id(v1_id)
|
|
||||||
if hue_group is None or hue_group.grouped_light is None:
|
if hue_group is None or hue_group.grouped_light is None:
|
||||||
# this may happen if we're looking at some orphaned entity
|
# this may happen if we're looking at some orphaned entity
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
|
@ -54,12 +54,12 @@ async def test_light_entity_migration(
|
|||||||
# create device/entity with V1 schema in registry
|
# create device/entity with V1 schema in registry
|
||||||
device = dev_reg.async_get_or_create(
|
device = dev_reg.async_get_or_create(
|
||||||
config_entry_id=config_entry.entry_id,
|
config_entry_id=config_entry.entry_id,
|
||||||
identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65")},
|
identifiers={(hue.DOMAIN, "00:17:88:01:09:aa:bb:65-0b")},
|
||||||
)
|
)
|
||||||
ent_reg.async_get_or_create(
|
ent_reg.async_get_or_create(
|
||||||
"light",
|
"light",
|
||||||
hue.DOMAIN,
|
hue.DOMAIN,
|
||||||
"00:17:88:01:09:aa:bb:65",
|
"00:17:88:01:09:aa:bb:65-0b",
|
||||||
suggested_object_id="migrated_light_1",
|
suggested_object_id="migrated_light_1",
|
||||||
device_id=device.id,
|
device_id=device.id,
|
||||||
)
|
)
|
||||||
@ -74,14 +74,13 @@ async def test_light_entity_migration(
|
|||||||
):
|
):
|
||||||
await hue.migration.handle_v2_migration(hass, config_entry)
|
await hue.migration.handle_v2_migration(hass, config_entry)
|
||||||
|
|
||||||
# migrated device should have new identifier (guid) and old style (mac)
|
# migrated device should now have the new identifier (guid) instead of old style (mac)
|
||||||
migrated_device = dev_reg.async_get(device.id)
|
migrated_device = dev_reg.async_get(device.id)
|
||||||
assert migrated_device is not None
|
assert migrated_device is not None
|
||||||
assert migrated_device.identifiers == {
|
assert migrated_device.identifiers == {
|
||||||
(hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50"),
|
(hue.DOMAIN, "0b216218-d811-4c95-8c55-bbcda50f9d50")
|
||||||
(hue.DOMAIN, "00:17:88:01:09:aa:bb:65"),
|
|
||||||
}
|
}
|
||||||
# the entity should have the new identifier (guid)
|
# the entity should have the new unique_id (guid)
|
||||||
migrated_entity = ent_reg.async_get("light.migrated_light_1")
|
migrated_entity = ent_reg.async_get("light.migrated_light_1")
|
||||||
assert migrated_entity is not None
|
assert migrated_entity is not None
|
||||||
assert migrated_entity.unique_id == "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1"
|
assert migrated_entity.unique_id == "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1"
|
||||||
@ -131,14 +130,13 @@ async def test_sensor_entity_migration(
|
|||||||
):
|
):
|
||||||
await hue.migration.handle_v2_migration(hass, config_entry)
|
await hue.migration.handle_v2_migration(hass, config_entry)
|
||||||
|
|
||||||
# migrated device should have new identifier (guid) and old style (mac)
|
# migrated device should now have the new identifier (guid) instead of old style (mac)
|
||||||
migrated_device = dev_reg.async_get(device.id)
|
migrated_device = dev_reg.async_get(device.id)
|
||||||
assert migrated_device is not None
|
assert migrated_device is not None
|
||||||
assert migrated_device.identifiers == {
|
assert migrated_device.identifiers == {
|
||||||
(hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6"),
|
(hue.DOMAIN, "2330b45d-6079-4c6e-bba6-1b68afb1a0d6")
|
||||||
(hue.DOMAIN, device_mac),
|
|
||||||
}
|
}
|
||||||
# the entities should have the correct V2 identifier (guid)
|
# the entities should have the correct V2 unique_id (guid)
|
||||||
for dev_class, platform, new_id in sensor_mappings:
|
for dev_class, platform, new_id in sensor_mappings:
|
||||||
migrated_entity = ent_reg.async_get(
|
migrated_entity = ent_reg.async_get(
|
||||||
f"{platform}.hue_migrated_{dev_class}_sensor"
|
f"{platform}.hue_migrated_{dev_class}_sensor"
|
||||||
@ -147,7 +145,7 @@ async def test_sensor_entity_migration(
|
|||||||
assert migrated_entity.unique_id == new_id
|
assert migrated_entity.unique_id == new_id
|
||||||
|
|
||||||
|
|
||||||
async def test_group_entity_migration(
|
async def test_group_entity_migration_with_v1_id(
|
||||||
hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data
|
hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data
|
||||||
):
|
):
|
||||||
"""Test if entity schema for grouped_lights migrates from v1 to v2."""
|
"""Test if entity schema for grouped_lights migrates from v1 to v2."""
|
||||||
@ -156,6 +154,7 @@ async def test_group_entity_migration(
|
|||||||
ent_reg = er.async_get(hass)
|
ent_reg = er.async_get(hass)
|
||||||
|
|
||||||
# create (deviceless) entity with V1 schema in registry
|
# create (deviceless) entity with V1 schema in registry
|
||||||
|
# using the legacy style group id as unique id
|
||||||
ent_reg.async_get_or_create(
|
ent_reg.async_get_or_create(
|
||||||
"light",
|
"light",
|
||||||
hue.DOMAIN,
|
hue.DOMAIN,
|
||||||
@ -177,3 +176,36 @@ async def test_group_entity_migration(
|
|||||||
migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light")
|
migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light")
|
||||||
assert migrated_entity is not None
|
assert migrated_entity is not None
|
||||||
assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34"
|
assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_group_entity_migration_with_v2_group_id(
|
||||||
|
hass, mock_bridge_v2, mock_config_entry_v2, v2_resources_test_data
|
||||||
|
):
|
||||||
|
"""Test if entity schema for grouped_lights migrates from v1 to v2."""
|
||||||
|
config_entry = mock_bridge_v2.config_entry = mock_config_entry_v2
|
||||||
|
|
||||||
|
ent_reg = er.async_get(hass)
|
||||||
|
|
||||||
|
# create (deviceless) entity with V1 schema in registry
|
||||||
|
# using the V2 group id as unique id
|
||||||
|
ent_reg.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
hue.DOMAIN,
|
||||||
|
"6ddc9066-7e7d-4a03-a773-c73937968296",
|
||||||
|
suggested_object_id="hue_migrated_grouped_light",
|
||||||
|
config_entry=config_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
# now run the migration and check results
|
||||||
|
await mock_bridge_v2.api.load_test_data(v2_resources_test_data)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hue.migration.HueBridgeV2",
|
||||||
|
return_value=mock_bridge_v2.api,
|
||||||
|
):
|
||||||
|
await hue.migration.handle_v2_migration(hass, config_entry)
|
||||||
|
|
||||||
|
# the entity should have the new identifier (guid)
|
||||||
|
migrated_entity = ent_reg.async_get("light.hue_migrated_grouped_light")
|
||||||
|
assert migrated_entity is not None
|
||||||
|
assert migrated_entity.unique_id == "e937f8db-2f0e-49a0-936e-027e60e15b34"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user