Reolink cleanup when CAM disconnected from NVR (#103888)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
ec647677e9
commit
9fa163c107
4 changed files with 120 additions and 5 deletions
|
@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
@ -148,6 +149,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||||
firmware_coordinator=firmware_coordinator,
|
firmware_coordinator=firmware_coordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cleanup_disconnected_cams(hass, config_entry.entry_id, host)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||||
|
|
||||||
config_entry.async_on_unload(
|
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)
|
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
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)
|
||||||
|
|
|
@ -25,6 +25,8 @@ TEST_PORT = 1234
|
||||||
TEST_NVR_NAME = "test_reolink_name"
|
TEST_NVR_NAME = "test_reolink_name"
|
||||||
TEST_NVR_NAME2 = "test2_reolink_name"
|
TEST_NVR_NAME2 = "test2_reolink_name"
|
||||||
TEST_USE_HTTPS = True
|
TEST_USE_HTTPS = True
|
||||||
|
TEST_HOST_MODEL = "RLN8-410"
|
||||||
|
TEST_CAM_MODEL = "RLC-123"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -70,8 +72,8 @@ def reolink_connect_class(
|
||||||
host_mock.hardware_version = "IPC_00000"
|
host_mock.hardware_version = "IPC_00000"
|
||||||
host_mock.sw_version = "v1.0.0.0.0.0000"
|
host_mock.sw_version = "v1.0.0.0.0.0000"
|
||||||
host_mock.manufacturer = "Reolink"
|
host_mock.manufacturer = "Reolink"
|
||||||
host_mock.model = "RLC-123"
|
host_mock.model = TEST_HOST_MODEL
|
||||||
host_mock.camera_model.return_value = "RLC-123"
|
host_mock.camera_model.return_value = TEST_CAM_MODEL
|
||||||
host_mock.camera_name.return_value = TEST_NVR_NAME
|
host_mock.camera_name.return_value = TEST_NVR_NAME
|
||||||
host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000"
|
host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000"
|
||||||
host_mock.session_active = True
|
host_mock.session_active = True
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
'event connection': 'Fast polling',
|
'event connection': 'Fast polling',
|
||||||
'firmware version': 'v1.0.0.0.0.0000',
|
'firmware version': 'v1.0.0.0.0.0000',
|
||||||
'hardware version': 'IPC_00000',
|
'hardware version': 'IPC_00000',
|
||||||
'model': 'RLC-123',
|
'model': 'RLN8-410',
|
||||||
'stream channels': list([
|
'stream channels': list([
|
||||||
0,
|
0,
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -11,11 +11,15 @@ from homeassistant.config import async_process_ha_core_config
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform
|
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
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.setup import async_setup_component
|
||||||
from homeassistant.util.dt import utcnow
|
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
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
@ -102,6 +106,7 @@ async def test_entry_reloading(
|
||||||
reolink_connect: MagicMock,
|
reolink_connect: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the entry is reloaded correctly when settings change."""
|
"""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)
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -115,6 +120,58 @@ async def test_entry_reloading(
|
||||||
assert config_entry.title == "New Name"
|
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(
|
async def test_no_repair_issue(
|
||||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue