diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 60829223e2f..30b1d1ad56d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -26,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( CONF_ALL_UPDATES, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DEFAULT_SCAN_INTERVAL, DEVICES_FOR_SUBSCRIBE, @@ -36,11 +35,11 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData +from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .migrate import async_migrate_data 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 _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: """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) @@ -139,15 +125,15 @@ async def async_remove_config_entry_device( ) -> bool: """Remove ufp config entry from a device.""" unifi_macs = { - async_unifi_mac(connection[1]) + _async_unifi_mac_from_hass(connection[1]) for connection in device_entry.connections if connection[0] == dr.CONNECTION_NETWORK_MAC } - data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] - if data.api.bootstrap.nvr.mac in unifi_macs: + api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) + assert api is not None + if api.bootstrap.nvr.mac in unifi_macs: 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: - data.async_ignore_mac(device.mac) - break + return False return True diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 1907a201c8d..f07ca923a53 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -35,7 +35,6 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, DEFAULT_MAX_MEDIA, @@ -47,7 +46,7 @@ from .const import ( ) from .data import async_last_update_was_successful 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__) @@ -121,7 +120,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle integration discovery.""" 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) source_ip = discovery_info["source_ip"] direct_connect_domain = discovery_info["direct_connect_domain"] @@ -183,7 +182,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = { "name": discovery_info["hostname"] 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"], } self.context["title_placeholders"] = placeholders @@ -225,7 +224,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, 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 ) -> FlowResult: """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: - try: - 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_create_entry(title="", data=user_input) return self.async_show_form( step_id="init", - data_schema=schema, - errors=errors, + data_schema=vol.Schema( + { + 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)), + } + ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 080dc41f358..93a0fa5ff74 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -20,7 +20,6 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" -CONF_IGNORED = "ignored_devices" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index c17b99d639f..20b5747a342 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -28,7 +28,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, @@ -37,11 +36,7 @@ from .const import ( DISPATCH_CHANNELS, DOMAIN, ) -from .utils import ( - async_dispatch_id as _ufpd, - async_get_devices_by_type, - convert_mac_list, -) +from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] @@ -72,7 +67,6 @@ class ProtectData: self._hass = hass self._entry = entry - self._existing_options = dict(entry.options) self._hass = hass self._update_interval = update_interval self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} @@ -80,8 +74,6 @@ class ProtectData: self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None 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.api = protect @@ -96,47 +88,6 @@ class ProtectData: """Max number of events to load at once.""" 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( self, device_types: Iterable[ModelType], ignore_unadopted: bool = True ) -> Generator[ProtectAdoptableDeviceModel, None, None]: @@ -148,8 +99,6 @@ class ProtectData: for device in devices: if ignore_unadopted and not device.is_adopted_by_us: continue - if device.mac in self.ignored_macs: - continue yield device async def async_setup(self) -> None: @@ -159,11 +108,6 @@ class ProtectData: ) 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: """Stop processing data.""" if self._unsub_websocket: @@ -228,7 +172,6 @@ class ProtectData: @callback def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None: - registry = dr.async_get(self._hass) device_entry = registry.async_get_device( identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)} @@ -353,13 +296,13 @@ class ProtectData: @callback -def async_ufp_data_for_config_entry_ids( +def async_ufp_instance_for_config_entry_ids( hass: HomeAssistant, config_entry_ids: set[str] -) -> ProtectData | None: +) -> ProtectApiClient | None: """Find the UFP instance for the config entry ids.""" domain_data = hass.data[DOMAIN] for config_entry_id in config_entry_ids: if config_entry_id in domain_data: protect_data: ProtectData = domain_data[config_entry_id] - return protect_data + return protect_data.api return None diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 914803e9c45..915c51b6c0a 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -25,7 +25,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids from homeassistant.util.read_only_dict import ReadOnlyDict 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_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) config_entry_ids = device_entry.config_entries - if ufp_data := async_ufp_data_for_config_entry_ids(hass, config_entry_ids): - return ufp_data.api + if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): + return ufp_instance raise HomeAssistantError(f"No device found for device id: {device_id}") diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d9750d31ae1..d3cfe24abd2 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -50,13 +50,9 @@ "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "override_connection_host": "Override Connection Host", - "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" + "max_media": "Max number of event to load for Media Browser (increases RAM usage)" } } - }, - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index c6050d05284..5d690e3fd3e 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -42,15 +42,11 @@ } }, "options": { - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" - }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "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)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 8c368da1c40..808117aac9e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,9 +1,9 @@ """UniFi Protect Integration utils.""" from __future__ import annotations +from collections.abc import Generator, Iterable import contextlib from enum import Enum -import re import socket from typing import Any @@ -14,16 +14,12 @@ from pyunifiprotect.data import ( LightModeType, ProtectAdoptableDeviceModel, ) -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv from .const import DOMAIN, ModelType -MAC_RE = re.compile(r"[0-9A-F]{12}") - def get_nested_attr(obj: Any, attr: str) -> Any: """Fetch a nested attribute.""" @@ -42,16 +38,15 @@ def get_nested_attr(obj: Any, attr: str) -> Any: @callback -def async_unifi_mac(mac: str) -> str: - """Convert MAC address to format from UniFi Protect.""" +def _async_unifi_mac_from_hass(mac: str) -> str: # MAC addresses in UFP are always caps - return mac.replace(":", "").replace("-", "").replace("_", "").upper() + return mac.replace(":", "").upper() @callback -def async_short_mac(mac: str) -> str: +def _async_short_mac(mac: str) -> str: """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: @@ -82,6 +77,18 @@ def async_get_devices_by_type( 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 def async_get_light_motion_current(obj: Light) -> str: """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.""" 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 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index fa245e8b1cc..b006dfbd004 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -68,7 +68,6 @@ def mock_ufp_config_entry(): "port": 443, "verify_ssl": False, }, - options={"ignored_devices": "FFFFFFFFFFFF,test"}, version=2, ) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 26a9dd73ee8..d0fb0dba9f2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.components import dhcp, ssdp from homeassistant.components.unifiprotect.const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, - CONF_IGNORED, CONF_OVERRIDE_CHOST, DOMAIN, ) @@ -270,52 +269,10 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "all_updates": True, "disable_rtsp": 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( "source, data", [ diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 7a1e590b87d..9392caa30ac 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -7,21 +7,20 @@ from unittest.mock import AsyncMock, patch import aiohttp 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 ( CONF_DISABLE_RTSP, - CONF_IGNORED, DEFAULT_SCAN_INTERVAL, DOMAIN, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState 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 . 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 @@ -212,38 +211,28 @@ async def test_device_remove_devices( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, - doorlock: Doorlock, - sensor: Sensor, hass_ws_client: Callable[ [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] ], ) -> None: """Test we can only remove a device that no longer exists.""" - sensor.mac = "FFFFFFFFFFFF" - - await init_entry(hass, ufp, [light, doorlock, sensor], regenerate_ids=False) + await init_entry(hass, ufp, [light]) assert await async_setup_component(hass, "config", {}) - + entity_id = "light.test_light" 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) - light_device = get_device_from_ufp_device(hass, light) - assert light_device is not None + live_device_entry = device_registry.async_get(entity.device_id) assert ( - await remove_device(await hass_ws_client(hass), light_device.id, entry_id) - is True + await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) + 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( config_entry_id=entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")}, @@ -253,10 +242,6 @@ async def test_device_remove_devices( 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( hass: HomeAssistant, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 3376db4ec51..bee479b8e2b 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform 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 import homeassistant.util.dt as dt_util @@ -229,13 +229,3 @@ async def adopt_devices( ufp.ws_msg(mock_msg) 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)} - )