2018-08-22 08:46:37 +00:00
|
|
|
"""Provide a way to connect entities belonging to one device."""
|
2018-08-27 22:37:04 +00:00
|
|
|
from collections import OrderedDict
|
2019-12-09 15:42:10 +00:00
|
|
|
import logging
|
2020-05-05 17:53:46 +00:00
|
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
2019-12-09 15:42:10 +00:00
|
|
|
import uuid
|
2018-08-27 22:37:04 +00:00
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
import attr
|
|
|
|
|
2020-05-05 17:53:46 +00:00
|
|
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
|
|
|
from homeassistant.core import Event, callback
|
2018-08-22 08:46:37 +00:00
|
|
|
|
2020-05-05 17:53:46 +00:00
|
|
|
from .debounce import Debouncer
|
2020-04-30 23:47:14 +00:00
|
|
|
from .singleton import singleton
|
2019-03-27 14:06:20 +00:00
|
|
|
from .typing import HomeAssistantType
|
|
|
|
|
2020-05-05 17:53:46 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from . import entity_registry
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs
|
2019-07-21 16:59:02 +00:00
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2018-09-17 11:39:30 +00:00
|
|
|
_UNDEF = object()
|
2018-08-22 08:46:37 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_REGISTRY = "device_registry"
|
|
|
|
EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated"
|
|
|
|
STORAGE_KEY = "core.device_registry"
|
2018-08-22 08:46:37 +00:00
|
|
|
STORAGE_VERSION = 1
|
|
|
|
SAVE_DELAY = 10
|
2020-05-05 17:53:46 +00:00
|
|
|
CLEANUP_DELAY = 10
|
2018-08-22 08:46:37 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
CONNECTION_NETWORK_MAC = "mac"
|
|
|
|
CONNECTION_UPNP = "upnp"
|
|
|
|
CONNECTION_ZIGBEE = "zigbee"
|
2018-08-24 17:37:22 +00:00
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
|
|
|
|
@attr.s(slots=True, frozen=True)
|
|
|
|
class DeviceEntry:
|
|
|
|
"""Device Registry Entry."""
|
|
|
|
|
2020-05-03 20:56:58 +00:00
|
|
|
config_entries: Set[str] = attr.ib(converter=set, default=attr.Factory(set))
|
|
|
|
connections: Set[Tuple[str, str]] = attr.ib(
|
|
|
|
converter=set, default=attr.Factory(set)
|
|
|
|
)
|
|
|
|
identifiers: Set[Tuple[str, str]] = attr.ib(
|
|
|
|
converter=set, default=attr.Factory(set)
|
|
|
|
)
|
|
|
|
manufacturer: str = attr.ib(default=None)
|
|
|
|
model: str = attr.ib(default=None)
|
|
|
|
name: str = attr.ib(default=None)
|
|
|
|
sw_version: str = attr.ib(default=None)
|
|
|
|
via_device_id: str = attr.ib(default=None)
|
|
|
|
area_id: str = attr.ib(default=None)
|
|
|
|
name_by_user: str = attr.ib(default=None)
|
|
|
|
entry_type: str = attr.ib(default=None)
|
|
|
|
id: str = attr.ib(default=attr.Factory(lambda: uuid.uuid4().hex))
|
2019-05-08 03:04:57 +00:00
|
|
|
# This value is not stored, just used to keep track of events to fire.
|
2020-05-03 20:56:58 +00:00
|
|
|
is_new: bool = attr.ib(default=False)
|
2018-08-22 08:46:37 +00:00
|
|
|
|
|
|
|
|
2019-12-21 07:23:48 +00:00
|
|
|
def format_mac(mac: str) -> str:
|
2018-11-06 15:33:31 +00:00
|
|
|
"""Format the mac address string for entry into dev reg."""
|
|
|
|
to_test = mac
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if len(to_test) == 17 and to_test.count(":") == 5:
|
2018-11-06 15:33:31 +00:00
|
|
|
return to_test.lower()
|
2018-11-06 18:27:52 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if len(to_test) == 17 and to_test.count("-") == 5:
|
|
|
|
to_test = to_test.replace("-", "")
|
|
|
|
elif len(to_test) == 14 and to_test.count(".") == 2:
|
|
|
|
to_test = to_test.replace(".", "")
|
2018-11-06 15:33:31 +00:00
|
|
|
|
|
|
|
if len(to_test) == 12:
|
|
|
|
# no : included
|
2019-07-31 19:25:30 +00:00
|
|
|
return ":".join(to_test.lower()[i : i + 2] for i in range(0, 12, 2))
|
2018-11-06 15:33:31 +00:00
|
|
|
|
|
|
|
# Not sure how formatted, return original
|
|
|
|
return mac
|
|
|
|
|
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
class DeviceRegistry:
|
|
|
|
"""Class to hold a registry of devices."""
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
devices: Dict[str, DeviceEntry]
|
|
|
|
|
|
|
|
def __init__(self, hass: HomeAssistantType) -> None:
|
2018-08-22 08:46:37 +00:00
|
|
|
"""Initialize the device registry."""
|
|
|
|
self.hass = hass
|
|
|
|
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
|
|
|
|
2019-03-11 18:02:37 +00:00
|
|
|
@callback
|
|
|
|
def async_get(self, device_id: str) -> Optional[DeviceEntry]:
|
|
|
|
"""Get device."""
|
|
|
|
return self.devices.get(device_id)
|
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
@callback
|
2019-08-15 15:53:25 +00:00
|
|
|
def async_get_device(
|
|
|
|
self, identifiers: set, connections: set
|
|
|
|
) -> Optional[DeviceEntry]:
|
2018-08-22 08:46:37 +00:00
|
|
|
"""Check if device is registered."""
|
2018-08-27 22:37:04 +00:00
|
|
|
for device in self.devices.values():
|
2019-07-31 19:25:30 +00:00
|
|
|
if any(iden in device.identifiers for iden in identifiers) or any(
|
|
|
|
conn in device.connections for conn in connections
|
|
|
|
):
|
2018-08-22 08:46:37 +00:00
|
|
|
return device
|
|
|
|
return None
|
|
|
|
|
|
|
|
@callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_get_or_create(
|
|
|
|
self,
|
|
|
|
*,
|
|
|
|
config_entry_id,
|
|
|
|
connections=None,
|
|
|
|
identifiers=None,
|
|
|
|
manufacturer=_UNDEF,
|
|
|
|
model=_UNDEF,
|
|
|
|
name=_UNDEF,
|
|
|
|
sw_version=_UNDEF,
|
2020-05-03 20:56:58 +00:00
|
|
|
entry_type=_UNDEF,
|
2019-07-31 19:25:30 +00:00
|
|
|
via_device=None,
|
|
|
|
):
|
2018-08-22 08:46:37 +00:00
|
|
|
"""Get device. Create if it doesn't exist."""
|
2018-08-25 08:59:28 +00:00
|
|
|
if not identifiers and not connections:
|
|
|
|
return None
|
|
|
|
|
2018-09-27 09:26:58 +00:00
|
|
|
if identifiers is None:
|
|
|
|
identifiers = set()
|
|
|
|
|
|
|
|
if connections is None:
|
|
|
|
connections = set()
|
|
|
|
|
2018-11-06 15:33:31 +00:00
|
|
|
connections = {
|
2019-07-31 19:25:30 +00:00
|
|
|
(key, format_mac(value)) if key == CONNECTION_NETWORK_MAC else (key, value)
|
2018-11-06 15:33:31 +00:00
|
|
|
for key, value in connections
|
|
|
|
}
|
|
|
|
|
2018-08-25 08:59:28 +00:00
|
|
|
device = self.async_get_device(identifiers, connections)
|
2018-08-22 08:46:37 +00:00
|
|
|
|
2018-09-27 09:26:58 +00:00
|
|
|
if device is None:
|
2019-05-08 03:04:57 +00:00
|
|
|
device = DeviceEntry(is_new=True)
|
2018-09-27 09:26:58 +00:00
|
|
|
self.devices[device.id] = device
|
|
|
|
|
2019-06-10 16:10:44 +00:00
|
|
|
if via_device is not None:
|
|
|
|
via = self.async_get_device({via_device}, set())
|
|
|
|
via_device_id = via.id if via else _UNDEF
|
2018-09-17 11:39:30 +00:00
|
|
|
else:
|
2019-06-10 16:10:44 +00:00
|
|
|
via_device_id = _UNDEF
|
2018-09-27 09:26:58 +00:00
|
|
|
|
|
|
|
return self._async_update_device(
|
|
|
|
device.id,
|
|
|
|
add_config_entry_id=config_entry_id,
|
2019-06-10 16:10:44 +00:00
|
|
|
via_device_id=via_device_id,
|
2018-10-25 14:43:11 +00:00
|
|
|
merge_connections=connections or _UNDEF,
|
|
|
|
merge_identifiers=identifiers or _UNDEF,
|
2018-08-22 08:46:37 +00:00
|
|
|
manufacturer=manufacturer,
|
|
|
|
model=model,
|
|
|
|
name=name,
|
2019-07-31 19:25:30 +00:00
|
|
|
sw_version=sw_version,
|
2020-05-03 20:56:58 +00:00
|
|
|
entry_type=entry_type,
|
2018-08-22 08:46:37 +00:00
|
|
|
)
|
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
@callback
|
2019-02-26 20:20:16 +00:00
|
|
|
def async_update_device(
|
2019-07-31 19:25:30 +00:00
|
|
|
self,
|
|
|
|
device_id,
|
|
|
|
*,
|
|
|
|
area_id=_UNDEF,
|
2020-04-21 17:40:16 +00:00
|
|
|
manufacturer=_UNDEF,
|
|
|
|
model=_UNDEF,
|
2019-07-31 19:25:30 +00:00
|
|
|
name=_UNDEF,
|
|
|
|
name_by_user=_UNDEF,
|
|
|
|
new_identifiers=_UNDEF,
|
2020-03-11 16:31:02 +00:00
|
|
|
sw_version=_UNDEF,
|
2019-07-31 19:25:30 +00:00
|
|
|
via_device_id=_UNDEF,
|
2019-09-25 21:00:18 +00:00
|
|
|
remove_config_entry_id=_UNDEF,
|
2019-07-31 19:25:30 +00:00
|
|
|
):
|
2019-01-28 23:52:42 +00:00
|
|
|
"""Update properties of a device."""
|
2019-02-26 20:20:16 +00:00
|
|
|
return self._async_update_device(
|
2019-07-31 19:25:30 +00:00
|
|
|
device_id,
|
|
|
|
area_id=area_id,
|
2020-04-21 17:40:16 +00:00
|
|
|
manufacturer=manufacturer,
|
|
|
|
model=model,
|
2019-07-31 19:25:30 +00:00
|
|
|
name=name,
|
|
|
|
name_by_user=name_by_user,
|
|
|
|
new_identifiers=new_identifiers,
|
2020-03-11 16:31:02 +00:00
|
|
|
sw_version=sw_version,
|
2019-07-31 19:25:30 +00:00
|
|
|
via_device_id=via_device_id,
|
2019-09-25 21:00:18 +00:00
|
|
|
remove_config_entry_id=remove_config_entry_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2018-09-17 11:39:30 +00:00
|
|
|
@callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def _async_update_device(
|
|
|
|
self,
|
|
|
|
device_id,
|
|
|
|
*,
|
|
|
|
add_config_entry_id=_UNDEF,
|
|
|
|
remove_config_entry_id=_UNDEF,
|
|
|
|
merge_connections=_UNDEF,
|
|
|
|
merge_identifiers=_UNDEF,
|
|
|
|
new_identifiers=_UNDEF,
|
|
|
|
manufacturer=_UNDEF,
|
|
|
|
model=_UNDEF,
|
|
|
|
name=_UNDEF,
|
|
|
|
sw_version=_UNDEF,
|
2020-05-03 20:56:58 +00:00
|
|
|
entry_type=_UNDEF,
|
2019-07-31 19:25:30 +00:00
|
|
|
via_device_id=_UNDEF,
|
|
|
|
area_id=_UNDEF,
|
|
|
|
name_by_user=_UNDEF,
|
|
|
|
):
|
2018-09-17 11:39:30 +00:00
|
|
|
"""Update device attributes."""
|
|
|
|
old = self.devices[device_id]
|
|
|
|
|
|
|
|
changes = {}
|
|
|
|
|
|
|
|
config_entries = old.config_entries
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
add_config_entry_id is not _UNDEF
|
|
|
|
and add_config_entry_id not in old.config_entries
|
|
|
|
):
|
2018-09-27 09:26:58 +00:00
|
|
|
config_entries = old.config_entries | {add_config_entry_id}
|
2018-09-17 11:39:30 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if (
|
|
|
|
remove_config_entry_id is not _UNDEF
|
|
|
|
and remove_config_entry_id in config_entries
|
|
|
|
):
|
2019-09-25 21:00:18 +00:00
|
|
|
if config_entries == {remove_config_entry_id}:
|
|
|
|
self.async_remove_device(device_id)
|
|
|
|
return
|
|
|
|
|
2018-09-27 09:26:58 +00:00
|
|
|
config_entries = config_entries - {remove_config_entry_id}
|
2018-09-17 11:39:30 +00:00
|
|
|
|
|
|
|
if config_entries is not old.config_entries:
|
2019-07-31 19:25:30 +00:00
|
|
|
changes["config_entries"] = config_entries
|
2018-09-17 11:39:30 +00:00
|
|
|
|
2018-09-27 09:26:58 +00:00
|
|
|
for attr_name, value in (
|
2019-07-31 19:25:30 +00:00
|
|
|
("connections", merge_connections),
|
|
|
|
("identifiers", merge_identifiers),
|
2018-09-27 09:26:58 +00:00
|
|
|
):
|
|
|
|
old_value = getattr(old, attr_name)
|
2018-10-25 14:43:11 +00:00
|
|
|
# If not undefined, check if `value` contains new items.
|
|
|
|
if value is not _UNDEF and not value.issubset(old_value):
|
2018-09-27 09:26:58 +00:00
|
|
|
changes[attr_name] = old_value | value
|
|
|
|
|
2019-04-30 17:04:37 +00:00
|
|
|
if new_identifiers is not _UNDEF:
|
2019-07-31 19:25:30 +00:00
|
|
|
changes["identifiers"] = new_identifiers
|
2019-04-30 17:04:37 +00:00
|
|
|
|
2018-09-27 09:26:58 +00:00
|
|
|
for attr_name, value in (
|
2019-07-31 19:25:30 +00:00
|
|
|
("manufacturer", manufacturer),
|
|
|
|
("model", model),
|
|
|
|
("name", name),
|
|
|
|
("sw_version", sw_version),
|
2020-05-03 20:56:58 +00:00
|
|
|
("entry_type", entry_type),
|
2019-07-31 19:25:30 +00:00
|
|
|
("via_device_id", via_device_id),
|
2018-09-27 09:26:58 +00:00
|
|
|
):
|
|
|
|
if value is not _UNDEF and value != getattr(old, attr_name):
|
|
|
|
changes[attr_name] = value
|
2018-09-17 11:39:30 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if area_id is not _UNDEF and area_id != old.area_id:
|
|
|
|
changes["area_id"] = area_id
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if name_by_user is not _UNDEF and name_by_user != old.name_by_user:
|
|
|
|
changes["name_by_user"] = name_by_user
|
2019-02-26 20:20:16 +00:00
|
|
|
|
2019-05-08 03:04:57 +00:00
|
|
|
if old.is_new:
|
2019-07-31 19:25:30 +00:00
|
|
|
changes["is_new"] = False
|
2019-05-08 03:04:57 +00:00
|
|
|
|
2018-09-17 11:39:30 +00:00
|
|
|
if not changes:
|
|
|
|
return old
|
|
|
|
|
|
|
|
new = self.devices[device_id] = attr.evolve(old, **changes)
|
|
|
|
self.async_schedule_save()
|
2019-05-08 03:04:57 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(
|
|
|
|
EVENT_DEVICE_REGISTRY_UPDATED,
|
|
|
|
{
|
|
|
|
"action": "create" if "is_new" in changes else "update",
|
|
|
|
"device_id": new.id,
|
|
|
|
},
|
|
|
|
)
|
2019-05-08 03:04:57 +00:00
|
|
|
|
2018-09-17 11:39:30 +00:00
|
|
|
return new
|
|
|
|
|
2020-01-29 21:59:45 +00:00
|
|
|
@callback
|
2019-12-21 07:23:48 +00:00
|
|
|
def async_remove_device(self, device_id: str) -> None:
|
2019-06-24 18:26:45 +00:00
|
|
|
"""Remove a device from the device registry."""
|
2019-05-19 09:41:39 +00:00
|
|
|
del self.devices[device_id]
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(
|
|
|
|
EVENT_DEVICE_REGISTRY_UPDATED, {"action": "remove", "device_id": device_id}
|
|
|
|
)
|
2019-05-19 09:41:39 +00:00
|
|
|
self.async_schedule_save()
|
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
async def async_load(self):
|
|
|
|
"""Load the device registry."""
|
2020-05-05 17:53:46 +00:00
|
|
|
async_setup_cleanup(self.hass, self)
|
|
|
|
|
2018-09-17 11:39:30 +00:00
|
|
|
data = await self._store.async_load()
|
|
|
|
|
|
|
|
devices = OrderedDict()
|
|
|
|
|
|
|
|
if data is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
for device in data["devices"]:
|
|
|
|
devices[device["id"]] = DeviceEntry(
|
|
|
|
config_entries=set(device["config_entries"]),
|
|
|
|
connections={tuple(conn) for conn in device["connections"]},
|
|
|
|
identifiers={tuple(iden) for iden in device["identifiers"]},
|
|
|
|
manufacturer=device["manufacturer"],
|
|
|
|
model=device["model"],
|
|
|
|
name=device["name"],
|
|
|
|
sw_version=device["sw_version"],
|
2020-05-03 20:56:58 +00:00
|
|
|
# Introduced in 0.110
|
|
|
|
entry_type=device.get("entry_type"),
|
2019-07-31 19:25:30 +00:00
|
|
|
id=device["id"],
|
2018-09-17 11:39:30 +00:00
|
|
|
# Introduced in 0.79
|
2019-06-10 16:10:44 +00:00
|
|
|
# renamed in 0.95
|
|
|
|
via_device_id=(
|
2019-07-31 19:25:30 +00:00
|
|
|
device.get("via_device_id") or device.get("hub_device_id")
|
|
|
|
),
|
2019-01-28 23:52:42 +00:00
|
|
|
# Introduced in 0.87
|
2019-07-31 19:25:30 +00:00
|
|
|
area_id=device.get("area_id"),
|
|
|
|
name_by_user=device.get("name_by_user"),
|
2018-09-17 11:39:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
self.devices = devices
|
2018-08-22 08:46:37 +00:00
|
|
|
|
|
|
|
@callback
|
2019-12-21 07:23:48 +00:00
|
|
|
def async_schedule_save(self) -> None:
|
2018-08-22 08:46:37 +00:00
|
|
|
"""Schedule saving the device registry."""
|
|
|
|
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
|
|
|
|
|
|
|
|
@callback
|
2019-12-21 07:23:48 +00:00
|
|
|
def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]:
|
2018-08-24 08:28:43 +00:00
|
|
|
"""Return data of device registry to store in a file."""
|
2018-08-22 08:46:37 +00:00
|
|
|
data = {}
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
data["devices"] = [
|
2018-08-22 08:46:37 +00:00
|
|
|
{
|
2019-07-31 19:25:30 +00:00
|
|
|
"config_entries": list(entry.config_entries),
|
|
|
|
"connections": list(entry.connections),
|
|
|
|
"identifiers": list(entry.identifiers),
|
|
|
|
"manufacturer": entry.manufacturer,
|
|
|
|
"model": entry.model,
|
|
|
|
"name": entry.name,
|
|
|
|
"sw_version": entry.sw_version,
|
2020-05-03 20:56:58 +00:00
|
|
|
"entry_type": entry.entry_type,
|
2019-07-31 19:25:30 +00:00
|
|
|
"id": entry.id,
|
|
|
|
"via_device_id": entry.via_device_id,
|
|
|
|
"area_id": entry.area_id,
|
|
|
|
"name_by_user": entry.name_by_user,
|
|
|
|
}
|
|
|
|
for entry in self.devices.values()
|
2018-08-22 08:46:37 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
2018-09-04 07:00:14 +00:00
|
|
|
@callback
|
2019-12-21 07:23:48 +00:00
|
|
|
def async_clear_config_entry(self, config_entry_id: str) -> None:
|
2018-09-04 07:00:14 +00:00
|
|
|
"""Clear config entry from registry entries."""
|
2020-05-05 17:53:46 +00:00
|
|
|
for device in list(self.devices.values()):
|
|
|
|
self._async_update_device(device.id, remove_config_entry_id=config_entry_id)
|
2018-09-04 07:00:14 +00:00
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
@callback
|
|
|
|
def async_clear_area_id(self, area_id: str) -> None:
|
|
|
|
"""Clear area id from registry entries."""
|
|
|
|
for dev_id, device in self.devices.items():
|
|
|
|
if area_id == device.area_id:
|
|
|
|
self._async_update_device(dev_id, area_id=None)
|
|
|
|
|
2018-08-22 08:46:37 +00:00
|
|
|
|
2020-04-30 23:47:14 +00:00
|
|
|
@singleton(DATA_REGISTRY)
|
2019-03-27 14:06:20 +00:00
|
|
|
async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry:
|
2020-04-30 23:47:14 +00:00
|
|
|
"""Create entity registry."""
|
|
|
|
reg = DeviceRegistry(hass)
|
|
|
|
await reg.async_load()
|
|
|
|
return reg
|
2019-03-04 17:51:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
2019-07-31 19:25:30 +00:00
|
|
|
def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> List[DeviceEntry]:
|
2019-03-04 17:51:12 +00:00
|
|
|
"""Return entries that match an area."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return [device for device in registry.devices.values() if device.area_id == area_id]
|
2020-01-10 18:57:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_entries_for_config_entry(
|
|
|
|
registry: DeviceRegistry, config_entry_id: str
|
|
|
|
) -> List[DeviceEntry]:
|
|
|
|
"""Return entries that match a config entry."""
|
|
|
|
return [
|
|
|
|
device
|
|
|
|
for device in registry.devices.values()
|
|
|
|
if config_entry_id in device.config_entries
|
|
|
|
]
|
2020-05-05 17:53:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_cleanup(
|
|
|
|
hass: HomeAssistantType,
|
|
|
|
dev_reg: DeviceRegistry,
|
|
|
|
ent_reg: "entity_registry.EntityRegistry",
|
|
|
|
) -> None:
|
|
|
|
"""Clean up device registry."""
|
|
|
|
# Find all devices that are no longer referenced in the entity registry.
|
|
|
|
referenced = {entry.device_id for entry in ent_reg.entities.values()}
|
|
|
|
orphan = set(dev_reg.devices) - referenced
|
|
|
|
|
|
|
|
for dev_id in orphan:
|
|
|
|
dev_reg.async_remove_device(dev_id)
|
|
|
|
|
|
|
|
# Find all referenced config entries that no longer exist
|
|
|
|
# This shouldn't happen but have not been able to track down the bug :(
|
|
|
|
config_entry_ids = {entry.entry_id for entry in hass.config_entries.async_entries()}
|
|
|
|
|
|
|
|
for device in list(dev_reg.devices.values()):
|
|
|
|
for config_entry_id in device.config_entries:
|
|
|
|
if config_entry_id not in config_entry_ids:
|
|
|
|
dev_reg.async_update_device(
|
|
|
|
device.id, remove_config_entry_id=config_entry_id
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> None:
|
|
|
|
"""Clean up device registry when entities removed."""
|
|
|
|
from . import entity_registry # pylint: disable=import-outside-toplevel
|
|
|
|
|
|
|
|
async def cleanup():
|
|
|
|
"""Cleanup."""
|
|
|
|
ent_reg = await entity_registry.async_get_registry(hass)
|
|
|
|
async_cleanup(hass, dev_reg, ent_reg)
|
|
|
|
|
|
|
|
debounced_cleanup = Debouncer(
|
|
|
|
hass, _LOGGER, cooldown=CLEANUP_DELAY, immediate=False, function=cleanup
|
|
|
|
)
|
|
|
|
|
|
|
|
async def entity_registry_changed(event: Event) -> None:
|
|
|
|
"""Handle entity updated or removed."""
|
|
|
|
if (
|
|
|
|
event.data["action"] == "update"
|
|
|
|
and "device_id" not in event.data["changes"]
|
|
|
|
) or event.data["action"] == "create":
|
|
|
|
return
|
|
|
|
|
|
|
|
await debounced_cleanup.async_call()
|
|
|
|
|
|
|
|
if hass.is_running:
|
|
|
|
hass.bus.async_listen(
|
|
|
|
entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
async def startup_clean(event: Event) -> None:
|
|
|
|
"""Clean up on startup."""
|
|
|
|
hass.bus.async_listen(
|
|
|
|
entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed
|
|
|
|
)
|
|
|
|
await debounced_cleanup.async_call()
|
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean)
|