From a37953512708a9a569fdd119ec478132ca060e54 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 18 Nov 2024 18:59:17 +0100 Subject: [PATCH] Reolink fix dev/entity id migration (#130836) --- homeassistant/components/reolink/__init__.py | 30 ++++- tests/components/reolink/test_init.py | 110 +++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 7a36991201a..ae0badb3d84 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -326,7 +326,19 @@ def migrate_entity_ids( else: new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" new_identifiers = {(DOMAIN, new_device_id)} - device_reg.async_update_device(device.id, new_identifiers=new_identifiers) + existing_device = device_reg.async_get_device(identifiers=new_identifiers) + if existing_device is None: + device_reg.async_update_device( + device.id, new_identifiers=new_identifiers + ) + else: + _LOGGER.warning( + "Reolink device with uid %s already exists, " + "removing device with uid %s", + new_device_id, + device_uid, + ) + device_reg.async_remove_device(device.id) entity_reg = er.async_get(hass) entities = er.async_entries_for_config_entry(entity_reg, config_entry_id) @@ -352,4 +364,18 @@ def migrate_entity_ids( id_parts = entity.unique_id.split("_", 2) if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch): new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}" - entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id) + existing_entity = entity_reg.async_get_entity_id( + entity.domain, entity.platform, new_id + ) + if existing_entity is None: + entity_reg.async_update_entity( + entity.entity_id, new_unique_id=new_id + ) + else: + _LOGGER.warning( + "Reolink entity with unique_id %s already exists, " + "removing device with unique_id %s", + new_id, + entity.unique_id, + ) + entity_reg.async_remove(entity.entity_id) diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 67ac2db8262..f851e13c91d 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -469,6 +469,116 @@ async def test_migrate_entity_ids( assert device_registry.async_get_device(identifiers={(DOMAIN, new_dev_id)}) +async def test_migrate_with_already_existing_device( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device ids that need to be migrated while the new ids already exist.""" + original_dev_id = f"{TEST_MAC}_ch0" + new_dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + + def mock_supported(ch, capability): + if capability == "UID" and ch is None: + return True + if capability == "UID": + return True + return True + + reolink_connect.channels = [0] + reolink_connect.supported = mock_supported + + device_registry.async_get_or_create( + identifiers={(DOMAIN, new_dev_id)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + device_registry.async_get_or_create( + identifiers={(DOMAIN, original_dev_id)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + assert device_registry.async_get_device(identifiers={(DOMAIN, original_dev_id)}) + assert device_registry.async_get_device(identifiers={(DOMAIN, new_dev_id)}) + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + device_registry.async_get_device(identifiers={(DOMAIN, original_dev_id)}) + is None + ) + assert device_registry.async_get_device(identifiers={(DOMAIN, new_dev_id)}) + + +async def test_migrate_with_already_existing_entity( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test entity ids that need to be migrated while the new ids already exist.""" + original_id = f"{TEST_UID}_0_record_audio" + new_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + + def mock_supported(ch, capability): + if capability == "UID" and ch is None: + return True + if capability == "UID": + return True + return True + + reolink_connect.channels = [0] + reolink_connect.supported = mock_supported + + dev_entry = device_registry.async_get_or_create( + identifiers={(DOMAIN, dev_id)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=new_id, + config_entry=config_entry, + suggested_object_id=new_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=original_id, + config_entry=config_entry, + suggested_object_id=original_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) + assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) + + # setup CH 0 and host entities/device + with patch("homeassistant.components.reolink.PLATFORMS", [domain]): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get_entity_id(domain, DOMAIN, original_id) is None + assert entity_registry.async_get_entity_id(domain, DOMAIN, new_id) + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: