Home Assistant Unofficial Reference 2024.12.1
floor_registry.py
Go to the documentation of this file.
1 """Provide a way to assign areas to floors in one's home."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Iterable
6 import dataclasses
7 from dataclasses import dataclass
8 from datetime import datetime
9 from typing import Any, Literal, TypedDict
10 
11 from homeassistant.core import Event, HomeAssistant, callback
12 from homeassistant.util.dt import utc_from_timestamp, utcnow
13 from homeassistant.util.event_type import EventType
14 from homeassistant.util.hass_dict import HassKey
15 
16 from .normalized_name_base_registry import (
17  NormalizedNameBaseRegistryEntry,
18  NormalizedNameBaseRegistryItems,
19 )
20 from .registry import BaseRegistry
21 from .singleton import singleton
22 from .storage import Store
23 from .typing import UNDEFINED, UndefinedType
24 
25 DATA_REGISTRY: HassKey[FloorRegistry] = HassKey("floor_registry")
26 EVENT_FLOOR_REGISTRY_UPDATED: EventType[EventFloorRegistryUpdatedData] = EventType(
27  "floor_registry_updated"
28 )
29 STORAGE_KEY = "core.floor_registry"
30 STORAGE_VERSION_MAJOR = 1
31 STORAGE_VERSION_MINOR = 2
32 
33 
34 class _FloorStoreData(TypedDict):
35  """Data type for individual floor. Used in FloorRegistryStoreData."""
36 
37  aliases: list[str]
38  floor_id: str
39  icon: str | None
40  level: int | None
41  name: str
42  created_at: str
43  modified_at: str
44 
45 
46 class FloorRegistryStoreData(TypedDict):
47  """Store data type for FloorRegistry."""
48 
49  floors: list[_FloorStoreData]
50 
51 
53  """Event data for when the floor registry is updated."""
54 
55  action: Literal["create", "remove", "update"]
56  floor_id: str
57 
58 
59 type EventFloorRegistryUpdated = Event[EventFloorRegistryUpdatedData]
60 
61 
62 @dataclass(slots=True, kw_only=True, frozen=True)
64  """Floor registry entry."""
65 
66  aliases: set[str]
67  floor_id: str
68  icon: str | None = None
69  level: int | None = None
70 
71 
72 class FloorRegistryStore(Store[FloorRegistryStoreData]):
73  """Store floor registry data."""
74 
76  self,
77  old_major_version: int,
78  old_minor_version: int,
79  old_data: dict[str, list[dict[str, Any]]],
80  ) -> FloorRegistryStoreData:
81  """Migrate to the new version."""
82  if old_major_version > STORAGE_VERSION_MAJOR:
83  raise ValueError("Can't migrate to future version")
84 
85  if old_major_version == 1:
86  if old_minor_version < 2:
87  # Version 1.2 implements migration and adds created_at and modified_at
88  created_at = utc_from_timestamp(0).isoformat()
89  for floor in old_data["floors"]:
90  floor["created_at"] = floor["modified_at"] = created_at
91 
92  return old_data # type: ignore[return-value]
93 
94 
95 class FloorRegistry(BaseRegistry[FloorRegistryStoreData]):
96  """Class to hold a registry of floors."""
97 
98  floors: NormalizedNameBaseRegistryItems[FloorEntry]
99  _floor_data: dict[str, FloorEntry]
100 
101  def __init__(self, hass: HomeAssistant) -> None:
102  """Initialize the floor registry."""
103  self.hasshass = hass
105  hass,
106  STORAGE_VERSION_MAJOR,
107  STORAGE_KEY,
108  atomic_writes=True,
109  minor_version=STORAGE_VERSION_MINOR,
110  )
111 
112  @callback
113  def async_get_floor(self, floor_id: str) -> FloorEntry | None:
114  """Get floor by id.
115 
116  We retrieve the FloorEntry from the underlying dict to avoid
117  the overhead of the UserDict __getitem__.
118  """
119  return self._floor_data_floor_data.get(floor_id)
120 
121  @callback
122  def async_get_floor_by_name(self, name: str) -> FloorEntry | None:
123  """Get floor by name."""
124  return self.floorsfloors.get_by_name(name)
125 
126  @callback
127  def async_list_floors(self) -> Iterable[FloorEntry]:
128  """Get all floors."""
129  return self.floorsfloors.values()
130 
131  def _generate_id(self, name: str) -> str:
132  """Generate floor ID."""
133  return self.floorsfloors.generate_id_from_name(name)
134 
135  @callback
137  self,
138  name: str,
139  *,
140  aliases: set[str] | None = None,
141  icon: str | None = None,
142  level: int | None = None,
143  ) -> FloorEntry:
144  """Create a new floor."""
145  self.hasshass.verify_event_loop_thread("floor_registry.async_create")
146 
147  if floor := self.async_get_floor_by_nameasync_get_floor_by_name(name):
148  raise ValueError(
149  f"The name {name} ({floor.normalized_name}) is already in use"
150  )
151 
152  floor = FloorEntry(
153  aliases=aliases or set(),
154  icon=icon,
155  floor_id=self._generate_id_generate_id(name),
156  name=name,
157  level=level,
158  )
159  floor_id = floor.floor_id
160  self.floorsfloors[floor_id] = floor
161  self.async_schedule_save()
162 
163  self.hasshass.bus.async_fire_internal(
164  EVENT_FLOOR_REGISTRY_UPDATED,
165  EventFloorRegistryUpdatedData(action="create", floor_id=floor_id),
166  )
167  return floor
168 
169  @callback
170  def async_delete(self, floor_id: str) -> None:
171  """Delete floor."""
172  self.hasshass.verify_event_loop_thread("floor_registry.async_delete")
173  del self.floorsfloors[floor_id]
174  self.hasshass.bus.async_fire_internal(
175  EVENT_FLOOR_REGISTRY_UPDATED,
177  action="remove",
178  floor_id=floor_id,
179  ),
180  )
181  self.async_schedule_save()
182 
183  @callback
185  self,
186  floor_id: str,
187  *,
188  aliases: set[str] | UndefinedType = UNDEFINED,
189  icon: str | None | UndefinedType = UNDEFINED,
190  level: int | UndefinedType = UNDEFINED,
191  name: str | UndefinedType = UNDEFINED,
192  ) -> FloorEntry:
193  """Update name of the floor."""
194  old = self.floorsfloors[floor_id]
195  changes: dict[str, Any] = {
196  attr_name: value
197  for attr_name, value in (
198  ("aliases", aliases),
199  ("icon", icon),
200  ("level", level),
201  )
202  if value is not UNDEFINED and value != getattr(old, attr_name)
203  }
204  if name is not UNDEFINED and name != old.name:
205  changes["name"] = name
206 
207  if not changes:
208  return old
209 
210  changes["modified_at"] = utcnow()
211 
212  self.hasshass.verify_event_loop_thread("floor_registry.async_update")
213  new = self.floorsfloors[floor_id] = dataclasses.replace(old, **changes)
214 
215  self.async_schedule_save()
216  self.hasshass.bus.async_fire_internal(
217  EVENT_FLOOR_REGISTRY_UPDATED,
219  action="update",
220  floor_id=floor_id,
221  ),
222  )
223 
224  return new
225 
226  async def async_load(self) -> None:
227  """Load the floor registry."""
228  data = await self._store_store.async_load()
229  floors = NormalizedNameBaseRegistryItems[FloorEntry]()
230 
231  if data is not None:
232  for floor in data["floors"]:
233  floors[floor["floor_id"]] = FloorEntry(
234  aliases=set(floor["aliases"]),
235  icon=floor["icon"],
236  floor_id=floor["floor_id"],
237  name=floor["name"],
238  level=floor["level"],
239  created_at=datetime.fromisoformat(floor["created_at"]),
240  modified_at=datetime.fromisoformat(floor["modified_at"]),
241  )
242 
243  self.floorsfloors = floors
244  self._floor_data_floor_data = floors.data
245 
246  @callback
247  def _data_to_save(self) -> FloorRegistryStoreData:
248  """Return data of floor registry to store in a file."""
249  return {
250  "floors": [
251  {
252  "aliases": list(entry.aliases),
253  "floor_id": entry.floor_id,
254  "icon": entry.icon,
255  "level": entry.level,
256  "name": entry.name,
257  "created_at": entry.created_at.isoformat(),
258  "modified_at": entry.modified_at.isoformat(),
259  }
260  for entry in self.floorsfloors.values()
261  ]
262  }
263 
264 
265 @callback
266 @singleton(DATA_REGISTRY)
267 def async_get(hass: HomeAssistant) -> FloorRegistry:
268  """Get floor registry."""
269  return FloorRegistry(hass)
270 
271 
272 async def async_load(hass: HomeAssistant) -> None:
273  """Load floor registry."""
274  assert DATA_REGISTRY not in hass.data
275  await async_get(hass).async_load()
FloorRegistryStoreData _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, list[dict[str, Any]]] old_data)
FloorEntry async_update(self, str floor_id, *set[str]|UndefinedType aliases=UNDEFINED, str|None|UndefinedType icon=UNDEFINED, int|UndefinedType level=UNDEFINED, str|UndefinedType name=UNDEFINED)
FloorEntry async_create(self, str name, *set[str]|None aliases=None, str|None icon=None, int|None level=None)
FloorEntry|None async_get_floor(self, str floor_id)
FloorEntry|None async_get_floor_by_name(self, str name)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
FloorRegistry async_get(HomeAssistant hass)
None async_load(HomeAssistant hass)