Reolink cleanup when CAM disconnected from NVR (#103888)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
starkillerOG 2023-11-30 01:50:37 +01:00 committed by GitHub
parent ec647677e9
commit 9fa163c107
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 120 additions and 5 deletions

View File

@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@ -148,6 +149,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
firmware_coordinator=firmware_coordinator,
)
cleanup_disconnected_cams(hass, config_entry.entry_id, host)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload(
@ -175,3 +178,56 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
def cleanup_disconnected_cams(
hass: HomeAssistant, config_entry_id: str, host: ReolinkHost
) -> None:
"""Clean-up disconnected camera channels or channels where a different model camera is connected."""
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_id = [
dev_id[1].split("_ch")
for dev_id in device.identifiers
if dev_id[0] == DOMAIN
][0]
if len(device_id) < 2:
# Do not consider the NVR itself
continue
ch = int(device_id[1])
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 entity and device registry
entity_reg = er.async_get(hass)
entities = er.async_entries_for_device(
entity_reg, device.id, include_disabled_entities=True
)
for entity in entities:
entity_reg.async_remove(entity.entity_id)
device_reg.async_remove_device(device.id)

View File

@ -25,6 +25,8 @@ TEST_PORT = 1234
TEST_NVR_NAME = "test_reolink_name"
TEST_NVR_NAME2 = "test2_reolink_name"
TEST_USE_HTTPS = True
TEST_HOST_MODEL = "RLN8-410"
TEST_CAM_MODEL = "RLC-123"
@pytest.fixture
@ -70,8 +72,8 @@ def reolink_connect_class(
host_mock.hardware_version = "IPC_00000"
host_mock.sw_version = "v1.0.0.0.0.0000"
host_mock.manufacturer = "Reolink"
host_mock.model = "RLC-123"
host_mock.camera_model.return_value = "RLC-123"
host_mock.model = TEST_HOST_MODEL
host_mock.camera_model.return_value = TEST_CAM_MODEL
host_mock.camera_name.return_value = TEST_NVR_NAME
host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000"
host_mock.session_active = True

View File

@ -41,7 +41,7 @@
'event connection': 'Fast polling',
'firmware version': 'v1.0.0.0.0.0000',
'hardware version': 'IPC_00000',
'model': 'RLC-123',
'model': 'RLN8-410',
'stream channels': list([
0,
]),

View File

@ -11,11 +11,15 @@ from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from .conftest import TEST_NVR_NAME
from .conftest import TEST_CAM_MODEL, TEST_HOST_MODEL, TEST_NVR_NAME
from tests.common import MockConfigEntry, async_fire_time_changed
@ -102,6 +106,7 @@ async def test_entry_reloading(
reolink_connect: MagicMock,
) -> None:
"""Test the entry is reloaded correctly when settings change."""
reolink_connect.is_nvr = False
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@ -115,6 +120,58 @@ async def test_entry_reloading(
assert config_entry.title == "New Name"
@pytest.mark.parametrize(
("attr", "value", "expected_models"),
[
(
None,
None,
[TEST_HOST_MODEL, TEST_CAM_MODEL],
),
("channels", [], [TEST_HOST_MODEL]),
(
"camera_model",
Mock(return_value="RLC-567"),
[TEST_HOST_MODEL, "RLC-567"],
),
],
)
async def test_cleanup_disconnected_cams(
hass: HomeAssistant,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
attr: str | None,
value: Any,
expected_models: list[str],
) -> None:
"""Test device and entity registry are cleaned up when camera is disconnected from NVR."""
reolink_connect.channels = [0]
# 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)
await hass.async_block_till_done()
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
device_models = [device.model for device in device_entries]
assert sorted(device_models) == sorted([TEST_HOST_MODEL, TEST_CAM_MODEL])
# reload integration after 'disconnecting' a camera.
if attr is not None:
setattr(reolink_connect, attr, value)
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
assert await hass.config_entries.async_reload(config_entry.entry_id)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry.entry_id
)
device_models = [device.model for device in device_entries]
assert sorted(device_models) == sorted(expected_models)
async def test_no_repair_issue(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> None: