Home Assistant Unofficial Reference 2024.12.1
entity_registry.py
Go to the documentation of this file.
1 """Provide a registry to track entity IDs.
2 
3 The Entity Registry keeps a registry of entities. Entities are uniquely
4 identified by their domain, platform and a unique id provided by that platform.
5 
6 The Entity Registry will persist itself 10 seconds after a new entity is
7 registered. Registering a new entity while a timer is in progress resets the
8 timer.
9 """
10 
11 from __future__ import annotations
12 
13 from collections import defaultdict
14 from collections.abc import Callable, Container, Hashable, KeysView, Mapping
15 from datetime import datetime, timedelta
16 from enum import StrEnum
17 import logging
18 import time
19 from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict
20 
21 import attr
22 import voluptuous as vol
23 
24 from homeassistant.const import (
25  ATTR_DEVICE_CLASS,
26  ATTR_FRIENDLY_NAME,
27  ATTR_ICON,
28  ATTR_RESTORED,
29  ATTR_SUPPORTED_FEATURES,
30  ATTR_UNIT_OF_MEASUREMENT,
31  EVENT_HOMEASSISTANT_START,
32  EVENT_HOMEASSISTANT_STOP,
33  MAX_LENGTH_STATE_DOMAIN,
34  MAX_LENGTH_STATE_ENTITY_ID,
35  STATE_UNAVAILABLE,
36  STATE_UNKNOWN,
37  EntityCategory,
38  Platform,
39 )
40 from homeassistant.core import (
41  Event,
42  HomeAssistant,
43  callback,
44  split_entity_id,
45  valid_entity_id,
46 )
47 from homeassistant.exceptions import MaxLengthExceeded
48 from homeassistant.loader import async_suggest_report_issue
49 from homeassistant.util import slugify, uuid as uuid_util
50 from homeassistant.util.dt import utc_from_timestamp, utcnow
51 from homeassistant.util.event_type import EventType
52 from homeassistant.util.hass_dict import HassKey
53 from homeassistant.util.json import format_unserializable_data
54 from homeassistant.util.read_only_dict import ReadOnlyDict
55 
56 from . import device_registry as dr, storage
57 from .device_registry import (
58  EVENT_DEVICE_REGISTRY_UPDATED,
59  EventDeviceRegistryUpdatedData,
60 )
61 from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment
62 from .registry import BaseRegistry, BaseRegistryItems, RegistryIndexType
63 from .singleton import singleton
64 from .typing import UNDEFINED, UndefinedType
65 
66 if TYPE_CHECKING:
67  # mypy cannot workout _cache Protocol with attrs
68  from propcache import cached_property as under_cached_property
69 
70  from homeassistant.config_entries import ConfigEntry
71 else:
72  from propcache import under_cached_property
73 
74 DATA_REGISTRY: HassKey[EntityRegistry] = HassKey("entity_registry")
75 EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = EventType(
76  "entity_registry_updated"
77 )
78 
79 _LOGGER = logging.getLogger(__name__)
80 
81 STORAGE_VERSION_MAJOR = 1
82 STORAGE_VERSION_MINOR = 15
83 STORAGE_KEY = "core.entity_registry"
84 
85 CLEANUP_INTERVAL = 3600 * 24
86 ORPHANED_ENTITY_KEEP_SECONDS = 3600 * 24 * 30
87 
88 ENTITY_CATEGORY_VALUE_TO_INDEX: dict[EntityCategory | None, int] = {
89  # mypy does not understand strenum
90  val: idx # type: ignore[misc]
91  for idx, val in enumerate(EntityCategory)
92 }
93 ENTITY_CATEGORY_INDEX_TO_VALUE = dict(enumerate(EntityCategory))
94 
95 # Attributes relevant to describing entity
96 # to external services.
97 ENTITY_DESCRIBING_ATTRIBUTES = {
98  "capabilities",
99  "device_class",
100  "entity_id",
101  "name",
102  "original_name",
103  "supported_features",
104  "unit_of_measurement",
105 }
106 
107 
108 class RegistryEntryDisabler(StrEnum):
109  """What disabled a registry entry."""
110 
111  CONFIG_ENTRY = "config_entry"
112  DEVICE = "device"
113  HASS = "hass"
114  INTEGRATION = "integration"
115  USER = "user"
116 
117 
118 class RegistryEntryHider(StrEnum):
119  """What hid a registry entry."""
120 
121  INTEGRATION = "integration"
122  USER = "user"
123 
124 
126  """EventEntityRegistryUpdated data for action type 'create' and 'remove'."""
127 
128  action: Literal["create", "remove"]
129  entity_id: str
130 
131 
133  """EventEntityRegistryUpdated data for action type 'update'."""
134 
135  action: Literal["update"]
136  entity_id: str
137  changes: dict[str, Any] # Required with action == "update"
138  old_entity_id: NotRequired[str]
139 
140 
141 type EventEntityRegistryUpdatedData = (
142  _EventEntityRegistryUpdatedData_CreateRemove
143  | _EventEntityRegistryUpdatedData_Update
144 )
145 
146 
147 type EntityOptionsType = Mapping[str, Mapping[str, Any]]
148 type ReadOnlyEntityOptionsType = ReadOnlyDict[str, ReadOnlyDict[str, Any]]
149 
150 DISPLAY_DICT_OPTIONAL = (
151  # key, attr_name, convert_to_list
152  ("ai", "area_id", False),
153  ("lb", "labels", True),
154  ("di", "device_id", False),
155  ("ic", "icon", False),
156  ("tk", "translation_key", False),
157 )
158 
159 
161  data: EntityOptionsType | None,
162 ) -> ReadOnlyEntityOptionsType:
163  """Protect entity options from being modified."""
164  if data is None:
165  return ReadOnlyDict({})
166  return ReadOnlyDict({key: ReadOnlyDict(val) for key, val in data.items()})
167 
168 
169 @attr.s(frozen=True, slots=True)
171  """Entity Registry Entry."""
172 
173  entity_id: str = attr.ib()
174  unique_id: str = attr.ib()
175  platform: str = attr.ib()
176  previous_unique_id: str | None = attr.ib(default=None)
177  aliases: set[str] = attr.ib(factory=set)
178  area_id: str | None = attr.ib(default=None)
179  categories: dict[str, str] = attr.ib(factory=dict)
180  capabilities: Mapping[str, Any] | None = attr.ib(default=None)
181  config_entry_id: str | None = attr.ib(default=None)
182  created_at: datetime = attr.ib(factory=utcnow)
183  device_class: str | None = attr.ib(default=None)
184  device_id: str | None = attr.ib(default=None)
185  domain: str = attr.ib(init=False, repr=False)
186  disabled_by: RegistryEntryDisabler | None = attr.ib(default=None)
187  entity_category: EntityCategory | None = attr.ib(default=None)
188  hidden_by: RegistryEntryHider | None = attr.ib(default=None)
189  icon: str | None = attr.ib(default=None)
190  id: str = attr.ib(
191  default=None,
192  converter=attr.converters.default_if_none(factory=uuid_util.random_uuid_hex), # type: ignore[misc]
193  )
194  has_entity_name: bool = attr.ib(default=False)
195  labels: set[str] = attr.ib(factory=set)
196  modified_at: datetime = attr.ib(factory=utcnow)
197  name: str | None = attr.ib(default=None)
198  options: ReadOnlyEntityOptionsType = attr.ib(
199  default=None, converter=_protect_entity_options
200  )
201  # As set by integration
202  original_device_class: str | None = attr.ib(default=None)
203  original_icon: str | None = attr.ib(default=None)
204  original_name: str | None = attr.ib(default=None)
205  supported_features: int = attr.ib(default=0)
206  translation_key: str | None = attr.ib(default=None)
207  unit_of_measurement: str | None = attr.ib(default=None)
208  _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
209 
210  @domain.default
211  def _domain_default(self) -> str:
212  """Compute domain value."""
213  return split_entity_id(self.entity_id)[0]
214 
215  @property
216  def disabled(self) -> bool:
217  """Return if entry is disabled."""
218  return self.disabled_by is not None
219 
220  @property
221  def hidden(self) -> bool:
222  """Return if entry is hidden."""
223  return self.hidden_by is not None
224 
225  @property
226  def _as_display_dict(self) -> dict[str, Any] | None:
227  """Return a partial dict representation of the entry.
228 
229  This version only includes what's needed for display.
230  Returns None if there's no data needed for display.
231  """
232  display_dict: dict[str, Any] = {"ei": self.entity_id, "pl": self.platform}
233  for key, attr_name, convert_list in DISPLAY_DICT_OPTIONAL:
234  if (attr_val := getattr(self, attr_name)) is not None:
235  # Convert sets and tuples to lists
236  # so the JSON serializer does not have to do
237  # it every time
238  display_dict[key] = list(attr_val) if convert_list else attr_val
239  if (category := self.entity_category) is not None:
240  display_dict["ec"] = ENTITY_CATEGORY_VALUE_TO_INDEX[category]
241  if self.hidden_by is not None:
242  display_dict["hb"] = True
243  if self.has_entity_name:
244  display_dict["hn"] = True
245  name = self.name or self.original_name
246  if name is not None:
247  display_dict["en"] = name
248  if self.domaindomain == "sensor" and (sensor_options := self.options.get("sensor")):
249  if (precision := sensor_options.get("display_precision")) is not None or (
250  precision := sensor_options.get("suggested_display_precision")
251  ) is not None:
252  display_dict["dp"] = precision
253  return display_dict
254 
255  @under_cached_property
256  def display_json_repr(self) -> bytes | None:
257  """Return a cached partial JSON representation of the entry.
258 
259  This version only includes what's needed for display.
260  """
261  try:
262  dict_repr = self._as_display_dict_as_display_dict
263  json_repr: bytes | None = json_bytes(dict_repr) if dict_repr else None
264  except (ValueError, TypeError):
265  _LOGGER.error(
266  "Unable to serialize entry %s to JSON. Bad data found at %s",
267  self.entity_id,
269  find_paths_unserializable_data(dict_repr, dump=JSON_DUMP)
270  ),
271  )
272  return None
273  return json_repr
274 
275  @under_cached_property
276  def as_partial_dict(self) -> dict[str, Any]:
277  """Return a partial dict representation of the entry."""
278  # Convert sets and tuples to lists
279  # so the JSON serializer does not have to do
280  # it every time
281  return {
282  "area_id": self.area_id,
283  "categories": self.categories,
284  "config_entry_id": self.config_entry_id,
285  "created_at": self.created_at.timestamp(),
286  "device_id": self.device_id,
287  "disabled_by": self.disabled_by,
288  "entity_category": self.entity_category,
289  "entity_id": self.entity_id,
290  "has_entity_name": self.has_entity_name,
291  "hidden_by": self.hidden_by,
292  "icon": self.icon,
293  "id": self.id,
294  "labels": list(self.labels),
295  "modified_at": self.modified_at.timestamp(),
296  "name": self.name,
297  "options": self.options,
298  "original_name": self.original_name,
299  "platform": self.platform,
300  "translation_key": self.translation_key,
301  "unique_id": self.unique_id,
302  }
303 
304  @under_cached_property
305  def extended_dict(self) -> dict[str, Any]:
306  """Return a extended dict representation of the entry."""
307  # Convert sets and tuples to lists
308  # so the JSON serializer does not have to do
309  # it every time
310  return {
311  **self.as_partial_dictas_partial_dict,
312  "aliases": list(self.aliases),
313  "capabilities": self.capabilities,
314  "device_class": self.device_class,
315  "original_device_class": self.original_device_class,
316  "original_icon": self.original_icon,
317  }
318 
319  @under_cached_property
320  def partial_json_repr(self) -> bytes | None:
321  """Return a cached partial JSON representation of the entry."""
322  try:
323  dict_repr = self.as_partial_dictas_partial_dict
324  return json_bytes(dict_repr)
325  except (ValueError, TypeError):
326  _LOGGER.error(
327  "Unable to serialize entry %s to JSON. Bad data found at %s",
328  self.entity_id,
330  find_paths_unserializable_data(dict_repr, dump=JSON_DUMP)
331  ),
332  )
333  return None
334 
335  @under_cached_property
336  def as_storage_fragment(self) -> json_fragment:
337  """Return a json fragment for storage."""
338  return json_fragment(
339  json_bytes(
340  {
341  "aliases": list(self.aliases),
342  "area_id": self.area_id,
343  "categories": self.categories,
344  "capabilities": self.capabilities,
345  "config_entry_id": self.config_entry_id,
346  "created_at": self.created_at,
347  "device_class": self.device_class,
348  "device_id": self.device_id,
349  "disabled_by": self.disabled_by,
350  "entity_category": self.entity_category,
351  "entity_id": self.entity_id,
352  "hidden_by": self.hidden_by,
353  "icon": self.icon,
354  "id": self.id,
355  "has_entity_name": self.has_entity_name,
356  "labels": list(self.labels),
357  "modified_at": self.modified_at,
358  "name": self.name,
359  "options": self.options,
360  "original_device_class": self.original_device_class,
361  "original_icon": self.original_icon,
362  "original_name": self.original_name,
363  "platform": self.platform,
364  "supported_features": self.supported_features,
365  "translation_key": self.translation_key,
366  "unique_id": self.unique_id,
367  "previous_unique_id": self.previous_unique_id,
368  "unit_of_measurement": self.unit_of_measurement,
369  }
370  )
371  )
372 
373  @callback
374  def write_unavailable_state(self, hass: HomeAssistant) -> None:
375  """Write the unavailable state to the state machine."""
376  attrs: dict[str, Any] = {ATTR_RESTORED: True}
377 
378  if self.capabilities is not None:
379  attrs.update(self.capabilities)
380 
381  device_class = self.device_class or self.original_device_class
382  if device_class is not None:
383  attrs[ATTR_DEVICE_CLASS] = device_class
384 
385  icon = self.icon or self.original_icon
386  if icon is not None:
387  attrs[ATTR_ICON] = icon
388 
389  name = self.name or self.original_name
390  if name is not None:
391  attrs[ATTR_FRIENDLY_NAME] = name
392 
393  if self.supported_features is not None:
394  attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features
395 
396  if self.unit_of_measurement is not None:
397  attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement
398 
399  hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs)
400 
401 
402 @attr.s(frozen=True, slots=True)
404  """Deleted Entity Registry Entry."""
405 
406  entity_id: str = attr.ib()
407  unique_id: str = attr.ib()
408  platform: str = attr.ib()
409  config_entry_id: str | None = attr.ib()
410  domain: str = attr.ib(init=False, repr=False)
411  id: str = attr.ib()
412  orphaned_timestamp: float | None = attr.ib()
413  created_at: datetime = attr.ib(factory=utcnow)
414  modified_at: datetime = attr.ib(factory=utcnow)
415  _cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
416 
417  @domain.default
418  def _domain_default(self) -> str:
419  """Compute domain value."""
420  return split_entity_id(self.entity_id)[0]
421 
422  @under_cached_property
423  def as_storage_fragment(self) -> json_fragment:
424  """Return a json fragment for storage."""
425  return json_fragment(
426  json_bytes(
427  {
428  "config_entry_id": self.config_entry_id,
429  "created_at": self.created_at,
430  "entity_id": self.entity_id,
431  "id": self.id,
432  "modified_at": self.modified_at,
433  "orphaned_timestamp": self.orphaned_timestamp,
434  "platform": self.platform,
435  "unique_id": self.unique_id,
436  }
437  )
438  )
439 
440 
441 class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
442  """Store entity registry data."""
443 
444  async def _async_migrate_func( # noqa: C901
445  self,
446  old_major_version: int,
447  old_minor_version: int,
448  old_data: dict[str, list[dict[str, Any]]],
449  ) -> dict:
450  """Migrate to the new version."""
451  data = old_data
452  if old_major_version == 1:
453  if old_minor_version < 2:
454  # Version 1.2 implements migration and freezes the available keys
455  for entity in data["entities"]:
456  # Populate keys which were introduced before version 1.2
457  entity.setdefault("area_id", None)
458  entity.setdefault("capabilities", {})
459  entity.setdefault("config_entry_id", None)
460  entity.setdefault("device_class", None)
461  entity.setdefault("device_id", None)
462  entity.setdefault("disabled_by", None)
463  entity.setdefault("entity_category", None)
464  entity.setdefault("icon", None)
465  entity.setdefault("name", None)
466  entity.setdefault("original_icon", None)
467  entity.setdefault("original_name", None)
468  entity.setdefault("supported_features", 0)
469  entity.setdefault("unit_of_measurement", None)
470 
471  if old_minor_version < 3:
472  # Version 1.3 adds original_device_class
473  for entity in data["entities"]:
474  # Move device_class to original_device_class
475  entity["original_device_class"] = entity["device_class"]
476  entity["device_class"] = None
477 
478  if old_minor_version < 4:
479  # Version 1.4 adds id
480  for entity in data["entities"]:
481  entity["id"] = uuid_util.random_uuid_hex()
482 
483  if old_minor_version < 5:
484  # Version 1.5 adds entity options
485  for entity in data["entities"]:
486  entity["options"] = {}
487 
488  if old_minor_version < 6:
489  # Version 1.6 adds hidden_by
490  for entity in data["entities"]:
491  entity["hidden_by"] = None
492 
493  if old_minor_version < 7:
494  # Version 1.7 adds has_entity_name
495  for entity in data["entities"]:
496  entity["has_entity_name"] = False
497 
498  if old_minor_version < 8:
499  # Cleanup after frontend bug which incorrectly updated device_class
500  # Fixed by frontend PR #13551
501  for entity in data["entities"]:
502  domain = split_entity_id(entity["entity_id"])[0]
503  if domain in [Platform.BINARY_SENSOR, Platform.COVER]:
504  continue
505  entity["device_class"] = None
506 
507  if old_minor_version < 9:
508  # Version 1.9 adds translation_key
509  for entity in data["entities"]:
510  entity["translation_key"] = None
511 
512  if old_minor_version < 10:
513  # Version 1.10 adds aliases
514  for entity in data["entities"]:
515  entity["aliases"] = []
516 
517  if old_minor_version < 11:
518  # Version 1.11 adds deleted_entities
519  data["deleted_entities"] = data.get("deleted_entities", [])
520 
521  if old_minor_version < 12:
522  # Version 1.12 adds previous_unique_id
523  for entity in data["entities"]:
524  entity["previous_unique_id"] = None
525 
526  if old_minor_version < 13:
527  # Version 1.13 adds labels
528  for entity in data["entities"]:
529  entity["labels"] = []
530 
531  if old_minor_version < 14:
532  # Version 1.14 adds categories
533  for entity in data["entities"]:
534  entity["categories"] = {}
535 
536  if old_minor_version < 15:
537  # Version 1.15 adds created_at and modified_at
538  created_at = utc_from_timestamp(0).isoformat()
539  for entity in data["entities"]:
540  entity["created_at"] = entity["modified_at"] = created_at
541  for entity in data["deleted_entities"]:
542  entity["created_at"] = entity["modified_at"] = created_at
543 
544  if old_major_version > 1:
545  raise NotImplementedError
546  return data
547 
548 
550  """Container for entity registry items, maps entity_id -> entry.
551 
552  Maintains six additional indexes:
553  - id -> entry
554  - (domain, platform, unique_id) -> entity_id
555  - config_entry_id -> dict[key, True]
556  - device_id -> dict[key, True]
557  - area_id -> dict[key, True]
558  - label -> dict[key, True]
559  """
560 
561  def __init__(self) -> None:
562  """Initialize the container."""
563  super().__init__()
564  self._entry_ids: dict[str, RegistryEntry] = {}
565  self._index: dict[tuple[str, str, str], str] = {}
566  self._config_entry_id_index: RegistryIndexType = defaultdict(dict)
567  self._device_id_index: RegistryIndexType = defaultdict(dict)
568  self._area_id_index: RegistryIndexType = defaultdict(dict)
569  self._labels_index: RegistryIndexType = defaultdict(dict)
570 
571  def _index_entry(self, key: str, entry: RegistryEntry) -> None:
572  """Index an entry."""
573  self._entry_ids[entry.id] = entry
574  self._index[(entry.domain, entry.platform, entry.unique_id)] = entry.entity_id
575  # python has no ordered set, so we use a dict with True values
576  # https://discuss.python.org/t/add-orderedset-to-stdlib/12730
577  if (config_entry_id := entry.config_entry_id) is not None:
578  self._config_entry_id_index[config_entry_id][key] = True
579  if (device_id := entry.device_id) is not None:
580  self._device_id_index[device_id][key] = True
581  if (area_id := entry.area_id) is not None:
582  self._area_id_index[area_id][key] = True
583  for label in entry.labels:
584  self._labels_index[label][key] = True
585 
587  self, key: str, replacement_entry: RegistryEntry | None = None
588  ) -> None:
589  """Unindex an entry."""
590  entry = self.data[key]
591  del self._entry_ids[entry.id]
592  del self._index[(entry.domain, entry.platform, entry.unique_id)]
593  if config_entry_id := entry.config_entry_id:
594  self._unindex_entry_value_unindex_entry_value(key, config_entry_id, self._config_entry_id_index)
595  if device_id := entry.device_id:
596  self._unindex_entry_value_unindex_entry_value(key, device_id, self._device_id_index)
597  if area_id := entry.area_id:
598  self._unindex_entry_value_unindex_entry_value(key, area_id, self._area_id_index)
599  if labels := entry.labels:
600  for label in labels:
601  self._unindex_entry_value_unindex_entry_value(key, label, self._labels_index)
602 
603  def get_device_ids(self) -> KeysView[str]:
604  """Return device ids."""
605  return self._device_id_index.keys()
606 
607  def get_entity_id(self, key: tuple[str, str, str]) -> str | None:
608  """Get entity_id from (domain, platform, unique_id)."""
609  return self._index.get(key)
610 
611  def get_entry(self, key: str) -> RegistryEntry | None:
612  """Get entry from id."""
613  return self._entry_ids.get(key)
614 
616  self, device_id: str, include_disabled_entities: bool = False
617  ) -> list[RegistryEntry]:
618  """Get entries for device."""
619  data = self.data
620  return [
621  entry
622  for key in self._device_id_index.get(device_id, ())
623  if not (entry := data[key]).disabled_by or include_disabled_entities
624  ]
625 
627  self, config_entry_id: str
628  ) -> list[RegistryEntry]:
629  """Get entries for config entry."""
630  data = self.data
631  return [
632  data[key] for key in self._config_entry_id_index.get(config_entry_id, ())
633  ]
634 
635  def get_entries_for_area_id(self, area_id: str) -> list[RegistryEntry]:
636  """Get entries for area."""
637  data = self.data
638  return [data[key] for key in self._area_id_index.get(area_id, ())]
639 
640  def get_entries_for_label(self, label: str) -> list[RegistryEntry]:
641  """Get entries for label."""
642  data = self.data
643  return [data[key] for key in self._labels_index.get(label, ())]
644 
645 
647  hass: HomeAssistant,
648  domain: str,
649  platform: str,
650  *,
651  disabled_by: RegistryEntryDisabler | None | UndefinedType = None,
652  entity_category: EntityCategory | None | UndefinedType = None,
653  hidden_by: RegistryEntryHider | None | UndefinedType = None,
654  report_non_string_unique_id: bool = True,
655  unique_id: str | Hashable | UndefinedType | Any,
656 ) -> None:
657  """Validate entity registry item."""
658  if unique_id is not UNDEFINED and not isinstance(unique_id, Hashable):
659  raise TypeError(f"unique_id must be a string, got {unique_id}")
660  if (
661  report_non_string_unique_id
662  and unique_id is not UNDEFINED
663  and not isinstance(unique_id, str)
664  ):
665  # In HA Core 2025.10, we should fail if unique_id is not a string
666  report_issue = async_suggest_report_issue(hass, integration_domain=platform)
667  _LOGGER.error(
668  ("'%s' from integration %s has a non string unique_id" " '%s', please %s"),
669  domain,
670  platform,
671  unique_id,
672  report_issue,
673  )
674  if (
675  disabled_by
676  and disabled_by is not UNDEFINED
677  and not isinstance(disabled_by, RegistryEntryDisabler)
678  ):
679  raise ValueError(
680  f"disabled_by must be a RegistryEntryDisabler value, got {disabled_by}"
681  )
682  if (
683  entity_category
684  and entity_category is not UNDEFINED
685  and not isinstance(entity_category, EntityCategory)
686  ):
687  raise ValueError(
688  f"entity_category must be a valid EntityCategory instance, got {entity_category}"
689  )
690  if (
691  hidden_by
692  and hidden_by is not UNDEFINED
693  and not isinstance(hidden_by, RegistryEntryHider)
694  ):
695  raise ValueError(
696  f"hidden_by must be a RegistryEntryHider value, got {hidden_by}"
697  )
698 
699 
701  """Class to hold a registry of entities."""
702 
703  deleted_entities: dict[tuple[str, str, str], DeletedRegistryEntry]
704  entities: EntityRegistryItems
705  _entities_data: dict[str, RegistryEntry]
706 
707  def __init__(self, hass: HomeAssistant) -> None:
708  """Initialize the registry."""
709  self.hasshass = hass
711  hass,
712  STORAGE_VERSION_MAJOR,
713  STORAGE_KEY,
714  atomic_writes=True,
715  minor_version=STORAGE_VERSION_MINOR,
716  )
717  self.hasshass.bus.async_listen(
718  EVENT_DEVICE_REGISTRY_UPDATED,
719  self.async_device_modifiedasync_device_modified,
720  )
721 
722  @callback
723  def async_is_registered(self, entity_id: str) -> bool:
724  """Check if an entity_id is currently registered."""
725  return entity_id in self.entitiesentities
726 
727  @callback
728  def async_get(self, entity_id_or_uuid: str) -> RegistryEntry | None:
729  """Get EntityEntry for an entity_id or entity entry id.
730 
731  We retrieve the RegistryEntry from the underlying dict to avoid
732  the overhead of the UserDict __getitem__.
733  """
734  return self._entities_data_entities_data.get(entity_id_or_uuid) or self.entitiesentities.get_entry(
735  entity_id_or_uuid
736  )
737 
738  @callback
740  self, domain: str, platform: str, unique_id: str
741  ) -> str | None:
742  """Check if an entity_id is currently registered."""
743  return self.entitiesentities.get_entity_id((domain, platform, unique_id))
744 
745  @callback
746  def async_device_ids(self) -> list[str]:
747  """Return known device ids."""
748  return list(self.entitiesentities.get_device_ids())
749 
751  self, entity_id: str, known_object_ids: Container[str] | None
752  ) -> bool:
753  """Return True if the entity_id is available.
754 
755  An entity_id is available if:
756  - It's not registered
757  - It's not known by the entity component adding the entity
758  - It's not in the state machine
759 
760  Note that an entity_id which belongs to a deleted entity is considered
761  available.
762  """
763  if known_object_ids is None:
764  known_object_ids = {}
765 
766  return (
767  entity_id not in self.entitiesentities
768  and entity_id not in known_object_ids
769  and self.hasshass.states.async_available(entity_id)
770  )
771 
772  @callback
774  self,
775  domain: str,
776  suggested_object_id: str,
777  known_object_ids: Container[str] | None = None,
778  ) -> str:
779  """Generate an entity ID that does not conflict.
780 
781  Conflicts checked against registered and currently existing entities.
782  """
783  preferred_string = f"{domain}.{slugify(suggested_object_id)}"
784 
785  if len(domain) > MAX_LENGTH_STATE_DOMAIN:
786  raise MaxLengthExceeded(domain, "domain", MAX_LENGTH_STATE_DOMAIN)
787 
788  test_string = preferred_string[:MAX_LENGTH_STATE_ENTITY_ID]
789  if known_object_ids is None:
790  known_object_ids = set()
791 
792  tries = 1
793  while not self._entity_id_available_entity_id_available(test_string, known_object_ids):
794  tries += 1
795  len_suffix = len(str(tries)) + 1
796  test_string = (
797  f"{preferred_string[:MAX_LENGTH_STATE_ENTITY_ID-len_suffix]}_{tries}"
798  )
799 
800  return test_string
801 
802  @callback
804  self,
805  domain: str,
806  platform: str,
807  unique_id: str,
808  *,
809  # To influence entity ID generation
810  known_object_ids: Container[str] | None = None,
811  suggested_object_id: str | None = None,
812  # To disable or hide an entity if it gets created
813  disabled_by: RegistryEntryDisabler | None = None,
814  hidden_by: RegistryEntryHider | None = None,
815  # Function to generate initial entity options if it gets created
816  get_initial_options: Callable[[], EntityOptionsType | None] | None = None,
817  # Data that we want entry to have
818  capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED,
819  config_entry: ConfigEntry | None | UndefinedType = UNDEFINED,
820  device_id: str | None | UndefinedType = UNDEFINED,
821  entity_category: EntityCategory | UndefinedType | None = UNDEFINED,
822  has_entity_name: bool | UndefinedType = UNDEFINED,
823  original_device_class: str | None | UndefinedType = UNDEFINED,
824  original_icon: str | None | UndefinedType = UNDEFINED,
825  original_name: str | None | UndefinedType = UNDEFINED,
826  supported_features: int | None | UndefinedType = UNDEFINED,
827  translation_key: str | None | UndefinedType = UNDEFINED,
828  unit_of_measurement: str | None | UndefinedType = UNDEFINED,
829  ) -> RegistryEntry:
830  """Get entity. Create if it doesn't exist."""
831  config_entry_id: str | None | UndefinedType = UNDEFINED
832  if not config_entry:
833  config_entry_id = None
834  elif config_entry is not UNDEFINED:
835  config_entry_id = config_entry.entry_id
836 
837  supported_features = supported_features or 0
838 
839  entity_id = self.async_get_entity_idasync_get_entity_id(domain, platform, unique_id)
840 
841  if entity_id:
842  return self.async_update_entityasync_update_entity(
843  entity_id,
844  capabilities=capabilities,
845  config_entry_id=config_entry_id,
846  device_id=device_id,
847  entity_category=entity_category,
848  has_entity_name=has_entity_name,
849  original_device_class=original_device_class,
850  original_icon=original_icon,
851  original_name=original_name,
852  supported_features=supported_features,
853  translation_key=translation_key,
854  unit_of_measurement=unit_of_measurement,
855  )
856 
857  self.hasshass.verify_event_loop_thread("entity_registry.async_get_or_create")
859  self.hasshass,
860  domain,
861  platform,
862  disabled_by=disabled_by,
863  entity_category=entity_category,
864  hidden_by=hidden_by,
865  unique_id=unique_id,
866  )
867 
868  entity_registry_id: str | None = None
869  created_at = utcnow()
870  deleted_entity = self.deleted_entitiesdeleted_entities.pop((domain, platform, unique_id), None)
871  if deleted_entity is not None:
872  # Restore id
873  entity_registry_id = deleted_entity.id
874  created_at = deleted_entity.created_at
875 
876  entity_id = self.async_generate_entity_idasync_generate_entity_id(
877  domain,
878  suggested_object_id or f"{platform}_{unique_id}",
879  known_object_ids,
880  )
881 
882  if (
883  disabled_by is None
884  and config_entry
885  and config_entry is not UNDEFINED
886  and config_entry.pref_disable_new_entities
887  ):
888  disabled_by = RegistryEntryDisabler.INTEGRATION
889 
890  def none_if_undefined[_T](value: _T | UndefinedType) -> _T | None:
891  """Return None if value is UNDEFINED, otherwise return value."""
892  return None if value is UNDEFINED else value
893 
894  initial_options = get_initial_options() if get_initial_options else None
895 
896  entry = RegistryEntry(
897  capabilities=none_if_undefined(capabilities),
898  config_entry_id=none_if_undefined(config_entry_id),
899  created_at=created_at,
900  device_id=none_if_undefined(device_id),
901  disabled_by=disabled_by,
902  entity_category=none_if_undefined(entity_category),
903  entity_id=entity_id,
904  hidden_by=hidden_by,
905  has_entity_name=none_if_undefined(has_entity_name) or False,
906  id=entity_registry_id,
907  options=initial_options,
908  original_device_class=none_if_undefined(original_device_class),
909  original_icon=none_if_undefined(original_icon),
910  original_name=none_if_undefined(original_name),
911  platform=platform,
912  supported_features=none_if_undefined(supported_features) or 0,
913  translation_key=none_if_undefined(translation_key),
914  unique_id=unique_id,
915  unit_of_measurement=none_if_undefined(unit_of_measurement),
916  )
917  self.entitiesentities[entity_id] = entry
918  _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id)
919  self.async_schedule_save()
920 
921  self.hasshass.bus.async_fire_internal(
922  EVENT_ENTITY_REGISTRY_UPDATED,
924  action="create", entity_id=entity_id
925  ),
926  )
927 
928  return entry
929 
930  @callback
931  def async_remove(self, entity_id: str) -> None:
932  """Remove an entity from registry."""
933  self.hasshass.verify_event_loop_thread("entity_registry.async_remove")
934  entity = self.entitiesentities.pop(entity_id)
935  config_entry_id = entity.config_entry_id
936  key = (entity.domain, entity.platform, entity.unique_id)
937  # If the entity does not belong to a config entry, mark it as orphaned
938  orphaned_timestamp = None if config_entry_id else time.time()
939  self.deleted_entitiesdeleted_entities[key] = DeletedRegistryEntry(
940  config_entry_id=config_entry_id,
941  created_at=entity.created_at,
942  entity_id=entity_id,
943  id=entity.id,
944  orphaned_timestamp=orphaned_timestamp,
945  platform=entity.platform,
946  unique_id=entity.unique_id,
947  )
948  self.hasshass.bus.async_fire_internal(
949  EVENT_ENTITY_REGISTRY_UPDATED,
951  action="remove", entity_id=entity_id
952  ),
953  )
954  self.async_schedule_save()
955 
956  @callback
958  self, event: Event[EventDeviceRegistryUpdatedData]
959  ) -> None:
960  """Handle the removal or update of a device.
961 
962  Remove entities from the registry that are associated to a device when
963  the device is removed.
964 
965  Disable entities in the registry that are associated to a device when
966  the device is disabled.
967  """
968  if event.data["action"] == "remove":
969  entities = async_entries_for_device(
970  self, event.data["device_id"], include_disabled_entities=True
971  )
972  for entity in entities:
973  self.async_removeasync_remove(entity.entity_id)
974  return
975 
976  if event.data["action"] != "update":
977  # Ignore "create" action
978  return
979 
980  device_registry = dr.async_get(self.hasshass)
981  device = device_registry.async_get(event.data["device_id"])
982 
983  # The device may be deleted already if the event handling is late, do nothing
984  # in that case. Entities will be removed when we get the "remove" event.
985  if not device:
986  return
987 
988  # Remove entities which belong to config entries no longer associated with the
989  # device
990  entities = async_entries_for_device(
991  self, event.data["device_id"], include_disabled_entities=True
992  )
993  for entity in entities:
994  if (
995  entity.config_entry_id is not None
996  and entity.config_entry_id not in device.config_entries
997  ):
998  self.async_removeasync_remove(entity.entity_id)
999 
1000  # Re-enable disabled entities if the device is no longer disabled
1001  if not device.disabled:
1002  entities = async_entries_for_device(
1003  self, event.data["device_id"], include_disabled_entities=True
1004  )
1005  for entity in entities:
1006  if entity.disabled_by is not RegistryEntryDisabler.DEVICE:
1007  continue
1008  self.async_update_entityasync_update_entity(entity.entity_id, disabled_by=None)
1009  return
1010 
1011  # Ignore device disabled by config entry, this is handled by
1012  # async_config_entry_disabled
1013  if device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY:
1014  return
1015 
1016  # Fetch entities which are not already disabled and disable them
1017  entities = async_entries_for_device(self, event.data["device_id"])
1018  for entity in entities:
1019  self.async_update_entityasync_update_entity(
1020  entity.entity_id, disabled_by=RegistryEntryDisabler.DEVICE
1021  )
1022 
1023  @callback
1025  self,
1026  entity_id: str,
1027  *,
1028  aliases: set[str] | UndefinedType = UNDEFINED,
1029  area_id: str | None | UndefinedType = UNDEFINED,
1030  categories: dict[str, str] | UndefinedType = UNDEFINED,
1031  capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED,
1032  config_entry_id: str | None | UndefinedType = UNDEFINED,
1033  device_class: str | None | UndefinedType = UNDEFINED,
1034  device_id: str | None | UndefinedType = UNDEFINED,
1035  disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED,
1036  entity_category: EntityCategory | None | UndefinedType = UNDEFINED,
1037  hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED,
1038  icon: str | None | UndefinedType = UNDEFINED,
1039  has_entity_name: bool | UndefinedType = UNDEFINED,
1040  labels: set[str] | UndefinedType = UNDEFINED,
1041  name: str | None | UndefinedType = UNDEFINED,
1042  new_entity_id: str | UndefinedType = UNDEFINED,
1043  new_unique_id: str | UndefinedType = UNDEFINED,
1044  options: EntityOptionsType | UndefinedType = UNDEFINED,
1045  original_device_class: str | None | UndefinedType = UNDEFINED,
1046  original_icon: str | None | UndefinedType = UNDEFINED,
1047  original_name: str | None | UndefinedType = UNDEFINED,
1048  platform: str | None | UndefinedType = UNDEFINED,
1049  supported_features: int | UndefinedType = UNDEFINED,
1050  translation_key: str | None | UndefinedType = UNDEFINED,
1051  unit_of_measurement: str | None | UndefinedType = UNDEFINED,
1052  ) -> RegistryEntry:
1053  """Private facing update properties method."""
1054  old = self.entitiesentities[entity_id]
1055 
1056  new_values: dict[str, Any] = {} # Dict with new key/value pairs
1057  old_values: dict[str, Any] = {} # Dict with old key/value pairs
1058 
1059  for attr_name, value in (
1060  ("aliases", aliases),
1061  ("area_id", area_id),
1062  ("categories", categories),
1063  ("capabilities", capabilities),
1064  ("config_entry_id", config_entry_id),
1065  ("device_class", device_class),
1066  ("device_id", device_id),
1067  ("disabled_by", disabled_by),
1068  ("entity_category", entity_category),
1069  ("hidden_by", hidden_by),
1070  ("icon", icon),
1071  ("has_entity_name", has_entity_name),
1072  ("labels", labels),
1073  ("name", name),
1074  ("options", options),
1075  ("original_device_class", original_device_class),
1076  ("original_icon", original_icon),
1077  ("original_name", original_name),
1078  ("platform", platform),
1079  ("supported_features", supported_features),
1080  ("translation_key", translation_key),
1081  ("unit_of_measurement", unit_of_measurement),
1082  ):
1083  if value is not UNDEFINED and value != getattr(old, attr_name):
1084  new_values[attr_name] = value
1085  old_values[attr_name] = getattr(old, attr_name)
1086 
1087  # Only validate if data has changed
1088  if new_values or new_unique_id is not UNDEFINED:
1090  self.hasshass,
1091  old.domain,
1092  old.platform,
1093  disabled_by=disabled_by,
1094  entity_category=entity_category,
1095  hidden_by=hidden_by,
1096  unique_id=new_unique_id,
1097  )
1098 
1099  if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id:
1100  if not self._entity_id_available_entity_id_available(new_entity_id, None):
1101  raise ValueError("Entity with this ID is already registered")
1102 
1103  if not valid_entity_id(new_entity_id):
1104  raise ValueError("Invalid entity ID")
1105 
1106  if split_entity_id(new_entity_id)[0] != split_entity_id(entity_id)[0]:
1107  raise ValueError("New entity ID should be same domain")
1108 
1109  self.entitiesentities.pop(entity_id)
1110  entity_id = new_values["entity_id"] = new_entity_id
1111  old_values["entity_id"] = old.entity_id
1112 
1113  if new_unique_id is not UNDEFINED:
1114  conflict_entity_id = self.async_get_entity_idasync_get_entity_id(
1115  old.domain, old.platform, new_unique_id
1116  )
1117  if conflict_entity_id:
1118  raise ValueError(
1119  f"Unique id '{new_unique_id}' is already in use by "
1120  f"'{conflict_entity_id}'"
1121  )
1122  new_values["unique_id"] = new_unique_id
1123  old_values["unique_id"] = old.unique_id
1124  new_values["previous_unique_id"] = old.unique_id
1125 
1126  if not new_values:
1127  return old
1128 
1129  new_values["modified_at"] = utcnow()
1130 
1131  self.hasshass.verify_event_loop_thread("entity_registry.async_update_entity")
1132 
1133  new = self.entitiesentities[entity_id] = attr.evolve(old, **new_values)
1134 
1135  self.async_schedule_save()
1136 
1137  data: _EventEntityRegistryUpdatedData_Update = {
1138  "action": "update",
1139  "entity_id": entity_id,
1140  "changes": old_values,
1141  }
1142 
1143  if old.entity_id != entity_id:
1144  data["old_entity_id"] = old.entity_id
1145 
1146  self.hasshass.bus.async_fire_internal(EVENT_ENTITY_REGISTRY_UPDATED, data)
1147 
1148  return new
1149 
1150  @callback
1152  self,
1153  entity_id: str,
1154  *,
1155  aliases: set[str] | UndefinedType = UNDEFINED,
1156  area_id: str | None | UndefinedType = UNDEFINED,
1157  categories: dict[str, str] | UndefinedType = UNDEFINED,
1158  capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED,
1159  config_entry_id: str | None | UndefinedType = UNDEFINED,
1160  device_class: str | None | UndefinedType = UNDEFINED,
1161  device_id: str | None | UndefinedType = UNDEFINED,
1162  disabled_by: RegistryEntryDisabler | None | UndefinedType = UNDEFINED,
1163  entity_category: EntityCategory | None | UndefinedType = UNDEFINED,
1164  hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED,
1165  icon: str | None | UndefinedType = UNDEFINED,
1166  has_entity_name: bool | UndefinedType = UNDEFINED,
1167  labels: set[str] | UndefinedType = UNDEFINED,
1168  name: str | None | UndefinedType = UNDEFINED,
1169  new_entity_id: str | UndefinedType = UNDEFINED,
1170  new_unique_id: str | UndefinedType = UNDEFINED,
1171  original_device_class: str | None | UndefinedType = UNDEFINED,
1172  original_icon: str | None | UndefinedType = UNDEFINED,
1173  original_name: str | None | UndefinedType = UNDEFINED,
1174  supported_features: int | UndefinedType = UNDEFINED,
1175  translation_key: str | None | UndefinedType = UNDEFINED,
1176  unit_of_measurement: str | None | UndefinedType = UNDEFINED,
1177  ) -> RegistryEntry:
1178  """Update properties of an entity."""
1179  return self._async_update_entity_async_update_entity(
1180  entity_id,
1181  aliases=aliases,
1182  area_id=area_id,
1183  categories=categories,
1184  capabilities=capabilities,
1185  config_entry_id=config_entry_id,
1186  device_class=device_class,
1187  device_id=device_id,
1188  disabled_by=disabled_by,
1189  entity_category=entity_category,
1190  hidden_by=hidden_by,
1191  icon=icon,
1192  has_entity_name=has_entity_name,
1193  labels=labels,
1194  name=name,
1195  new_entity_id=new_entity_id,
1196  new_unique_id=new_unique_id,
1197  original_device_class=original_device_class,
1198  original_icon=original_icon,
1199  original_name=original_name,
1200  supported_features=supported_features,
1201  translation_key=translation_key,
1202  unit_of_measurement=unit_of_measurement,
1203  )
1204 
1205  @callback
1207  self,
1208  entity_id: str,
1209  new_platform: str,
1210  *,
1211  new_config_entry_id: str | UndefinedType = UNDEFINED,
1212  new_unique_id: str | UndefinedType = UNDEFINED,
1213  new_device_id: str | None | UndefinedType = UNDEFINED,
1214  ) -> RegistryEntry:
1215  """Update entity platform.
1216 
1217  This should only be used when an entity needs to be migrated between
1218  integrations.
1219  """
1220  if (
1221  state := self.hasshass.states.get(entity_id)
1222  ) is not None and state.state != STATE_UNKNOWN:
1223  raise ValueError("Only entities that haven't been loaded can be migrated")
1224 
1225  old = self.entitiesentities[entity_id]
1226  if new_config_entry_id == UNDEFINED and old.config_entry_id is not None:
1227  raise ValueError(
1228  f"new_config_entry_id required because {entity_id} is already linked "
1229  "to a config entry"
1230  )
1231 
1232  return self._async_update_entity_async_update_entity(
1233  entity_id,
1234  new_unique_id=new_unique_id,
1235  config_entry_id=new_config_entry_id,
1236  device_id=new_device_id,
1237  platform=new_platform,
1238  )
1239 
1240  @callback
1242  self, entity_id: str, domain: str, options: Mapping[str, Any] | None
1243  ) -> RegistryEntry:
1244  """Update entity options for a domain.
1245 
1246  If the domain options are set to None, they will be removed.
1247  """
1248  old = self.entitiesentities[entity_id]
1249  new_options: dict[str, Mapping] = {
1250  key: value for key, value in old.options.items() if key != domain
1251  }
1252  if options is not None:
1253  new_options[domain] = options
1254  return self._async_update_entity_async_update_entity(entity_id, options=new_options)
1255 
1256  async def async_load(self) -> None:
1257  """Load the entity registry."""
1258  _async_setup_cleanup(self.hasshass, self)
1259  _async_setup_entity_restore(self.hasshass, self)
1260 
1261  data = await self._store_store.async_load()
1262  entities = EntityRegistryItems()
1263  deleted_entities: dict[tuple[str, str, str], DeletedRegistryEntry] = {}
1264 
1265  if data is not None:
1266  for entity in data["entities"]:
1267  try:
1268  domain = split_entity_id(entity["entity_id"])[0]
1270  self.hasshass,
1271  domain,
1272  entity["platform"],
1273  report_non_string_unique_id=False,
1274  unique_id=entity["unique_id"],
1275  )
1276  except (TypeError, ValueError) as err:
1277  report_issue = async_suggest_report_issue(
1278  self.hasshass, integration_domain=entity["platform"]
1279  )
1280  _LOGGER.error(
1281  (
1282  "Entity registry entry '%s' from integration %s could not "
1283  "be loaded: '%s', please %s"
1284  ),
1285  entity["entity_id"],
1286  entity["platform"],
1287  str(err),
1288  report_issue,
1289  )
1290  continue
1291 
1292  entities[entity["entity_id"]] = RegistryEntry(
1293  aliases=set(entity["aliases"]),
1294  area_id=entity["area_id"],
1295  categories=entity["categories"],
1296  capabilities=entity["capabilities"],
1297  config_entry_id=entity["config_entry_id"],
1298  created_at=datetime.fromisoformat(entity["created_at"]),
1299  device_class=entity["device_class"],
1300  device_id=entity["device_id"],
1301  disabled_by=RegistryEntryDisabler(entity["disabled_by"])
1302  if entity["disabled_by"]
1303  else None,
1304  entity_category=EntityCategory(entity["entity_category"])
1305  if entity["entity_category"]
1306  else None,
1307  entity_id=entity["entity_id"],
1308  hidden_by=RegistryEntryHider(entity["hidden_by"])
1309  if entity["hidden_by"]
1310  else None,
1311  icon=entity["icon"],
1312  id=entity["id"],
1313  has_entity_name=entity["has_entity_name"],
1314  labels=set(entity["labels"]),
1315  modified_at=datetime.fromisoformat(entity["modified_at"]),
1316  name=entity["name"],
1317  options=entity["options"],
1318  original_device_class=entity["original_device_class"],
1319  original_icon=entity["original_icon"],
1320  original_name=entity["original_name"],
1321  platform=entity["platform"],
1322  supported_features=entity["supported_features"],
1323  translation_key=entity["translation_key"],
1324  unique_id=entity["unique_id"],
1325  previous_unique_id=entity["previous_unique_id"],
1326  unit_of_measurement=entity["unit_of_measurement"],
1327  )
1328  for entity in data["deleted_entities"]:
1329  try:
1330  domain = split_entity_id(entity["entity_id"])[0]
1332  self.hasshass,
1333  domain,
1334  entity["platform"],
1335  report_non_string_unique_id=False,
1336  unique_id=entity["unique_id"],
1337  )
1338  except (TypeError, ValueError):
1339  continue
1340  key = (
1341  split_entity_id(entity["entity_id"])[0],
1342  entity["platform"],
1343  entity["unique_id"],
1344  )
1345  deleted_entities[key] = DeletedRegistryEntry(
1346  config_entry_id=entity["config_entry_id"],
1347  created_at=datetime.fromisoformat(entity["created_at"]),
1348  entity_id=entity["entity_id"],
1349  id=entity["id"],
1350  modified_at=datetime.fromisoformat(entity["modified_at"]),
1351  orphaned_timestamp=entity["orphaned_timestamp"],
1352  platform=entity["platform"],
1353  unique_id=entity["unique_id"],
1354  )
1355 
1356  self.deleted_entitiesdeleted_entities = deleted_entities
1357  self.entitiesentities = entities
1358  self._entities_data_entities_data = entities.data
1359 
1360  @callback
1361  def _data_to_save(self) -> dict[str, Any]:
1362  """Return data of entity registry to store in a file."""
1363  return {
1364  "entities": [entry.as_storage_fragment for entry in self.entitiesentities.values()],
1365  "deleted_entities": [
1366  entry.as_storage_fragment for entry in self.deleted_entitiesdeleted_entities.values()
1367  ],
1368  }
1369 
1370  @callback
1371  def async_clear_category_id(self, scope: str, category_id: str) -> None:
1372  """Clear category id from registry entries."""
1373  for entity_id, entry in self.entitiesentities.items():
1374  if (
1375  existing_category_id := entry.categories.get(scope)
1376  ) and category_id == existing_category_id:
1377  categories = entry.categories.copy()
1378  del categories[scope]
1379  self.async_update_entityasync_update_entity(entity_id, categories=categories)
1380 
1381  @callback
1382  def async_clear_label_id(self, label_id: str) -> None:
1383  """Clear label from registry entries."""
1384  for entry in self.entitiesentities.get_entries_for_label(label_id):
1385  self.async_update_entityasync_update_entity(entry.entity_id, labels=entry.labels - {label_id})
1386 
1387  @callback
1388  def async_clear_config_entry(self, config_entry_id: str) -> None:
1389  """Clear config entry from registry entries."""
1390  now_time = time.time()
1391  for entity_id in [
1392  entry.entity_id
1393  for entry in self.entitiesentities.get_entries_for_config_entry_id(config_entry_id)
1394  ]:
1395  self.async_removeasync_remove(entity_id)
1396  for key, deleted_entity in list(self.deleted_entitiesdeleted_entities.items()):
1397  if config_entry_id != deleted_entity.config_entry_id:
1398  continue
1399  # Add a time stamp when the deleted entity became orphaned
1400  self.deleted_entitiesdeleted_entities[key] = attr.evolve(
1401  deleted_entity, orphaned_timestamp=now_time, config_entry_id=None
1402  )
1403  self.async_schedule_save()
1404 
1405  @callback
1407  """Purge expired orphaned entities from the registry.
1408 
1409  We need to purge these periodically to avoid the database
1410  growing without bound.
1411  """
1412  now_time = time.time()
1413  for key, deleted_entity in list(self.deleted_entitiesdeleted_entities.items()):
1414  if (orphaned_timestamp := deleted_entity.orphaned_timestamp) is None:
1415  continue
1416 
1417  if orphaned_timestamp + ORPHANED_ENTITY_KEEP_SECONDS < now_time:
1418  self.deleted_entitiesdeleted_entities.pop(key)
1419  self.async_schedule_save()
1420 
1421  @callback
1422  def async_clear_area_id(self, area_id: str) -> None:
1423  """Clear area id from registry entries."""
1424  for entry in self.entitiesentities.get_entries_for_area_id(area_id):
1425  self.async_update_entityasync_update_entity(entry.entity_id, area_id=None)
1426 
1427 
1428 @callback
1429 @singleton(DATA_REGISTRY)
1430 def async_get(hass: HomeAssistant) -> EntityRegistry:
1431  """Get entity registry."""
1432  return EntityRegistry(hass)
1433 
1434 
1435 async def async_load(hass: HomeAssistant) -> None:
1436  """Load entity registry."""
1437  assert DATA_REGISTRY not in hass.data
1438  await async_get(hass).async_load()
1439 
1440 
1441 @callback
1443  registry: EntityRegistry, device_id: str, include_disabled_entities: bool = False
1444 ) -> list[RegistryEntry]:
1445  """Return entries that match a device."""
1446  return registry.entities.get_entries_for_device_id(
1447  device_id, include_disabled_entities
1448  )
1449 
1450 
1451 @callback
1453  registry: EntityRegistry, area_id: str
1454 ) -> list[RegistryEntry]:
1455  """Return entries that match an area."""
1456  return registry.entities.get_entries_for_area_id(area_id)
1457 
1458 
1459 @callback
1461  registry: EntityRegistry, label_id: str
1462 ) -> list[RegistryEntry]:
1463  """Return entries that match a label."""
1464  return registry.entities.get_entries_for_label(label_id)
1465 
1466 
1467 @callback
1469  registry: EntityRegistry, scope: str, category_id: str
1470 ) -> list[RegistryEntry]:
1471  """Return entries that match a category in a scope."""
1472  return [
1473  entry
1474  for entry in registry.entities.values()
1475  if (
1476  (existing_category_id := entry.categories.get(scope))
1477  and category_id == existing_category_id
1478  )
1479  ]
1480 
1481 
1482 @callback
1484  registry: EntityRegistry, config_entry_id: str
1485 ) -> list[RegistryEntry]:
1486  """Return entries that match a config entry."""
1487  return registry.entities.get_entries_for_config_entry_id(config_entry_id)
1488 
1489 
1490 @callback
1492  registry: EntityRegistry, config_entry: ConfigEntry
1493 ) -> None:
1494  """Handle a config entry being disabled or enabled.
1495 
1496  Disable entities in the registry that are associated with a config entry when
1497  the config entry is disabled, enable entities in the registry that are associated
1498  with a config entry when the config entry is enabled and the entities are marked
1499  DISABLED_CONFIG_ENTRY.
1500  """
1501 
1502  entities = async_entries_for_config_entry(registry, config_entry.entry_id)
1503 
1504  if not config_entry.disabled_by:
1505  for entity in entities:
1506  if entity.disabled_by is not RegistryEntryDisabler.CONFIG_ENTRY:
1507  continue
1508  registry.async_update_entity(entity.entity_id, disabled_by=None)
1509  return
1510 
1511  for entity in entities:
1512  if entity.disabled:
1513  # Entity already disabled, do not overwrite
1514  continue
1515  registry.async_update_entity(
1516  entity.entity_id, disabled_by=RegistryEntryDisabler.CONFIG_ENTRY
1517  )
1518 
1519 
1520 @callback
1521 def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None:
1522  """Clean up device registry when entities removed."""
1523  # pylint: disable-next=import-outside-toplevel
1524  from . import category_registry as cr, event, label_registry as lr
1525 
1526  @callback
1527  def _removed_from_registry_filter(
1528  event_data: lr.EventLabelRegistryUpdatedData
1529  | cr.EventCategoryRegistryUpdatedData,
1530  ) -> bool:
1531  """Filter all except for the remove action from registry events."""
1532  return event_data["action"] == "remove"
1533 
1534  @callback
1535  def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None:
1536  """Update entity that have a label that has been removed."""
1537  registry.async_clear_label_id(event.data["label_id"])
1538 
1539  hass.bus.async_listen(
1540  event_type=lr.EVENT_LABEL_REGISTRY_UPDATED,
1541  event_filter=_removed_from_registry_filter,
1542  listener=_handle_label_registry_update,
1543  )
1544 
1545  @callback
1546  def _handle_category_registry_update(
1547  event: cr.EventCategoryRegistryUpdated,
1548  ) -> None:
1549  """Update entity that have a category that has been removed."""
1550  registry.async_clear_category_id(event.data["scope"], event.data["category_id"])
1551 
1552  hass.bus.async_listen(
1553  event_type=cr.EVENT_CATEGORY_REGISTRY_UPDATED,
1554  event_filter=_removed_from_registry_filter,
1555  listener=_handle_category_registry_update,
1556  )
1557 
1558  @callback
1559  def cleanup(_: datetime) -> None:
1560  """Clean up entity registry."""
1561  # Periodic purge of orphaned entities to avoid the registry
1562  # growing without bounds when there are lots of deleted entities
1563  registry.async_purge_expired_orphaned_entities()
1564 
1565  cancel = event.async_track_time_interval(
1566  hass, cleanup, timedelta(seconds=CLEANUP_INTERVAL)
1567  )
1568 
1569  @callback
1570  def _on_homeassistant_stop(event: Event) -> None:
1571  """Cancel cleanup."""
1572  cancel()
1573 
1574  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _on_homeassistant_stop)
1575 
1576 
1577 @callback
1578 def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -> None:
1579  """Set up the entity restore mechanism."""
1580 
1581  @callback
1582  def cleanup_restored_states_filter(event_data: Mapping[str, Any]) -> bool:
1583  """Clean up restored states filter."""
1584  return bool(event_data["action"] == "remove")
1585 
1586  @callback
1587  def cleanup_restored_states(event: Event[EventEntityRegistryUpdatedData]) -> None:
1588  """Clean up restored states."""
1589  state = hass.states.get(event.data["entity_id"])
1590 
1591  if state is None or not state.attributes.get(ATTR_RESTORED):
1592  return
1593 
1594  hass.states.async_remove(event.data["entity_id"], context=event.context)
1595 
1596  hass.bus.async_listen(
1597  EVENT_ENTITY_REGISTRY_UPDATED,
1598  cleanup_restored_states,
1599  event_filter=cleanup_restored_states_filter,
1600  )
1601 
1602  if hass.is_running:
1603  return
1604 
1605  @callback
1606  def _write_unavailable_states(_: Event) -> None:
1607  """Make sure state machine contains entry for each registered entity."""
1608  existing = set(hass.states.async_entity_ids())
1609 
1610  for entry in registry.entities.values():
1611  if entry.entity_id in existing or entry.disabled:
1612  continue
1613 
1614  entry.write_unavailable_state(hass)
1615 
1616  hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states)
1617 
1618 
1620  hass: HomeAssistant,
1621  config_entry_id: str,
1622  entry_callback: Callable[[RegistryEntry], dict[str, Any] | None],
1623 ) -> None:
1624  """Migrate entity registry entries which belong to a config entry.
1625 
1626  Can be used as a migrator of unique_ids or to update other entity registry data.
1627  Can also be used to remove duplicated entity registry entries.
1628  """
1629  ent_reg = async_get(hass)
1630  entities = ent_reg.entities
1631  for entry in entities.get_entries_for_config_entry_id(config_entry_id):
1632  if (
1633  entities.get_entry(entry.id)
1634  and (updates := entry_callback(entry)) is not None
1635  ):
1636  ent_reg.async_update_entity(entry.entity_id, **updates)
1637 
1638 
1639 @callback
1640 def async_validate_entity_id(registry: EntityRegistry, entity_id_or_uuid: str) -> str:
1641  """Validate and resolve an entity id or UUID to an entity id.
1642 
1643  Raises vol.Invalid if the entity or UUID is invalid, or if the UUID is not
1644  associated with an entity registry item.
1645  """
1646  if valid_entity_id(entity_id_or_uuid):
1647  return entity_id_or_uuid
1648  if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None:
1649  raise vol.Invalid(f"Unknown entity registry entry {entity_id_or_uuid}")
1650  return entry.entity_id
1651 
1652 
1653 @callback
1655  registry: EntityRegistry, entity_id_or_uuid: str
1656 ) -> str | None:
1657  """Validate and resolve an entity id or UUID to an entity id.
1658 
1659  Returns None if the entity or UUID is invalid, or if the UUID is not
1660  associated with an entity registry item.
1661  """
1662  if valid_entity_id(entity_id_or_uuid):
1663  return entity_id_or_uuid
1664  if (entry := registry.entities.get_entry(entity_id_or_uuid)) is None:
1665  return None
1666  return entry.entity_id
1667 
1668 
1669 @callback
1671  registry: EntityRegistry, entity_ids_or_uuids: list[str]
1672 ) -> list[str]:
1673  """Validate and resolve a list of entity ids or UUIDs to a list of entity ids.
1674 
1675  Returns a list with UUID resolved to entity_ids.
1676  Raises vol.Invalid if any item is invalid, or if any a UUID is not associated with
1677  an entity registry item.
1678  """
1679 
1680  return [async_validate_entity_id(registry, item) for item in entity_ids_or_uuids]
list[RegistryEntry] get_entries_for_device_id(self, str device_id, bool include_disabled_entities=False)
list[RegistryEntry] get_entries_for_label(self, str label)
None _index_entry(self, str key, RegistryEntry entry)
None _unindex_entry(self, str key, RegistryEntry|None replacement_entry=None)
list[RegistryEntry] get_entries_for_config_entry_id(self, str config_entry_id)
str|None get_entity_id(self, tuple[str, str, str] key)
list[RegistryEntry] get_entries_for_area_id(self, str area_id)
dict _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, list[dict[str, Any]]] old_data)
RegistryEntry async_update_entity_options(self, str entity_id, str domain, Mapping[str, Any]|None options)
RegistryEntry|None async_get(self, str entity_id_or_uuid)
None async_clear_config_entry(self, str config_entry_id)
str async_generate_entity_id(self, str domain, str suggested_object_id, Container[str]|None known_object_ids=None)
RegistryEntry async_get_or_create(self, str domain, str platform, str unique_id, *Container[str]|None known_object_ids=None, str|None suggested_object_id=None, RegistryEntryDisabler|None disabled_by=None, RegistryEntryHider|None hidden_by=None, Callable[[], EntityOptionsType|None]|None get_initial_options=None, Mapping[str, Any]|None|UndefinedType capabilities=UNDEFINED, ConfigEntry|None|UndefinedType config_entry=UNDEFINED, str|None|UndefinedType device_id=UNDEFINED, EntityCategory|UndefinedType|None entity_category=UNDEFINED, bool|UndefinedType has_entity_name=UNDEFINED, str|None|UndefinedType original_device_class=UNDEFINED, str|None|UndefinedType original_icon=UNDEFINED, str|None|UndefinedType original_name=UNDEFINED, int|None|UndefinedType supported_features=UNDEFINED, str|None|UndefinedType translation_key=UNDEFINED, str|None|UndefinedType unit_of_measurement=UNDEFINED)
None async_device_modified(self, Event[EventDeviceRegistryUpdatedData] event)
RegistryEntry async_update_entity_platform(self, str entity_id, str new_platform, *str|UndefinedType new_config_entry_id=UNDEFINED, str|UndefinedType new_unique_id=UNDEFINED, str|None|UndefinedType new_device_id=UNDEFINED)
RegistryEntry _async_update_entity(self, str entity_id, *set[str]|UndefinedType aliases=UNDEFINED, str|None|UndefinedType area_id=UNDEFINED, dict[str, str]|UndefinedType categories=UNDEFINED, Mapping[str, Any]|None|UndefinedType capabilities=UNDEFINED, str|None|UndefinedType config_entry_id=UNDEFINED, str|None|UndefinedType device_class=UNDEFINED, str|None|UndefinedType device_id=UNDEFINED, RegistryEntryDisabler|None|UndefinedType disabled_by=UNDEFINED, EntityCategory|None|UndefinedType entity_category=UNDEFINED, RegistryEntryHider|None|UndefinedType hidden_by=UNDEFINED, str|None|UndefinedType icon=UNDEFINED, bool|UndefinedType has_entity_name=UNDEFINED, set[str]|UndefinedType labels=UNDEFINED, str|None|UndefinedType name=UNDEFINED, str|UndefinedType new_entity_id=UNDEFINED, str|UndefinedType new_unique_id=UNDEFINED, EntityOptionsType|UndefinedType options=UNDEFINED, str|None|UndefinedType original_device_class=UNDEFINED, str|None|UndefinedType original_icon=UNDEFINED, str|None|UndefinedType original_name=UNDEFINED, str|None|UndefinedType platform=UNDEFINED, int|UndefinedType supported_features=UNDEFINED, str|None|UndefinedType translation_key=UNDEFINED, str|None|UndefinedType unit_of_measurement=UNDEFINED)
bool _entity_id_available(self, str entity_id, Container[str]|None known_object_ids)
None async_clear_category_id(self, str scope, str category_id)
str|None async_get_entity_id(self, str domain, str platform, str unique_id)
RegistryEntry async_update_entity(self, str entity_id, *set[str]|UndefinedType aliases=UNDEFINED, str|None|UndefinedType area_id=UNDEFINED, dict[str, str]|UndefinedType categories=UNDEFINED, Mapping[str, Any]|None|UndefinedType capabilities=UNDEFINED, str|None|UndefinedType config_entry_id=UNDEFINED, str|None|UndefinedType device_class=UNDEFINED, str|None|UndefinedType device_id=UNDEFINED, RegistryEntryDisabler|None|UndefinedType disabled_by=UNDEFINED, EntityCategory|None|UndefinedType entity_category=UNDEFINED, RegistryEntryHider|None|UndefinedType hidden_by=UNDEFINED, str|None|UndefinedType icon=UNDEFINED, bool|UndefinedType has_entity_name=UNDEFINED, set[str]|UndefinedType labels=UNDEFINED, str|None|UndefinedType name=UNDEFINED, str|UndefinedType new_entity_id=UNDEFINED, str|UndefinedType new_unique_id=UNDEFINED, str|None|UndefinedType original_device_class=UNDEFINED, str|None|UndefinedType original_icon=UNDEFINED, str|None|UndefinedType original_name=UNDEFINED, int|UndefinedType supported_features=UNDEFINED, str|None|UndefinedType translation_key=UNDEFINED, str|None|UndefinedType unit_of_measurement=UNDEFINED)
None write_unavailable_state(self, HomeAssistant hass)
None _unindex_entry_value(self, str key, str value, RegistryIndexType index)
Definition: registry.py:48
config_entries.ConfigEntry|None get_entry(HomeAssistant hass, websocket_api.ActiveConnection connection, str entry_id, int msg_id)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool valid_entity_id(str entity_id)
Definition: core.py:235
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
None async_config_entry_disabled_by_changed(EntityRegistry registry, ConfigEntry config_entry)
list[RegistryEntry] async_entries_for_device(EntityRegistry registry, str device_id, bool include_disabled_entities=False)
str async_validate_entity_id(EntityRegistry registry, str entity_id_or_uuid)
str|None async_resolve_entity_id(EntityRegistry registry, str entity_id_or_uuid)
None _validate_item(HomeAssistant hass, str domain, str platform, *RegistryEntryDisabler|None|UndefinedType disabled_by=None, EntityCategory|None|UndefinedType entity_category=None, RegistryEntryHider|None|UndefinedType hidden_by=None, bool report_non_string_unique_id=True, str|Hashable|UndefinedType|Any unique_id)
None async_migrate_entries(HomeAssistant hass, str config_entry_id, Callable[[RegistryEntry], dict[str, Any]|None] entry_callback)
EntityRegistry async_get(HomeAssistant hass)
list[RegistryEntry] async_entries_for_category(EntityRegistry registry, str scope, str category_id)
list[RegistryEntry] async_entries_for_config_entry(EntityRegistry registry, str config_entry_id)
None _async_setup_cleanup(HomeAssistant hass, EntityRegistry registry)
None _async_setup_entity_restore(HomeAssistant hass, EntityRegistry registry)
list[str] async_validate_entity_ids(EntityRegistry registry, list[str] entity_ids_or_uuids)
list[RegistryEntry] async_entries_for_area(EntityRegistry registry, str area_id)
list[RegistryEntry] async_entries_for_label(EntityRegistry registry, str label_id)
ReadOnlyEntityOptionsType _protect_entity_options(EntityOptionsType|None data)
dict[str, Any] find_paths_unserializable_data(Any bad_data, *Callable[[Any], str] dump=json.dumps)
Definition: json.py:233
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1752
str format_unserializable_data(dict[str, Any] data)
Definition: json.py:126