diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 433af396d63..48b5dc1a3d6 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -364,53 +364,90 @@ def migrate_entity_ids( devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) ch_device_ids = {} for device in devices: - (device_uid, ch, is_chime) = get_device_uid_and_ch(device, host) + for dev_id in device.identifiers: + (device_uid, ch, is_chime) = get_device_uid_and_ch(dev_id, host) + if not device_uid: + continue - 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 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) - 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: + 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 ) - else: - _LOGGER.warning( - "Reolink device with uid %s already exists, " - "removing device with uid %s", - new_device_id, - device_uid, + + 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_remove_device(device.id) + 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 + ): + 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, + ) + 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/homeassistant/components/reolink/util.py b/homeassistant/components/reolink/util.py index 17e666ac52c..a80e9f8962c 100644 --- a/homeassistant/components/reolink/util.py +++ b/homeassistant/components/reolink/util.py @@ -76,13 +76,18 @@ def get_store(hass: HomeAssistant, config_entry_id: str) -> Store[str]: def get_device_uid_and_ch( - device: dr.DeviceEntry, host: ReolinkHost + device: dr.DeviceEntry | tuple[str, str], host: ReolinkHost ) -> tuple[list[str], int | None, bool]: """Get the channel and the split device_uid from a reolink DeviceEntry.""" device_uid = [] is_chime = False - for dev_id in device.identifiers: + if isinstance(device, dr.DeviceEntry): + dev_ids = device.identifiers + else: + dev_ids = {device} + + for dev_id in dev_ids: if dev_id[0] == DOMAIN: device_uid = dev_id[1].split("_") if device_uid[0] == host.unique_id: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index 6b57c1c253f..f2ae22913ad 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -630,7 +630,7 @@ async def test_cleanup_mac_connection( domain = Platform.SWITCH dev_entry = device_registry.async_get_or_create( - identifiers={(DOMAIN, dev_id)}, + identifiers={(DOMAIN, dev_id), ("OTHER_INTEGRATION", "SOME_ID")}, connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, config_entry_id=config_entry.entry_id, disabled_by=None, @@ -664,6 +664,66 @@ async def test_cleanup_mac_connection( reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM +async def test_cleanup_combined_with_NVR( + 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 combined with the NVR device.""" + reolink_connect.channels = [0] + reolink_connect.baichuan.mac_address.return_value = None + 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), + (DOMAIN, TEST_UID), + ("OTHER_INTEGRATION", "SOME_ID"), + } + + dev_entry = device_registry.async_get_or_create( + identifiers=start_identifiers, + connections={(CONNECTION_NETWORK_MAC, TEST_MAC)}, + 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 == {(DOMAIN, dev_id)} + host_device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_UID)}) + assert host_device + assert host_device.identifiers == { + (DOMAIN, TEST_UID), + ("OTHER_INTEGRATION", "SOME_ID"), + } + + reolink_connect.baichuan.mac_address.return_value = TEST_MAC_CAM + + async def test_no_repair_issue( hass: HomeAssistant, config_entry: MockConfigEntry, issue_registry: ir.IssueRegistry ) -> None: