Revert "Add ability to ignore devices for UniFi Protect" (#77916)

This commit is contained in:
Franck Nijhof 2022-09-06 20:13:01 +02:00 committed by GitHub
parent 5632e33426
commit dbb556a812
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 75 additions and 258 deletions

View file

@ -26,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import ( from .const import (
CONF_ALL_UPDATES, CONF_ALL_UPDATES,
CONF_IGNORED,
CONF_OVERRIDE_CHOST, CONF_OVERRIDE_CHOST,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DEVICES_FOR_SUBSCRIBE, DEVICES_FOR_SUBSCRIBE,
@ -36,11 +35,11 @@ from .const import (
OUTDATED_LOG_MESSAGE, OUTDATED_LOG_MESSAGE,
PLATFORMS, PLATFORMS,
) )
from .data import ProtectData from .data import ProtectData, async_ufp_instance_for_config_entry_ids
from .discovery import async_start_discovery from .discovery import async_start_discovery
from .migrate import async_migrate_data from .migrate import async_migrate_data
from .services import async_cleanup_services, async_setup_services from .services import async_cleanup_services, async_setup_services
from .utils import async_unifi_mac, convert_mac_list from .utils import _async_unifi_mac_from_hass, async_get_devices
from .views import ThumbnailProxyView, VideoProxyView from .views import ThumbnailProxyView, VideoProxyView
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -107,19 +106,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options.""" """Update options."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
changed = data.async_get_changed_options(entry)
if len(changed) == 1 and CONF_IGNORED in changed:
new_macs = convert_mac_list(entry.options.get(CONF_IGNORED, ""))
added_macs = new_macs - data.ignored_macs
removed_macs = data.ignored_macs - new_macs
# if only ignored macs are added, we can handle without reloading
if not removed_macs and added_macs:
data.async_add_new_ignored_macs(added_macs)
return
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
@ -139,15 +125,15 @@ async def async_remove_config_entry_device(
) -> bool: ) -> bool:
"""Remove ufp config entry from a device.""" """Remove ufp config entry from a device."""
unifi_macs = { unifi_macs = {
async_unifi_mac(connection[1]) _async_unifi_mac_from_hass(connection[1])
for connection in device_entry.connections for connection in device_entry.connections
if connection[0] == dr.CONNECTION_NETWORK_MAC if connection[0] == dr.CONNECTION_NETWORK_MAC
} }
data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id})
if data.api.bootstrap.nvr.mac in unifi_macs: assert api is not None
if api.bootstrap.nvr.mac in unifi_macs:
return False return False
for device in data.get_by_types(DEVICES_THAT_ADOPT): for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT):
if device.is_adopted_by_us and device.mac in unifi_macs: if device.is_adopted_by_us and device.mac in unifi_macs:
data.async_ignore_mac(device.mac) return False
break
return True return True

View file

@ -35,7 +35,6 @@ from homeassistant.util.network import is_ip_address
from .const import ( from .const import (
CONF_ALL_UPDATES, CONF_ALL_UPDATES,
CONF_DISABLE_RTSP, CONF_DISABLE_RTSP,
CONF_IGNORED,
CONF_MAX_MEDIA, CONF_MAX_MEDIA,
CONF_OVERRIDE_CHOST, CONF_OVERRIDE_CHOST,
DEFAULT_MAX_MEDIA, DEFAULT_MAX_MEDIA,
@ -47,7 +46,7 @@ from .const import (
) )
from .data import async_last_update_was_successful from .data import async_last_update_was_successful
from .discovery import async_start_discovery from .discovery import async_start_discovery
from .utils import _async_resolve, async_short_mac, async_unifi_mac, convert_mac_list from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -121,7 +120,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult: ) -> FlowResult:
"""Handle integration discovery.""" """Handle integration discovery."""
self._discovered_device = discovery_info self._discovered_device = discovery_info
mac = async_unifi_mac(discovery_info["hw_addr"]) mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"])
await self.async_set_unique_id(mac) await self.async_set_unique_id(mac)
source_ip = discovery_info["source_ip"] source_ip = discovery_info["source_ip"]
direct_connect_domain = discovery_info["direct_connect_domain"] direct_connect_domain = discovery_info["direct_connect_domain"]
@ -183,7 +182,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
placeholders = { placeholders = {
"name": discovery_info["hostname"] "name": discovery_info["hostname"]
or discovery_info["platform"] or discovery_info["platform"]
or f"NVR {async_short_mac(discovery_info['hw_addr'])}", or f"NVR {_async_short_mac(discovery_info['hw_addr'])}",
"ip_address": discovery_info["source_ip"], "ip_address": discovery_info["source_ip"],
} }
self.context["title_placeholders"] = placeholders self.context["title_placeholders"] = placeholders
@ -225,7 +224,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
CONF_ALL_UPDATES: False, CONF_ALL_UPDATES: False,
CONF_OVERRIDE_CHOST: False, CONF_OVERRIDE_CHOST: False,
CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA,
CONF_IGNORED: "",
}, },
) )
@ -367,53 +365,33 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Manage the options.""" """Manage the options."""
values = user_input or self.config_entry.options
schema = vol.Schema(
{
vol.Optional(
CONF_DISABLE_RTSP,
description={
"suggested_value": values.get(CONF_DISABLE_RTSP, False)
},
): bool,
vol.Optional(
CONF_ALL_UPDATES,
description={
"suggested_value": values.get(CONF_ALL_UPDATES, False)
},
): bool,
vol.Optional(
CONF_OVERRIDE_CHOST,
description={
"suggested_value": values.get(CONF_OVERRIDE_CHOST, False)
},
): bool,
vol.Optional(
CONF_MAX_MEDIA,
description={
"suggested_value": values.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA)
},
): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)),
vol.Optional(
CONF_IGNORED,
description={"suggested_value": values.get(CONF_IGNORED, "")},
): str,
}
)
errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
try: return self.async_create_entry(title="", data=user_input)
convert_mac_list(user_input.get(CONF_IGNORED, ""), raise_exception=True)
except vol.Invalid:
errors[CONF_IGNORED] = "invalid_mac_list"
if not errors:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form( return self.async_show_form(
step_id="init", step_id="init",
data_schema=schema, data_schema=vol.Schema(
errors=errors, {
vol.Optional(
CONF_DISABLE_RTSP,
default=self.config_entry.options.get(CONF_DISABLE_RTSP, False),
): bool,
vol.Optional(
CONF_ALL_UPDATES,
default=self.config_entry.options.get(CONF_ALL_UPDATES, False),
): bool,
vol.Optional(
CONF_OVERRIDE_CHOST,
default=self.config_entry.options.get(
CONF_OVERRIDE_CHOST, False
),
): bool,
vol.Optional(
CONF_MAX_MEDIA,
default=self.config_entry.options.get(
CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA
),
): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)),
}
),
) )

View file

@ -20,7 +20,6 @@ CONF_DISABLE_RTSP = "disable_rtsp"
CONF_ALL_UPDATES = "all_updates" CONF_ALL_UPDATES = "all_updates"
CONF_OVERRIDE_CHOST = "override_connection_host" CONF_OVERRIDE_CHOST = "override_connection_host"
CONF_MAX_MEDIA = "max_media" CONF_MAX_MEDIA = "max_media"
CONF_IGNORED = "ignored_devices"
CONFIG_OPTIONS = [ CONFIG_OPTIONS = [
CONF_ALL_UPDATES, CONF_ALL_UPDATES,

View file

@ -28,7 +28,6 @@ from homeassistant.helpers.event import async_track_time_interval
from .const import ( from .const import (
CONF_DISABLE_RTSP, CONF_DISABLE_RTSP,
CONF_IGNORED,
CONF_MAX_MEDIA, CONF_MAX_MEDIA,
DEFAULT_MAX_MEDIA, DEFAULT_MAX_MEDIA,
DEVICES_THAT_ADOPT, DEVICES_THAT_ADOPT,
@ -37,11 +36,7 @@ from .const import (
DISPATCH_CHANNELS, DISPATCH_CHANNELS,
DOMAIN, DOMAIN,
) )
from .utils import ( from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type
async_dispatch_id as _ufpd,
async_get_devices_by_type,
convert_mac_list,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR]
@ -72,7 +67,6 @@ class ProtectData:
self._hass = hass self._hass = hass
self._entry = entry self._entry = entry
self._existing_options = dict(entry.options)
self._hass = hass self._hass = hass
self._update_interval = update_interval self._update_interval = update_interval
self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {}
@ -80,8 +74,6 @@ class ProtectData:
self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_interval: CALLBACK_TYPE | None = None
self._unsub_websocket: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None
self._auth_failures = 0 self._auth_failures = 0
self._ignored_macs: set[str] | None = None
self._ignore_update_cancel: Callable[[], None] | None = None
self.last_update_success = False self.last_update_success = False
self.api = protect self.api = protect
@ -96,47 +88,6 @@ class ProtectData:
"""Max number of events to load at once.""" """Max number of events to load at once."""
return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA)
@property
def ignored_macs(self) -> set[str]:
"""Set of ignored MAC addresses."""
if self._ignored_macs is None:
self._ignored_macs = convert_mac_list(
self._entry.options.get(CONF_IGNORED, "")
)
return self._ignored_macs
@callback
def async_get_changed_options(self, entry: ConfigEntry) -> dict[str, Any]:
"""Get changed options for when entry is updated."""
return dict(
set(self._entry.options.items()) - set(self._existing_options.items())
)
@callback
def async_ignore_mac(self, mac: str) -> None:
"""Ignores a MAC address for a UniFi Protect device."""
new_macs = (self._ignored_macs or set()).copy()
new_macs.add(mac)
_LOGGER.debug("Updating ignored_devices option: %s", self.ignored_macs)
options = dict(self._entry.options)
options[CONF_IGNORED] = ",".join(new_macs)
self._hass.config_entries.async_update_entry(self._entry, options=options)
@callback
def async_add_new_ignored_macs(self, new_macs: set[str]) -> None:
"""Add new ignored MAC addresses and ensures the devices are removed."""
for mac in new_macs:
device = self.api.bootstrap.get_device_from_mac(mac)
if device is not None:
self._async_remove_device(device)
self._ignored_macs = None
self._existing_options = dict(self._entry.options)
def get_by_types( def get_by_types(
self, device_types: Iterable[ModelType], ignore_unadopted: bool = True self, device_types: Iterable[ModelType], ignore_unadopted: bool = True
) -> Generator[ProtectAdoptableDeviceModel, None, None]: ) -> Generator[ProtectAdoptableDeviceModel, None, None]:
@ -148,8 +99,6 @@ class ProtectData:
for device in devices: for device in devices:
if ignore_unadopted and not device.is_adopted_by_us: if ignore_unadopted and not device.is_adopted_by_us:
continue continue
if device.mac in self.ignored_macs:
continue
yield device yield device
async def async_setup(self) -> None: async def async_setup(self) -> None:
@ -159,11 +108,6 @@ class ProtectData:
) )
await self.async_refresh() await self.async_refresh()
for mac in self.ignored_macs:
device = self.api.bootstrap.get_device_from_mac(mac)
if device is not None:
self._async_remove_device(device)
async def async_stop(self, *args: Any) -> None: async def async_stop(self, *args: Any) -> None:
"""Stop processing data.""" """Stop processing data."""
if self._unsub_websocket: if self._unsub_websocket:
@ -228,7 +172,6 @@ class ProtectData:
@callback @callback
def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None:
registry = dr.async_get(self._hass) registry = dr.async_get(self._hass)
device_entry = registry.async_get_device( device_entry = registry.async_get_device(
identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}
@ -353,13 +296,13 @@ class ProtectData:
@callback @callback
def async_ufp_data_for_config_entry_ids( def async_ufp_instance_for_config_entry_ids(
hass: HomeAssistant, config_entry_ids: set[str] hass: HomeAssistant, config_entry_ids: set[str]
) -> ProtectData | None: ) -> ProtectApiClient | None:
"""Find the UFP instance for the config entry ids.""" """Find the UFP instance for the config entry ids."""
domain_data = hass.data[DOMAIN] domain_data = hass.data[DOMAIN]
for config_entry_id in config_entry_ids: for config_entry_id in config_entry_ids:
if config_entry_id in domain_data: if config_entry_id in domain_data:
protect_data: ProtectData = domain_data[config_entry_id] protect_data: ProtectData = domain_data[config_entry_id]
return protect_data return protect_data.api
return None return None

View file

@ -25,7 +25,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids
from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.read_only_dict import ReadOnlyDict
from .const import ATTR_MESSAGE, DOMAIN from .const import ATTR_MESSAGE, DOMAIN
from .data import async_ufp_data_for_config_entry_ids from .data import async_ufp_instance_for_config_entry_ids
SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text"
SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text"
@ -70,8 +70,8 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl
return _async_get_ufp_instance(hass, device_entry.via_device_id) return _async_get_ufp_instance(hass, device_entry.via_device_id)
config_entry_ids = device_entry.config_entries config_entry_ids = device_entry.config_entries
if ufp_data := async_ufp_data_for_config_entry_ids(hass, config_entry_ids): if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids):
return ufp_data.api return ufp_instance
raise HomeAssistantError(f"No device found for device id: {device_id}") raise HomeAssistantError(f"No device found for device id: {device_id}")

View file

@ -50,13 +50,9 @@
"disable_rtsp": "Disable the RTSP stream", "disable_rtsp": "Disable the RTSP stream",
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
"override_connection_host": "Override Connection Host", "override_connection_host": "Override Connection Host",
"max_media": "Max number of event to load for Media Browser (increases RAM usage)", "max_media": "Max number of event to load for Media Browser (increases RAM usage)"
"ignored_devices": "Comma separated list of MAC addresses of devices to ignore"
} }
} }
},
"error": {
"invalid_mac_list": "Must be a list of MAC addresses seperated by commas"
} }
} }
} }

View file

@ -42,15 +42,11 @@
} }
}, },
"options": { "options": {
"error": {
"invalid_mac_list": "Must be a list of MAC addresses seperated by commas"
},
"step": { "step": {
"init": { "init": {
"data": { "data": {
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
"disable_rtsp": "Disable the RTSP stream", "disable_rtsp": "Disable the RTSP stream",
"ignored_devices": "Comma separated list of MAC addresses of devices to ignore",
"max_media": "Max number of event to load for Media Browser (increases RAM usage)", "max_media": "Max number of event to load for Media Browser (increases RAM usage)",
"override_connection_host": "Override Connection Host" "override_connection_host": "Override Connection Host"
}, },

View file

@ -1,9 +1,9 @@
"""UniFi Protect Integration utils.""" """UniFi Protect Integration utils."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator, Iterable
import contextlib import contextlib
from enum import Enum from enum import Enum
import re
import socket import socket
from typing import Any from typing import Any
@ -14,16 +14,12 @@ from pyunifiprotect.data import (
LightModeType, LightModeType,
ProtectAdoptableDeviceModel, ProtectAdoptableDeviceModel,
) )
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, ModelType from .const import DOMAIN, ModelType
MAC_RE = re.compile(r"[0-9A-F]{12}")
def get_nested_attr(obj: Any, attr: str) -> Any: def get_nested_attr(obj: Any, attr: str) -> Any:
"""Fetch a nested attribute.""" """Fetch a nested attribute."""
@ -42,16 +38,15 @@ def get_nested_attr(obj: Any, attr: str) -> Any:
@callback @callback
def async_unifi_mac(mac: str) -> str: def _async_unifi_mac_from_hass(mac: str) -> str:
"""Convert MAC address to format from UniFi Protect."""
# MAC addresses in UFP are always caps # MAC addresses in UFP are always caps
return mac.replace(":", "").replace("-", "").replace("_", "").upper() return mac.replace(":", "").upper()
@callback @callback
def async_short_mac(mac: str) -> str: def _async_short_mac(mac: str) -> str:
"""Get the short mac address from the full mac.""" """Get the short mac address from the full mac."""
return async_unifi_mac(mac)[-6:] return _async_unifi_mac_from_hass(mac)[-6:]
async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: async def _async_resolve(hass: HomeAssistant, host: str) -> str | None:
@ -82,6 +77,18 @@ def async_get_devices_by_type(
return devices return devices
@callback
def async_get_devices(
bootstrap: Bootstrap, model_type: Iterable[ModelType]
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
"""Return all device by type."""
return (
device
for device_type in model_type
for device in async_get_devices_by_type(bootstrap, device_type).values()
)
@callback @callback
def async_get_light_motion_current(obj: Light) -> str: def async_get_light_motion_current(obj: Light) -> str:
"""Get light motion mode for Flood Light.""" """Get light motion mode for Flood Light."""
@ -99,22 +106,3 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str:
"""Generate entry specific dispatch ID.""" """Generate entry specific dispatch ID."""
return f"{DOMAIN}.{entry.entry_id}.{dispatch}" return f"{DOMAIN}.{entry.entry_id}.{dispatch}"
@callback
def convert_mac_list(option: str, raise_exception: bool = False) -> set[str]:
"""Convert csv list of MAC addresses."""
macs = set()
values = cv.ensure_list_csv(option)
for value in values:
if value == "":
continue
value = async_unifi_mac(value)
if not MAC_RE.match(value):
if raise_exception:
raise vol.Invalid("invalid_mac_list")
continue
macs.add(value)
return macs

View file

@ -68,7 +68,6 @@ def mock_ufp_config_entry():
"port": 443, "port": 443,
"verify_ssl": False, "verify_ssl": False,
}, },
options={"ignored_devices": "FFFFFFFFFFFF,test"},
version=2, version=2,
) )

View file

@ -14,7 +14,6 @@ from homeassistant.components import dhcp, ssdp
from homeassistant.components.unifiprotect.const import ( from homeassistant.components.unifiprotect.const import (
CONF_ALL_UPDATES, CONF_ALL_UPDATES,
CONF_DISABLE_RTSP, CONF_DISABLE_RTSP,
CONF_IGNORED,
CONF_OVERRIDE_CHOST, CONF_OVERRIDE_CHOST,
DOMAIN, DOMAIN,
) )
@ -270,52 +269,10 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -
"all_updates": True, "all_updates": True,
"disable_rtsp": True, "disable_rtsp": True,
"override_connection_host": True, "override_connection_host": True,
"max_media": 1000,
} }
async def test_form_options_invalid_mac(
hass: HomeAssistant, ufp_client: ProtectApiClient
) -> None:
"""Test we handle options flows."""
mock_config = MockConfigEntry(
domain=DOMAIN,
data={
"host": "1.1.1.1",
"username": "test-username",
"password": "test-password",
"id": "UnifiProtect",
"port": 443,
"verify_ssl": False,
"max_media": 1000,
},
version=2,
unique_id=dr.format_mac(MAC_ADDR),
)
mock_config.add_to_hass(hass)
with _patch_discovery(), patch(
"homeassistant.components.unifiprotect.ProtectApiClient"
) as mock_api:
mock_api.return_value = ufp_client
await hass.config_entries.async_setup(mock_config.entry_id)
await hass.async_block_till_done()
assert mock_config.state == config_entries.ConfigEntryState.LOADED
result = await hass.config_entries.options.async_init(mock_config.entry_id)
assert result["type"] == FlowResultType.FORM
assert not result["errors"]
assert result["step_id"] == "init"
result2 = await hass.config_entries.options.async_configure(
result["flow_id"],
{CONF_IGNORED: "test,test2"},
)
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {CONF_IGNORED: "invalid_mac_list"}
@pytest.mark.parametrize( @pytest.mark.parametrize(
"source, data", "source, data",
[ [

View file

@ -7,21 +7,20 @@ from unittest.mock import AsyncMock, patch
import aiohttp import aiohttp
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, Doorlock, Light, Sensor from pyunifiprotect.data import NVR, Bootstrap, Light
from homeassistant.components.unifiprotect.const import ( from homeassistant.components.unifiprotect.const import (
CONF_DISABLE_RTSP, CONF_DISABLE_RTSP,
CONF_IGNORED,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DOMAIN, DOMAIN,
) )
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import _patch_discovery from . import _patch_discovery
from .utils import MockUFPFixture, get_device_from_ufp_device, init_entry, time_changed from .utils import MockUFPFixture, init_entry, time_changed
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -212,38 +211,28 @@ async def test_device_remove_devices(
hass: HomeAssistant, hass: HomeAssistant,
ufp: MockUFPFixture, ufp: MockUFPFixture,
light: Light, light: Light,
doorlock: Doorlock,
sensor: Sensor,
hass_ws_client: Callable[ hass_ws_client: Callable[
[HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
], ],
) -> None: ) -> None:
"""Test we can only remove a device that no longer exists.""" """Test we can only remove a device that no longer exists."""
sensor.mac = "FFFFFFFFFFFF" await init_entry(hass, ufp, [light])
await init_entry(hass, ufp, [light, doorlock, sensor], regenerate_ids=False)
assert await async_setup_component(hass, "config", {}) assert await async_setup_component(hass, "config", {})
entity_id = "light.test_light"
entry_id = ufp.entry.entry_id entry_id = ufp.entry.entry_id
registry: er.EntityRegistry = er.async_get(hass)
entity = registry.async_get(entity_id)
assert entity is not None
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
light_device = get_device_from_ufp_device(hass, light) live_device_entry = device_registry.async_get(entity.device_id)
assert light_device is not None
assert ( assert (
await remove_device(await hass_ws_client(hass), light_device.id, entry_id) await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id)
is True is False
) )
doorlock_device = get_device_from_ufp_device(hass, doorlock)
assert (
await remove_device(await hass_ws_client(hass), doorlock_device.id, entry_id)
is True
)
sensor_device = get_device_from_ufp_device(hass, sensor)
assert sensor_device is None
dead_device_entry = device_registry.async_get_or_create( dead_device_entry = device_registry.async_get_or_create(
config_entry_id=entry_id, config_entry_id=entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")}, connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")},
@ -253,10 +242,6 @@ async def test_device_remove_devices(
is True is True
) )
await time_changed(hass, 60)
entry = hass.config_entries.async_get_entry(entry_id)
entry.options[CONF_IGNORED] == f"{light.mac},{doorlock.mac}"
async def test_device_remove_devices_nvr( async def test_device_remove_devices_nvr(
hass: HomeAssistant, hass: HomeAssistant,

View file

@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.core import HomeAssistant, split_entity_id
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -229,13 +229,3 @@ async def adopt_devices(
ufp.ws_msg(mock_msg) ufp.ws_msg(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
def get_device_from_ufp_device(
hass: HomeAssistant, device: ProtectAdoptableDeviceModel
) -> dr.DeviceEntry | None:
"""Return all device by type."""
registry = dr.async_get(hass)
return registry.async_get_device(
identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}
)