diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 48b5dc1a3d6..57d41c20521 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -364,90 +364,88 @@ def migrate_entity_ids( devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) ch_device_ids = {} for device in devices: - for dev_id in device.identifiers: - (device_uid, ch, is_chime) = get_device_uid_and_ch(dev_id, host) - if not device_uid: - continue + (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) - if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: - if ch is None: - new_device_id = f"{host.unique_id}" - else: - new_device_id = f"{host.unique_id}_{device_uid[1]}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", - device_uid, - new_device_id, - ) - new_identifiers = {(DOMAIN, new_device_id)} - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) + if host.api.supported(None, "UID") and device_uid[0] != host.unique_id: + if ch is None: + new_device_id = f"{host.unique_id}" + else: + new_device_id = f"{host.unique_id}_{device_uid[1]}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) - if ch is None or is_chime: - continue # Do not consider the NVR itself or chimes - - # Check for wrongfully combined host with NVR entities in one device - # Can be removed in HA 2025.12 - if (DOMAIN, host.unique_id) in device.identifiers: - new_identifiers = device.identifiers.copy() - for old_id in device.identifiers: - if old_id[0] == DOMAIN and old_id[1] != host.unique_id: - new_identifiers.remove(old_id) - _LOGGER.debug( - "Updating Reolink device identifiers from %s to %s", - device.identifiers, - new_identifiers, - ) - device_reg.async_update_device( - device.id, new_identifiers=new_identifiers - ) - break - - # Check for wrongfully added MAC of the NVR/Hub to the camera - # Can be removed in HA 2025.12 - host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) - if host_connnection in device.connections: - new_connections = device.connections.copy() - new_connections.remove(host_connnection) - _LOGGER.debug( - "Updating Reolink device connections from %s to %s", - device.connections, - new_connections, - ) - device_reg.async_update_device( - device.id, new_connections=new_connections - ) - - ch_device_ids[device.id] = ch - if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid( - ch + # Check for wrongfully combined entities in one device + # Can be removed in HA 2025.12 + new_identifiers = device.identifiers.copy() + remove_ids = False + if (DOMAIN, host.unique_id) in device.identifiers: + remove_ids = True # NVR/Hub in identifiers, keep that one, remove others + for old_id in device.identifiers: + (old_device_uid, old_ch, old_is_chime) = get_device_uid_and_ch(old_id, host) + if ( + not old_device_uid + or old_device_uid[0] != host.unique_id + or old_id[1] == host.unique_id ): - if host.api.supported(None, "UID"): - new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" - else: - new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" - _LOGGER.debug( - "Updating Reolink device UID from %s to %s", - device_uid, + continue + if remove_ids: + new_identifiers.remove(old_id) + remove_ids = True # after the first identifier, remove the others + if new_identifiers != device.identifiers: + _LOGGER.debug( + "Updating Reolink device identifiers from %s to %s", + device.identifiers, + new_identifiers, + ) + device_reg.async_update_device(device.id, new_identifiers=new_identifiers) + break + + if ch is None or is_chime: + continue # Do not consider the NVR itself or chimes + + # Check for wrongfully added MAC of the NVR/Hub to the camera + # Can be removed in HA 2025.12 + host_connnection = (CONNECTION_NETWORK_MAC, host.api.mac_address) + if host_connnection in device.connections: + new_connections = device.connections.copy() + new_connections.remove(host_connnection) + _LOGGER.debug( + "Updating Reolink device connections from %s to %s", + device.connections, + new_connections, + ) + device_reg.async_update_device(device.id, new_connections=new_connections) + + ch_device_ids[device.id] = ch + if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch): + if host.api.supported(None, "UID"): + new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}" + else: + new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}" + _LOGGER.debug( + "Updating Reolink device UID from %s to %s", + device_uid, + new_device_id, + ) + new_identifiers = {(DOMAIN, new_device_id)} + 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, ) - new_identifiers = {(DOMAIN, new_device_id)} - 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) + 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) diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index f2ae22913ad..3551632903f 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -724,6 +724,57 @@ async def test_cleanup_combined_with_NVR( reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM +async def test_cleanup_hub_and_direct_connection( + hass: HomeAssistant, + config_entry: MockConfigEntry, + reolink_connect: MagicMock, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test cleanup of the device registry if IPC camera device was connected directly and through the hub/NVR.""" + reolink_connect.channels = [0] + entity_id = f"{TEST_UID}_{TEST_UID_CAM}_record_audio" + dev_id = f"{TEST_UID}_{TEST_UID_CAM}" + domain = Platform.SWITCH + start_identifiers = { + (DOMAIN, dev_id), # IPC camera through hub + (DOMAIN, TEST_UID_CAM), # directly connected IPC camera + ("OTHER_INTEGRATION", "SOME_ID"), + } + + dev_entry = device_registry.async_get_or_create( + identifiers=start_identifiers, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC_CAM)}, + config_entry_id=config_entry.entry_id, + disabled_by=None, + ) + + entity_registry.async_get_or_create( + domain=domain, + platform=DOMAIN, + unique_id=entity_id, + config_entry=config_entry, + suggested_object_id=entity_id, + disabled_by=None, + device_id=dev_entry.id, + ) + + assert entity_registry.async_get_entity_id(domain, DOMAIN, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == start_identifiers + + # 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, entity_id) + device = device_registry.async_get_device(identifiers={(DOMAIN, dev_id)}) + assert device + assert device.identifiers == start_identifiers + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: