From 5cb41106b5628df354c17b81ec65429f6276c27c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 2 Jul 2024 13:31:23 +0200 Subject: [PATCH] Reolink replace automatic removal of devices by manual removal (#120981) Co-authored-by: Robert Resch --- homeassistant/components/reolink/__init__.py | 87 ++++++++++---------- tests/components/reolink/test_init.py | 31 +++++-- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 150a23dc64e..02d3cc16419 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -147,9 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b firmware_coordinator=firmware_coordinator, ) - # first migrate and then cleanup, otherwise entities lost migrate_entity_ids(hass, config_entry.entry_id, host) - cleanup_disconnected_cams(hass, config_entry.entry_id, host) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -179,6 +177,50 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> bool: + """Remove a device from a config entry.""" + host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host + (device_uid, ch) = get_device_uid_and_ch(device, host) + + if not host.api.is_nvr or ch is None: + _LOGGER.warning( + "Cannot remove Reolink device %s, because it is not a camera connected " + "to a NVR/Hub, please remove the integration entry instead", + device.name, + ) + return False # Do not remove the host/NVR itself + + if ch not in host.api.channels: + _LOGGER.debug( + "Removing Reolink device %s, " + "since no camera is connected to NVR channel %s anymore", + device.name, + ch, + ) + return True + + await host.api.get_state(cmd="GetChannelstatus") # update the camera_online status + if not host.api.camera_online(ch): + _LOGGER.debug( + "Removing Reolink device %s, " + "since the camera connected to channel %s is offline", + device.name, + ch, + ) + return True + + _LOGGER.warning( + "Cannot remove Reolink device %s on channel %s, because it is still connected " + "to the NVR/Hub, please first remove the camera from the NVR/Hub " + "in the reolink app", + device.name, + ch, + ) + return False + + def get_device_uid_and_ch( device: dr.DeviceEntry, host: ReolinkHost ) -> tuple[list[str], int | None]: @@ -197,47 +239,6 @@ def get_device_uid_and_ch( return (device_uid, ch) -def cleanup_disconnected_cams( - hass: HomeAssistant, config_entry_id: str, host: ReolinkHost -) -> None: - """Clean-up disconnected camera channels.""" - if not host.api.is_nvr: - return - - device_reg = dr.async_get(hass) - devices = dr.async_entries_for_config_entry(device_reg, config_entry_id) - for device in devices: - (device_uid, ch) = get_device_uid_and_ch(device, host) - if ch is None: - continue # Do not consider the NVR itself - - ch_model = host.api.camera_model(ch) - remove = False - if ch not in host.api.channels: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since no camera is connected to NVR channel %s anymore", - device.name, - ch, - ) - if ch_model not in [device.model, "Unknown"]: - remove = True - _LOGGER.debug( - "Removing Reolink device %s, " - "since the camera model connected to channel %s changed from %s to %s", - device.name, - ch, - device.model, - ch_model, - ) - if not remove: - continue - - # clean device registry and associated entities - device_reg.async_remove_device(device.id) - - def migrate_entity_ids( hass: HomeAssistant, config_entry_id: str, host: ReolinkHost ) -> None: diff --git a/tests/components/reolink/test_init.py b/tests/components/reolink/test_init.py index a6c798f9415..f70fd312051 100644 --- a/tests/components/reolink/test_init.py +++ b/tests/components/reolink/test_init.py @@ -36,6 +36,7 @@ from .conftest import ( ) from tests.common import MockConfigEntry, async_fire_time_changed +from tests.typing import WebSocketGenerator pytestmark = pytest.mark.usefixtures("reolink_connect", "reolink_platforms") @@ -179,16 +180,27 @@ async def test_entry_reloading( None, [TEST_HOST_MODEL, TEST_CAM_MODEL], ), + ( + "is_nvr", + False, + [TEST_HOST_MODEL, TEST_CAM_MODEL], + ), ("channels", [], [TEST_HOST_MODEL]), ( - "camera_model", - Mock(return_value="RLC-567"), - [TEST_HOST_MODEL, "RLC-567"], + "camera_online", + Mock(return_value=False), + [TEST_HOST_MODEL], + ), + ( + "channel_for_uid", + Mock(return_value=-1), + [TEST_HOST_MODEL], ), ], ) -async def test_cleanup_disconnected_cams( +async def test_removing_disconnected_cams( hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, config_entry: MockConfigEntry, reolink_connect: MagicMock, device_registry: dr.DeviceRegistry, @@ -197,8 +209,10 @@ async def test_cleanup_disconnected_cams( value: Any, expected_models: list[str], ) -> None: - """Test device and entity registry are cleaned up when camera is disconnected from NVR.""" + """Test device and entity registry are cleaned up when camera is removed.""" reolink_connect.channels = [0] + assert await async_setup_component(hass, "config", {}) + client = await hass_ws_client(hass) # setup CH 0 and NVR switch entities/device with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -215,6 +229,13 @@ async def test_cleanup_disconnected_cams( setattr(reolink_connect, attr, value) with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): assert await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + expected_success = TEST_CAM_MODEL not in expected_models + for device in device_entries: + if device.model == TEST_CAM_MODEL: + response = await client.remove_device(device.id, config_entry.entry_id) + assert response["success"] == expected_success device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id