Home Assistant Unofficial Reference 2024.12.1
label_registry.py
Go to the documentation of this file.
1 """Provide a way to label and group anything."""
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[LabelRegistry] = HassKey("label_registry")
26 EVENT_LABEL_REGISTRY_UPDATED: EventType[EventLabelRegistryUpdatedData] = EventType(
27  "label_registry_updated"
28 )
29 STORAGE_KEY = "core.label_registry"
30 STORAGE_VERSION_MAJOR = 1
31 STORAGE_VERSION_MINOR = 2
32 
33 
34 class _LabelStoreData(TypedDict):
35  """Data type for individual label. Used in LabelRegistryStoreData."""
36 
37  color: str | None
38  description: str | None
39  icon: str | None
40  label_id: str
41  name: str
42  created_at: str
43  modified_at: str
44 
45 
46 class LabelRegistryStoreData(TypedDict):
47  """Store data type for LabelRegistry."""
48 
49  labels: list[_LabelStoreData]
50 
51 
53  """Event data for when the label registry is updated."""
54 
55  action: Literal["create", "remove", "update"]
56  label_id: str
57 
58 
59 type EventLabelRegistryUpdated = Event[EventLabelRegistryUpdatedData]
60 
61 
62 @dataclass(slots=True, frozen=True, kw_only=True)
64  """Label Registry Entry."""
65 
66  label_id: str
67  description: str | None = None
68  color: str | None = None
69  icon: str | None = None
70 
71 
72 class LabelRegistryStore(Store[LabelRegistryStoreData]):
73  """Store label 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  ) -> LabelRegistryStoreData:
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 label in old_data["labels"]:
90  label["created_at"] = label["modified_at"] = created_at
91 
92  return old_data # type: ignore[return-value]
93 
94 
95 class LabelRegistry(BaseRegistry[LabelRegistryStoreData]):
96  """Class to hold a registry of labels."""
97 
98  labels: NormalizedNameBaseRegistryItems[LabelEntry]
99  _label_data: dict[str, LabelEntry]
100 
101  def __init__(self, hass: HomeAssistant) -> None:
102  """Initialize the label 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_label(self, label_id: str) -> LabelEntry | None:
114  """Get label by ID.
115 
116  We retrieve the LabelEntry from the underlying dict to avoid
117  the overhead of the UserDict __getitem__.
118  """
119  return self._label_data_label_data.get(label_id)
120 
121  @callback
122  def async_get_label_by_name(self, name: str) -> LabelEntry | None:
123  """Get label by name."""
124  return self.labelslabels.get_by_name(name)
125 
126  @callback
127  def async_list_labels(self) -> Iterable[LabelEntry]:
128  """Get all labels."""
129  return self.labelslabels.values()
130 
131  def _generate_id(self, name: str) -> str:
132  """Generate label ID."""
133  return self.labelslabels.generate_id_from_name(name)
134 
135  @callback
137  self,
138  name: str,
139  *,
140  color: str | None = None,
141  icon: str | None = None,
142  description: str | None = None,
143  ) -> LabelEntry:
144  """Create a new label."""
145  self.hasshass.verify_event_loop_thread("label_registry.async_create")
146 
147  if label := self.async_get_label_by_nameasync_get_label_by_name(name):
148  raise ValueError(
149  f"The name {name} ({label.normalized_name}) is already in use"
150  )
151 
152  label = LabelEntry(
153  color=color,
154  description=description,
155  icon=icon,
156  label_id=self._generate_id_generate_id(name),
157  name=name,
158  )
159  label_id = label.label_id
160  self.labelslabels[label_id] = label
161  self.async_schedule_save()
162 
163  self.hasshass.bus.async_fire_internal(
164  EVENT_LABEL_REGISTRY_UPDATED,
165  EventLabelRegistryUpdatedData(action="create", label_id=label_id),
166  )
167  return label
168 
169  @callback
170  def async_delete(self, label_id: str) -> None:
171  """Delete label."""
172  self.hasshass.verify_event_loop_thread("label_registry.async_delete")
173  del self.labelslabels[label_id]
174  self.hasshass.bus.async_fire_internal(
175  EVENT_LABEL_REGISTRY_UPDATED,
177  action="remove",
178  label_id=label_id,
179  ),
180  )
181  self.async_schedule_save()
182 
183  @callback
185  self,
186  label_id: str,
187  *,
188  color: str | None | UndefinedType = UNDEFINED,
189  description: str | None | UndefinedType = UNDEFINED,
190  icon: str | None | UndefinedType = UNDEFINED,
191  name: str | UndefinedType = UNDEFINED,
192  ) -> LabelEntry:
193  """Update name of label."""
194  old = self.labelslabels[label_id]
195  changes: dict[str, Any] = {
196  attr_name: value
197  for attr_name, value in (
198  ("color", color),
199  ("description", description),
200  ("icon", icon),
201  )
202  if value is not UNDEFINED and getattr(old, attr_name) != value
203  }
204 
205  if name is not UNDEFINED and name != old.name:
206  changes["name"] = name
207 
208  if not changes:
209  return old
210 
211  changes["modified_at"] = utcnow()
212 
213  self.hasshass.verify_event_loop_thread("label_registry.async_update")
214  new = self.labelslabels[label_id] = dataclasses.replace(old, **changes)
215 
216  self.async_schedule_save()
217  self.hasshass.bus.async_fire_internal(
218  EVENT_LABEL_REGISTRY_UPDATED,
220  action="update",
221  label_id=label_id,
222  ),
223  )
224 
225  return new
226 
227  async def async_load(self) -> None:
228  """Load the label registry."""
229  data = await self._store_store.async_load()
230  labels = NormalizedNameBaseRegistryItems[LabelEntry]()
231 
232  if data is not None:
233  for label in data["labels"]:
234  labels[label["label_id"]] = LabelEntry(
235  color=label["color"],
236  description=label["description"],
237  icon=label["icon"],
238  label_id=label["label_id"],
239  name=label["name"],
240  created_at=datetime.fromisoformat(label["created_at"]),
241  modified_at=datetime.fromisoformat(label["modified_at"]),
242  )
243 
244  self.labelslabels = labels
245  self._label_data_label_data = labels.data
246 
247  @callback
248  def _data_to_save(self) -> LabelRegistryStoreData:
249  """Return data of label registry to store in a file."""
250  return {
251  "labels": [
252  {
253  "color": entry.color,
254  "description": entry.description,
255  "icon": entry.icon,
256  "label_id": entry.label_id,
257  "name": entry.name,
258  "created_at": entry.created_at.isoformat(),
259  "modified_at": entry.modified_at.isoformat(),
260  }
261  for entry in self.labelslabels.values()
262  ]
263  }
264 
265 
266 @callback
267 @singleton(DATA_REGISTRY)
268 def async_get(hass: HomeAssistant) -> LabelRegistry:
269  """Get label registry."""
270  return LabelRegistry(hass)
271 
272 
273 async def async_load(hass: HomeAssistant) -> None:
274  """Load label registry."""
275  assert DATA_REGISTRY not in hass.data
276  await async_get(hass).async_load()
LabelRegistryStoreData _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, list[dict[str, Any]]] old_data)
LabelEntry|None async_get_label_by_name(self, str name)
LabelEntry async_update(self, str label_id, *str|None|UndefinedType color=UNDEFINED, str|None|UndefinedType description=UNDEFINED, str|None|UndefinedType icon=UNDEFINED, str|UndefinedType name=UNDEFINED)
LabelEntry async_create(self, str name, *str|None color=None, str|None icon=None, str|None description=None)
LabelEntry|None async_get_label(self, str label_id)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_load(HomeAssistant hass)
LabelRegistry async_get(HomeAssistant hass)