Home Assistant Unofficial Reference 2024.12.1
collection.py
Go to the documentation of this file.
1 """Helper to deal with YAML + storage."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 import asyncio
7 from collections.abc import Awaitable, Callable, Coroutine, Iterable
8 from dataclasses import dataclass
9 from functools import partial
10 from hashlib import md5
11 from itertools import groupby
12 import logging
13 from operator import attrgetter
14 from typing import Any, Generic, TypedDict
15 
16 from typing_extensions import TypeVar
17 import voluptuous as vol
18 from voluptuous.humanize import humanize_error
19 
20 from homeassistant.components import websocket_api
21 from homeassistant.const import CONF_ID
22 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
23 from homeassistant.exceptions import HomeAssistantError
24 from homeassistant.util import slugify
25 
26 from . import entity_registry
27 from .entity import Entity
28 from .entity_component import EntityComponent
29 from .json import json_bytes
30 from .storage import Store
31 from .typing import ConfigType, VolDictType
32 
33 STORAGE_VERSION = 1
34 SAVE_DELAY = 10
35 
36 CHANGE_ADDED = "added"
37 CHANGE_UPDATED = "updated"
38 CHANGE_REMOVED = "removed"
39 
40 _EntityT = TypeVar("_EntityT", bound=Entity, default=Entity)
41 
42 
43 @dataclass(slots=True)
45  """Class to represent an item in a change set.
46 
47  change_type: One of CHANGE_*
48  item_id: The id of the item
49  item: The item
50  """
51 
52  change_type: str
53  item_id: str
54  item: Any
55  item_hash: str | None = None
56 
57 
58 type ChangeListener = Callable[
59  [
60  # Change type
61  str,
62  # Item ID
63  str,
64  # New or removed config
65  dict,
66  ],
67  Awaitable[None],
68 ]
69 
70 type ChangeSetListener = Callable[[Iterable[CollectionChange]], Awaitable[None]]
71 
72 
74  """Base class for collection related errors."""
75 
76 
77 class ItemNotFound(CollectionError):
78  """Raised when an item is not found."""
79 
80  def __init__(self, item_id: str) -> None:
81  """Initialize item not found error."""
82  super().__init__(f"Item {item_id} not found.")
83  self.item_iditem_id = item_id
84 
85 
86 class IDManager:
87  """Keep track of IDs across different collections."""
88 
89  def __init__(self) -> None:
90  """Initiate the ID manager."""
91  self.collections: list[dict[str, Any]] = []
92 
93  def add_collection(self, collection: dict[str, Any]) -> None:
94  """Add a collection to check for ID usage."""
95  self.collections.append(collection)
96 
97  def has_id(self, item_id: str) -> bool:
98  """Test if the ID exists."""
99  return any(item_id in collection for collection in self.collections)
100 
101  def generate_id(self, suggestion: str) -> str:
102  """Generate an ID."""
103  base = slugify(suggestion)
104  proposal = base
105  attempt = 1
106 
107  while self.has_idhas_id(proposal):
108  attempt += 1
109  proposal = f"{base}_{attempt}"
110 
111  return proposal
112 
113 
115  """Mixin class for entities managed by an ObservableCollection."""
116 
117  @classmethod
118  @abstractmethod
119  def from_storage(cls, config: ConfigType) -> CollectionEntity:
120  """Create instance from storage."""
121 
122  @classmethod
123  @abstractmethod
124  def from_yaml(cls, config: ConfigType) -> CollectionEntity:
125  """Create instance from yaml config."""
126 
127  @abstractmethod
128  async def async_update_config(self, config: ConfigType) -> None:
129  """Handle updated configuration."""
130 
131 
132 class ObservableCollection[_ItemT](ABC):
133  """Base collection type that can be observed."""
134 
135  def __init__(self, id_manager: IDManager | None) -> None:
136  """Initialize the base collection."""
137  self.id_managerid_manager = id_manager or IDManager()
138  self.data: dict[str, _ItemT] = {}
139  self.listeners: list[ChangeListener] = []
140  self.change_set_listeners: list[ChangeSetListener] = []
141 
142  self.id_managerid_manager.add_collection(self.data)
143 
144  @callback
145  def async_items(self) -> list[_ItemT]:
146  """Return list of items in collection."""
147  return list(self.data.values())
148 
149  @callback
150  def async_add_listener(self, listener: ChangeListener) -> Callable[[], None]:
151  """Add a listener.
152 
153  Will be called with (change_type, item_id, updated_config).
154  """
155  self.listeners.append(listener)
156  return partial(self.listeners.remove, listener)
157 
158  @callback
160  self, listener: ChangeSetListener
161  ) -> Callable[[], None]:
162  """Add a listener for a full change set.
163 
164  Will be called with [(change_type, item_id, updated_config), ...]
165  """
166  self.change_set_listeners.append(listener)
167  return partial(self.change_set_listeners.remove, listener)
168 
169  async def notify_changes(self, change_set: Iterable[CollectionChange]) -> None:
170  """Notify listeners of a change."""
171  await asyncio.gather(
172  *(
173  listener(change.change_type, change.item_id, change.item)
174  for listener in self.listeners
175  for change in change_set
176  ),
177  *(
178  change_set_listener(change_set)
179  for change_set_listener in self.change_set_listeners
180  ),
181  )
182 
183 
185  """Offer a collection based on static data."""
186 
187  def __init__(
188  self,
189  logger: logging.Logger,
190  id_manager: IDManager | None = None,
191  ) -> None:
192  """Initialize the storage collection."""
193  super().__init__(id_manager)
194  self.loggerlogger = logger
195 
196  @staticmethod
198  entity_class: type[CollectionEntity], config: ConfigType
199  ) -> CollectionEntity:
200  """Create a CollectionEntity instance."""
201  return entity_class.from_yaml(config)
202 
203  async def async_load(self, data: list[dict]) -> None:
204  """Load the YAML collection. Overrides existing data."""
205  old_ids = set(self.data)
206 
207  change_set = []
208 
209  for item in data:
210  item_id = item[CONF_ID]
211 
212  if item_id in old_ids:
213  old_ids.remove(item_id)
214  event = CHANGE_UPDATED
215  elif self.id_managerid_manager.has_id(item_id):
216  self.loggerlogger.warning("Duplicate ID '%s' detected, skipping", item_id)
217  continue
218  else:
219  event = CHANGE_ADDED
220 
221  self.data[item_id] = item
222  change_set.append(CollectionChange(event, item_id, item))
223 
224  change_set.extend(
225  CollectionChange(CHANGE_REMOVED, item_id, self.data.pop(item_id))
226  for item_id in old_ids
227  )
228 
229  if change_set:
230  await self.notify_changesnotify_changes(change_set)
231 
232 
234  """Serialized storage collection."""
235 
236  items: list[dict[str, Any]]
237 
238 
239 class StorageCollection[_ItemT, _StoreT: SerializedStorageCollection](
240  ObservableCollection[_ItemT]
241 ):
242  """Offer a CRUD interface on top of JSON storage."""
243 
244  def __init__(
245  self,
246  store: Store[_StoreT],
247  id_manager: IDManager | None = None,
248  ) -> None:
249  """Initialize the storage collection."""
250  super().__init__(id_manager)
251  self.store = store
252 
253  @staticmethod
255  entity_class: type[CollectionEntity], config: ConfigType
256  ) -> CollectionEntity:
257  """Create a CollectionEntity instance."""
258  return entity_class.from_storage(config)
259 
260  @property
261  def hass(self) -> HomeAssistant:
262  """Home Assistant object."""
263  return self.store.hass
264 
265  async def _async_load_data(self) -> _StoreT | None:
266  """Load the data."""
267  return await self.store.async_load()
268 
269  async def async_load(self) -> None:
270  """Load the storage Manager."""
271  if not (raw_storage := await self._async_load_data()):
272  return
273 
274  for item in raw_storage["items"]:
275  self.data[item[CONF_ID]] = self._deserialize_item(item)
276 
277  await self.notify_changes(
278  [
280  CHANGE_ADDED, item[CONF_ID], item, self._hash_item(item)
281  )
282  for item in raw_storage["items"]
283  ]
284  )
285 
286  @abstractmethod
287  async def _process_create_data(self, data: dict) -> dict:
288  """Validate the config is valid."""
289 
290  @callback
291  @abstractmethod
292  def _get_suggested_id(self, info: dict) -> str:
293  """Suggest an ID based on the config."""
294 
295  @abstractmethod
296  async def _update_data(self, item: _ItemT, update_data: dict) -> _ItemT:
297  """Return a new updated item."""
298 
299  @abstractmethod
300  def _create_item(self, item_id: str, data: dict) -> _ItemT:
301  """Create an item from validated config."""
302 
303  @abstractmethod
304  def _deserialize_item(self, data: dict) -> _ItemT:
305  """Create an item from its serialized representation."""
306 
307  @abstractmethod
308  def _serialize_item(self, item_id: str, item: _ItemT) -> dict:
309  """Return the serialized representation of an item for storing.
310 
311  The serialized representation must include the item_id in the "id" key.
312  """
313 
314  async def async_create_item(self, data: dict) -> _ItemT:
315  """Create a new item."""
316  validated_data = await self._process_create_data(data)
317  item_id = self.id_manager.generate_id(self._get_suggested_id(validated_data))
318  item = self._create_item(item_id, validated_data)
319  self.data[item_id] = item
320  self._async_schedule_save()
321  await self.notify_changes(
322  [
324  CHANGE_ADDED,
325  item_id,
326  item,
327  self._hash_item(self._serialize_item(item_id, item)),
328  )
329  ]
330  )
331  return item
332 
333  async def async_update_item(self, item_id: str, updates: dict) -> _ItemT:
334  """Update item."""
335  if item_id not in self.data:
336  raise ItemNotFound(item_id)
337 
338  if CONF_ID in updates:
339  raise ValueError("Cannot update ID")
340 
341  current = self.data[item_id]
342 
343  updated = await self._update_data(current, updates)
344 
345  self.data[item_id] = updated
346  self._async_schedule_save()
347 
348  await self.notify_changes(
349  [
351  CHANGE_UPDATED,
352  item_id,
353  updated,
354  self._hash_item(self._serialize_item(item_id, updated)),
355  )
356  ]
357  )
358 
359  return self.data[item_id]
360 
361  async def async_delete_item(self, item_id: str) -> None:
362  """Delete item."""
363  if item_id not in self.data:
364  raise ItemNotFound(item_id)
365 
366  item = self.data.pop(item_id)
367  self._async_schedule_save()
368 
369  await self.notify_changes([CollectionChange(CHANGE_REMOVED, item_id, item)])
370 
371  @callback
372  def _async_schedule_save(self) -> None:
373  """Schedule saving the collection."""
374  self.store.async_delay_save(self._data_to_save, SAVE_DELAY)
375 
376  @callback
377  def _base_data_to_save(self) -> SerializedStorageCollection:
378  """Return JSON-compatible data for storing to file."""
379  return {
380  "items": [
381  self._serialize_item(item_id, item)
382  for item_id, item in self.data.items()
383  ]
384  }
385 
386  @abstractmethod
387  @callback
388  def _data_to_save(self) -> _StoreT:
389  """Return JSON-compatible date for storing to file."""
390 
391  def _hash_item(self, item: dict) -> str:
392  """Return a hash of the item."""
393  return md5(json_bytes(item)).hexdigest()
394 
395 
396 class DictStorageCollection(StorageCollection[dict, SerializedStorageCollection]):
397  """A specialized StorageCollection where the items are untyped dicts."""
398 
399  def _create_item(self, item_id: str, data: dict) -> dict:
400  """Create an item from its validated, serialized representation."""
401  return {CONF_ID: item_id} | data
402 
403  def _deserialize_item(self, data: dict) -> dict:
404  """Create an item from its validated, serialized representation."""
405  return data
406 
407  def _serialize_item(self, item_id: str, item: dict) -> dict:
408  """Return the serialized representation of an item for storing."""
409  return item
410 
411  @callback
412  def _data_to_save(self) -> SerializedStorageCollection:
413  """Return JSON-compatible date for storing to file."""
414  return self._base_data_to_save()
415 
416 
418  """A collection without IDs."""
419 
420  counter = 0
421 
422  async def async_load(self, data: list[dict]) -> None:
423  """Load the collection. Overrides existing data."""
424  await self.notify_changesnotify_changes(
425  [
426  CollectionChange(CHANGE_REMOVED, item_id, item)
427  for item_id, item in list(self.data.items())
428  ]
429  )
430 
431  self.data.clear()
432 
433  for item in data:
434  self.countercounter += 1
435  item_id = f"fakeid-{self.counter}"
436 
437  self.data[item_id] = item
438 
439  await self.notify_changesnotify_changes(
440  [
441  CollectionChange(CHANGE_ADDED, item_id, item)
442  for item_id, item in self.data.items()
443  ]
444  )
445 
446 
447 _GROUP_BY_KEY = attrgetter("change_type")
448 
449 
450 @dataclass(slots=True, frozen=True)
451 class _CollectionLifeCycle(Generic[_EntityT]):
452  """Life cycle for a collection of entities."""
453 
454  domain: str
455  platform: str
456  entity_component: EntityComponent[_EntityT]
457  collection: StorageCollection | YamlCollection
458  entity_class: type[CollectionEntity]
460  entities: dict[str, CollectionEntity]
461 
462  @callback
463  def async_setup(self) -> None:
464  """Set up the collection life cycle."""
465  self.collection.async_add_change_set_listener(self._collection_changed_collection_changed)
466 
467  def _entity_removed(self, item_id: str) -> None:
468  """Remove entity from entities if it's removed or not added."""
469  self.entities.pop(item_id, None)
470 
471  @callback
472  def _add_entity(self, change_set: CollectionChange) -> CollectionEntity:
473  item_id = change_set.item_id
474  entity = self.collection.create_entity(self.entity_class, change_set.item)
475  self.entities[item_id] = entity
476  entity.async_on_remove(partial(self._entity_removed_entity_removed, item_id))
477  return entity
478 
479  async def _remove_entity(self, change_set: CollectionChange) -> None:
480  item_id = change_set.item_id
481  ent_reg = self.ent_reg
482  entities = self.entities
483  ent_to_remove = ent_reg.async_get_entity_id(self.domain, self.platform, item_id)
484  if ent_to_remove is not None:
485  ent_reg.async_remove(ent_to_remove)
486  elif entity := entities.get(item_id):
487  await entity.async_remove(force_remove=True)
488  # Unconditionally pop the entity from the entity list to avoid racing against
489  # the entity registry event handled by Entity._async_registry_updated
490  entities.pop(item_id, None)
491 
492  async def _update_entity(self, change_set: CollectionChange) -> None:
493  if entity := self.entities.get(change_set.item_id):
494  if change_set.item_hash:
495  self.ent_reg.async_update_entity_options(
496  entity.entity_id, "collection", {"hash": change_set.item_hash}
497  )
498  await entity.async_update_config(change_set.item)
499 
500  async def _collection_changed(self, change_set: Iterable[CollectionChange]) -> None:
501  """Handle a collection change."""
502  # Create a new bucket every time we have a different change type
503  # to ensure operations happen in order. We only group
504  # the same change type.
505  new_entities: list[CollectionEntity] = []
506  coros: list[Coroutine[Any, Any, CollectionEntity | None]] = []
507  grouped: Iterable[CollectionChange]
508  for _, grouped in groupby(change_set, _GROUP_BY_KEY):
509  for change in grouped:
510  change_type = change.change_type
511  if change_type == CHANGE_ADDED:
512  new_entities.append(self._add_entity_add_entity(change))
513  elif change_type == CHANGE_REMOVED:
514  coros.append(self._remove_entity_remove_entity(change))
515  elif change_type == CHANGE_UPDATED:
516  coros.append(self._update_entity_update_entity(change))
517 
518  if coros:
519  await asyncio.gather(*coros)
520 
521  if new_entities:
522  await self.entity_component.async_add_entities(new_entities)
523 
524 
525 @callback
527  hass: HomeAssistant,
528  domain: str,
529  platform: str,
530  entity_component: EntityComponent[_EntityT],
531  collection: StorageCollection | YamlCollection,
532  entity_class: type[CollectionEntity],
533 ) -> None:
534  """Map a collection to an entity component."""
535  ent_reg = entity_registry.async_get(hass)
537  domain, platform, entity_component, collection, entity_class, ent_reg, {}
538  ).async_setup()
539 
540 
541 class StorageCollectionWebsocket[_StorageCollectionT: StorageCollection]:
542  """Class to expose storage collection management over websocket."""
543 
544  def __init__(
545  self,
546  storage_collection: _StorageCollectionT,
547  api_prefix: str,
548  model_name: str,
549  create_schema: VolDictType,
550  update_schema: VolDictType,
551  ) -> None:
552  """Initialize a websocket CRUD."""
553  self.storage_collection = storage_collection
554  self.api_prefix = api_prefix
555  self.model_name = model_name
556  self.create_schema = create_schema
557  self.update_schema = update_schema
558 
559  self._remove_subscription: CALLBACK_TYPE | None = None
560  self._subscribers: set[tuple[websocket_api.ActiveConnection, int]] = set()
561 
562  assert self.api_prefix[-1] != "/", "API prefix should not end in /"
563 
564  @property
565  def item_id_key(self) -> str:
566  """Return item ID key."""
567  return f"{self.model_name}_id"
568 
569  @callback
570  def async_setup(self, hass: HomeAssistant) -> None:
571  """Set up the websocket commands."""
572  websocket_api.async_register_command(
573  hass,
574  f"{self.api_prefix}/list",
575  self.ws_list_item,
576  websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
577  {vol.Required("type"): f"{self.api_prefix}/list"}
578  ),
579  )
580 
581  websocket_api.async_register_command(
582  hass,
583  f"{self.api_prefix}/create",
584  websocket_api.require_admin(
585  websocket_api.async_response(self.ws_create_item)
586  ),
587  websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
588  {
589  **self.create_schema,
590  vol.Required("type"): f"{self.api_prefix}/create",
591  }
592  ),
593  )
594 
595  websocket_api.async_register_command(
596  hass,
597  f"{self.api_prefix}/subscribe",
598  self._ws_subscribe,
599  websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
600  {vol.Required("type"): f"{self.api_prefix}/subscribe"}
601  ),
602  )
603 
604  websocket_api.async_register_command(
605  hass,
606  f"{self.api_prefix}/update",
607  websocket_api.require_admin(
608  websocket_api.async_response(self.ws_update_item)
609  ),
610  websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
611  {
612  **self.update_schema,
613  vol.Required("type"): f"{self.api_prefix}/update",
614  vol.Required(self.item_id_key): str,
615  }
616  ),
617  )
618 
619  websocket_api.async_register_command(
620  hass,
621  f"{self.api_prefix}/delete",
622  websocket_api.require_admin(
623  websocket_api.async_response(self.ws_delete_item)
624  ),
625  websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
626  {
627  vol.Required("type"): f"{self.api_prefix}/delete",
628  vol.Required(self.item_id_key): str,
629  }
630  ),
631  )
632 
633  @callback
635  self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
636  ) -> None:
637  """List items."""
638  connection.send_result(msg["id"], self.storage_collection.async_items())
639 
640  async def ws_create_item(
641  self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
642  ) -> None:
643  """Create an item."""
644  try:
645  data = dict(msg)
646  data.pop("id")
647  data.pop("type")
648  item = await self.storage_collection.async_create_item(data)
649  connection.send_result(msg["id"], item)
650  except vol.Invalid as err:
651  connection.send_error(
652  msg["id"],
653  websocket_api.ERR_INVALID_FORMAT,
654  humanize_error(data, err),
655  )
656  except ValueError as err:
657  connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
658 
659  @callback
661  self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
662  ) -> None:
663  """Subscribe to collection updates."""
664 
665  async def async_change_listener(
666  change_set: Iterable[CollectionChange],
667  ) -> None:
668  json_msg = [
669  {
670  "change_type": change.change_type,
671  self.item_id_key: change.item_id,
672  "item": change.item,
673  }
674  for change in change_set
675  ]
676  for conn, msg_id in self._subscribers:
677  conn.send_message(websocket_api.event_message(msg_id, json_msg))
678 
679  if not self._subscribers:
680  self._remove_subscription = (
681  self.storage_collection.async_add_change_set_listener(
682  async_change_listener
683  )
684  )
685 
686  self._subscribers.add((connection, msg["id"]))
687 
688  @callback
689  def cancel_subscription() -> None:
690  self._subscribers.remove((connection, msg["id"]))
691  if not self._subscribers and self._remove_subscription:
692  self._remove_subscription()
693  self._remove_subscription = None
694 
695  connection.subscriptions[msg["id"]] = cancel_subscription
696 
697  connection.send_message(websocket_api.result_message(msg["id"]))
698 
699  json_msg = [
700  {
701  "change_type": CHANGE_ADDED,
702  self.item_id_key: item_id,
703  "item": item,
704  }
705  for item_id, item in self.storage_collection.data.items()
706  ]
707  connection.send_message(websocket_api.event_message(msg["id"], json_msg))
708 
709  async def ws_update_item(
710  self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
711  ) -> None:
712  """Update an item."""
713  data = dict(msg)
714  msg_id = data.pop("id")
715  item_id = data.pop(self.item_id_key)
716  data.pop("type")
717 
718  try:
719  item = await self.storage_collection.async_update_item(item_id, data)
720  connection.send_result(msg_id, item)
721  except ItemNotFound:
722  connection.send_error(
723  msg["id"],
724  websocket_api.ERR_NOT_FOUND,
725  f"Unable to find {self.item_id_key} {item_id}",
726  )
727  except vol.Invalid as err:
728  connection.send_error(
729  msg["id"],
730  websocket_api.ERR_INVALID_FORMAT,
731  humanize_error(data, err),
732  )
733  except ValueError as err:
734  connection.send_error(msg_id, websocket_api.ERR_INVALID_FORMAT, str(err))
735 
736  async def ws_delete_item(
737  self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
738  ) -> None:
739  """Delete an item."""
740  try:
741  await self.storage_collection.async_delete_item(msg[self.item_id_key])
742  except ItemNotFound:
743  connection.send_error(
744  msg["id"],
745  websocket_api.ERR_NOT_FOUND,
746  f"Unable to find {self.item_id_key} {msg[self.item_id_key]}",
747  )
748 
749  connection.send_result(msg["id"])
750 
751 
753  """Class to expose storage collection management over websocket."""
None async_update_config(self, ConfigType config)
Definition: collection.py:128
CollectionEntity from_yaml(cls, ConfigType config)
Definition: collection.py:124
CollectionEntity from_storage(cls, ConfigType config)
Definition: collection.py:119
dict _create_item(self, str item_id, dict data)
Definition: collection.py:399
SerializedStorageCollection _data_to_save(self)
Definition: collection.py:412
dict _serialize_item(self, str item_id, dict item)
Definition: collection.py:407
str generate_id(self, str suggestion)
Definition: collection.py:101
None add_collection(self, dict[str, Any] collection)
Definition: collection.py:93
Callable[[], None] async_add_listener(self, ChangeListener listener)
Definition: collection.py:150
None notify_changes(self, Iterable[CollectionChange] change_set)
Definition: collection.py:169
None __init__(self, IDManager|None id_manager)
Definition: collection.py:135
Callable[[], None] async_add_change_set_listener(self, ChangeSetListener listener)
Definition: collection.py:161
CollectionEntity create_entity(type[CollectionEntity] entity_class, ConfigType config)
Definition: collection.py:199
None __init__(self, logging.Logger logger, IDManager|None id_manager=None)
Definition: collection.py:191
None async_load(self, list[dict] data)
Definition: collection.py:203
None _collection_changed(self, Iterable[CollectionChange] change_set)
Definition: collection.py:500
None _remove_entity(self, CollectionChange change_set)
Definition: collection.py:479
None _update_entity(self, CollectionChange change_set)
Definition: collection.py:492
CollectionEntity _add_entity(self, CollectionChange change_set)
Definition: collection.py:472
bool add(self, _T matcher)
Definition: match.py:185
bool remove(self, _T matcher)
Definition: match.py:214
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
str humanize_error(HomeAssistant hass, vol.Invalid validation_error, str domain, dict config, str|None link, int max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH)
Definition: config.py:520
SerializedStorageCollection _base_data_to_save(self)
Definition: collection.py:377
str _hash_item(self, dict item)
Definition: collection.py:391
None _ws_subscribe(self, HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: collection.py:662
_ItemT _deserialize_item(self, dict data)
Definition: collection.py:304
None ws_create_item(self, HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: collection.py:642
str _get_suggested_id(self, dict info)
Definition: collection.py:292
None ws_update_item(self, HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: collection.py:711
_ItemT async_create_item(self, dict data)
Definition: collection.py:314
None __init__(self, Store[_StoreT] store, IDManager|None id_manager=None)
Definition: collection.py:248
dict _process_create_data(self, dict data)
Definition: collection.py:287
None ws_delete_item(self, HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: collection.py:738
HomeAssistant hass(self)
Definition: collection.py:261
_ItemT _create_item(self, str item_id, dict data)
Definition: collection.py:300
_StoreT|None _async_load_data(self)
Definition: collection.py:265
_ItemT _update_data(self, _ItemT item, dict update_data)
Definition: collection.py:296
None async_setup(self, HomeAssistant hass)
Definition: collection.py:570
CollectionEntity create_entity(type[CollectionEntity] entity_class, ConfigType config)
Definition: collection.py:256
None ws_list_item(self, HomeAssistant hass, websocket_api.ActiveConnection connection, dict msg)
Definition: collection.py:636
None sync_entity_lifecycle(HomeAssistant hass, str domain, str platform, EntityComponent[_EntityT] entity_component, StorageCollection|YamlCollection collection, type[CollectionEntity] entity_class)
Definition: collection.py:533
_ItemT async_update_item(self, str item_id, dict updates)
Definition: collection.py:333
dict _serialize_item(self, str item_id, _ItemT item)
Definition: collection.py:308
None async_delete_item(self, str item_id)
Definition: collection.py:361
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
Definition: storage.py:444