1 """Provide a way to connect devices to one physical location."""
3 from __future__
import annotations
5 from collections
import defaultdict
6 from collections.abc
import Iterable
8 from dataclasses
import dataclass, field
9 from datetime
import datetime
10 from typing
import TYPE_CHECKING, Any, Literal, TypedDict
17 from .
import device_registry
as dr, entity_registry
as er
18 from .json
import json_bytes, json_fragment
19 from .normalized_name_base_registry
import (
20 NormalizedNameBaseRegistryEntry,
21 NormalizedNameBaseRegistryItems,
23 from .registry
import BaseRegistry, RegistryIndexType
24 from .singleton
import singleton
25 from .storage
import Store
26 from .typing
import UNDEFINED, UndefinedType
30 from propcache
import cached_property
as under_cached_property
32 from propcache
import under_cached_property
35 DATA_REGISTRY: HassKey[AreaRegistry] =
HassKey(
"area_registry")
36 EVENT_AREA_REGISTRY_UPDATED: EventType[EventAreaRegistryUpdatedData] =
EventType(
37 "area_registry_updated"
39 STORAGE_KEY =
"core.area_registry"
40 STORAGE_VERSION_MAJOR = 1
41 STORAGE_VERSION_MINOR = 7
45 """Data type for individual area. Used in AreasRegistryStoreData."""
59 """Store data type for AreaRegistry."""
61 areas: list[_AreaStoreData]
65 """EventAreaRegistryUpdated data."""
67 action: Literal[
"create",
"remove",
"update"]
71 @dataclass(frozen=True, kw_only=True, slots=True)
73 """Area Registry Entry."""
79 labels: set[str] = field(default_factory=set)
81 _cache: dict[str, Any] = field(default_factory=dict, compare=
False, init=
False)
83 @under_cached_property
85 """Return a JSON representation of this AreaEntry."""
89 "aliases":
list(self.aliases),
91 "floor_id": self.floor_id,
93 "labels":
list(self.labels),
95 "picture": self.picture,
96 "created_at": self.created_at.timestamp(),
97 "modified_at": self.modified_at.timestamp(),
104 """Store area registry data."""
108 old_major_version: int,
109 old_minor_version: int,
110 old_data: dict[str, list[dict[str, Any]]],
111 ) -> AreasRegistryStoreData:
112 """Migrate to the new version."""
113 if old_major_version < 2:
114 if old_minor_version < 2:
116 for area
in old_data[
"areas"]:
118 area.setdefault(
"picture",
None)
120 if old_minor_version < 3:
122 for area
in old_data[
"areas"]:
125 if old_minor_version < 4:
127 for area
in old_data[
"areas"]:
130 if old_minor_version < 5:
132 for area
in old_data[
"areas"]:
133 area[
"floor_id"] =
None
135 if old_minor_version < 6:
137 for area
in old_data[
"areas"]:
140 if old_minor_version < 7:
143 for area
in old_data[
"areas"]:
144 area[
"created_at"] = area[
"modified_at"] = created_at
146 if old_major_version > 1:
147 raise NotImplementedError
152 """Class to hold area registry items."""
155 """Initialize the area registry items."""
157 self._labels_index: RegistryIndexType = defaultdict(dict)
158 self._floors_index: RegistryIndexType = defaultdict(dict)
161 """Index an entry."""
163 if entry.floor_id
is not None:
164 self._floors_index[entry.floor_id][key] =
True
165 for label
in entry.labels:
166 self._labels_index[label][key] =
True
169 self, key: str, replacement_entry: AreaEntry |
None =
None
173 entry = self.data[key]
174 if labels := entry.labels:
176 self._unindex_entry_value(key, label, self._labels_index)
177 if floor_id := entry.floor_id:
178 self._unindex_entry_value(key, floor_id, self._floors_index)
181 """Get areas for label."""
183 return [data[key]
for key
in self._labels_index.
get(label, ())]
186 """Get areas for floor."""
188 return [data[key]
for key
in self._floors_index.
get(floor, ())]
192 """Class to hold a registry of areas."""
194 areas: AreaRegistryItems
195 _area_data: dict[str, AreaEntry]
198 """Initialize the area registry."""
202 STORAGE_VERSION_MAJOR,
205 minor_version=STORAGE_VERSION_MINOR,
212 We retrieve the DeviceEntry from the underlying dict to avoid
213 the overhead of the UserDict __getitem__.
219 """Get area by name."""
225 return self.
areasareas.values()
229 """Get or create an area."""
235 """Generate area ID."""
243 aliases: set[str] |
None =
None,
244 floor_id: str |
None =
None,
245 icon: str |
None =
None,
246 labels: set[str] |
None =
None,
247 picture: str |
None =
None,
249 """Create a new area."""
250 self.
hasshass.verify_event_loop_thread(
"area_registry.async_create")
254 f
"The name {name} ({area.normalized_name}) is already in use"
258 aliases=aliases
or set(),
262 labels=labels
or set(),
267 self.
areasareas[area_id] = area
268 self.async_schedule_save()
270 self.
hasshass.bus.async_fire_internal(
271 EVENT_AREA_REGISTRY_UPDATED,
279 self.
hasshass.verify_event_loop_thread(
"area_registry.async_delete")
280 device_registry = dr.async_get(self.
hasshass)
281 entity_registry = er.async_get(self.
hasshass)
282 device_registry.async_clear_area_id(area_id)
283 entity_registry.async_clear_area_id(area_id)
285 del self.
areasareas[area_id]
287 self.
hasshass.bus.async_fire_internal(
288 EVENT_AREA_REGISTRY_UPDATED,
292 self.async_schedule_save()
299 aliases: set[str] | UndefinedType = UNDEFINED,
300 floor_id: str |
None | UndefinedType = UNDEFINED,
301 icon: str |
None | UndefinedType = UNDEFINED,
302 labels: set[str] | UndefinedType = UNDEFINED,
303 name: str | UndefinedType = UNDEFINED,
304 picture: str |
None | UndefinedType = UNDEFINED,
306 """Update name of area."""
320 self.
hasshass.bus.async_fire(
321 EVENT_AREA_REGISTRY_UPDATED,
331 aliases: set[str] | UndefinedType = UNDEFINED,
332 floor_id: str |
None | UndefinedType = UNDEFINED,
333 icon: str |
None | UndefinedType = UNDEFINED,
334 labels: set[str] | UndefinedType = UNDEFINED,
335 name: str | UndefinedType = UNDEFINED,
336 picture: str |
None | UndefinedType = UNDEFINED,
338 """Update name of area."""
339 old = self.
areasareas[area_id]
341 new_values: dict[str, Any] = {
343 for attr_name, value
in (
344 (
"aliases", aliases),
347 (
"picture", picture),
348 (
"floor_id", floor_id),
350 if value
is not UNDEFINED
and value != getattr(old, attr_name)
353 if name
is not UNDEFINED
and name != old.name:
354 new_values[
"name"] = name
359 new_values[
"modified_at"] =
utcnow()
361 self.
hasshass.verify_event_loop_thread(
"area_registry.async_update")
362 new = self.
areasareas[area_id] = dataclasses.replace(old, **new_values)
364 self.async_schedule_save()
368 """Load the area registry."""
376 for area
in data[
"areas"]:
377 assert area[
"name"]
is not None and area[
"id"]
is not None
379 aliases=set(area[
"aliases"]),
380 floor_id=area[
"floor_id"],
383 labels=set(area[
"labels"]),
385 picture=area[
"picture"],
386 created_at=datetime.fromisoformat(area[
"created_at"]),
387 modified_at=datetime.fromisoformat(area[
"modified_at"]),
395 """Return data of area registry to store in a file."""
399 "aliases":
list(entry.aliases),
400 "floor_id": entry.floor_id,
403 "labels":
list(entry.labels),
405 "picture": entry.picture,
406 "created_at": entry.created_at.isoformat(),
407 "modified_at": entry.modified_at.isoformat(),
409 for entry
in self.
areasareas.values()
415 """Set up the area registry cleanup."""
418 floor_registry
as fr,
419 label_registry
as lr,
423 def _removed_from_registry_filter(
424 event_data: fr.EventFloorRegistryUpdatedData
425 | lr.EventLabelRegistryUpdatedData,
427 """Filter all except for the item removed from registry events."""
428 return event_data[
"action"] ==
"remove"
431 def _handle_floor_registry_update(event: fr.EventFloorRegistryUpdated) ->
None:
432 """Update areas that are associated with a floor that has been removed."""
433 floor_id = event.data[
"floor_id"]
434 for area
in self.
areasareas.get_areas_for_floor(floor_id):
437 self.
hasshass.bus.async_listen(
438 event_type=fr.EVENT_FLOOR_REGISTRY_UPDATED,
439 event_filter=_removed_from_registry_filter,
440 listener=_handle_floor_registry_update,
444 def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) ->
None:
445 """Update areas that have a label that has been removed."""
446 label_id = event.data[
"label_id"]
447 for area
in self.
areasareas.get_areas_for_label(label_id):
448 self.
async_updateasync_update(area.id, labels=area.labels - {label_id})
450 self.
hasshass.bus.async_listen(
451 event_type=lr.EVENT_LABEL_REGISTRY_UPDATED,
452 event_filter=_removed_from_registry_filter,
453 listener=_handle_label_registry_update,
458 @singleton(DATA_REGISTRY)
460 """Get area registry."""
465 """Load area registry."""
466 assert DATA_REGISTRY
not in hass.data
472 """Return entries that match a floor."""
473 return registry.areas.get_areas_for_floor(floor_id)
478 """Return entries that match a label."""
479 return registry.areas.get_areas_for_label(label_id)
json_fragment json_fragment(self)
list[AreaEntry] get_areas_for_floor(self, str floor)
None _unindex_entry(self, str key, AreaEntry|None replacement_entry=None)
list[AreaEntry] get_areas_for_label(self, str label)
None _index_entry(self, str key, AreaEntry entry)
AreasRegistryStoreData _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, list[dict[str, Any]]] old_data)
AreaEntry _async_update(self, str area_id, *set[str]|UndefinedType aliases=UNDEFINED, str|None|UndefinedType floor_id=UNDEFINED, str|None|UndefinedType icon=UNDEFINED, set[str]|UndefinedType labels=UNDEFINED, str|UndefinedType name=UNDEFINED, str|None|UndefinedType picture=UNDEFINED)
None async_delete(self, str area_id)
AreaEntry async_update(self, str area_id, *set[str]|UndefinedType aliases=UNDEFINED, str|None|UndefinedType floor_id=UNDEFINED, str|None|UndefinedType icon=UNDEFINED, set[str]|UndefinedType labels=UNDEFINED, str|UndefinedType name=UNDEFINED, str|None|UndefinedType picture=UNDEFINED)
AreaEntry|None async_get_area(self, str area_id)
str _generate_id(self, str name)
AreaEntry async_get_or_create(self, str name)
Iterable[AreaEntry] async_list_areas(self)
None __init__(self, HomeAssistant hass)
AreaEntry|None async_get_area_by_name(self, str name)
None _async_setup_cleanup(self)
AreasRegistryStoreData _data_to_save(self)
AreaEntry async_create(self, str name, *set[str]|None aliases=None, str|None floor_id=None, str|None icon=None, set[str]|None labels=None, str|None picture=None)
web.Response get(self, web.Request request, str config_key)
None async_load(HomeAssistant hass)
list[AreaEntry] async_entries_for_floor(AreaRegistry registry, str floor_id)
list[AreaEntry] async_entries_for_label(AreaRegistry registry, str label_id)
AreaRegistry async_get(HomeAssistant hass)
_VT|None get_by_name(self, str name)
str generate_id_from_name(self, str name)