Home Assistant Unofficial Reference 2024.12.1
passive_update_processor.py
Go to the documentation of this file.
1 """Passive update processors for the Bluetooth integration."""
2 
3 from __future__ import annotations
4 
5 import dataclasses
6 from datetime import timedelta
7 from functools import cache
8 import logging
9 from typing import TYPE_CHECKING, Any, Self, TypedDict, cast
10 
11 from habluetooth import BluetoothScanningMode
12 
13 from homeassistant import config_entries
14 from homeassistant.const import (
15  ATTR_CONNECTIONS,
16  ATTR_IDENTIFIERS,
17  ATTR_NAME,
18  CONF_ENTITY_CATEGORY,
19  EVENT_HOMEASSISTANT_STOP,
20  EntityCategory,
21 )
22 from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
23 from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
24 from homeassistant.helpers.entity import Entity, EntityDescription
25 from homeassistant.helpers.entity_platform import async_get_current_platform
26 from homeassistant.helpers.event import async_track_time_interval
27 from homeassistant.helpers.storage import Store
28 from homeassistant.helpers.typing import UNDEFINED
29 from homeassistant.util.enum import try_parse_enum
30 
31 from .const import DOMAIN
32 from .update_coordinator import BasePassiveBluetoothCoordinator
33 
34 if TYPE_CHECKING:
35  from collections.abc import Callable
36 
37  from homeassistant.helpers.entity_platform import AddEntitiesCallback
38 
39  from .models import BluetoothChange, BluetoothServiceInfoBleak
40 
41 STORAGE_KEY = "bluetooth.passive_update_processor"
42 STORAGE_VERSION = 1
43 STORAGE_SAVE_INTERVAL = timedelta(minutes=15)
44 PASSIVE_UPDATE_PROCESSOR = "passive_update_processor"
45 
46 
47 @dataclasses.dataclass(slots=True, frozen=True)
49  """Key for a passive bluetooth entity.
50 
51  Example:
52  key: temperature
53  device_id: outdoor_sensor_1
54 
55  """
56 
57  key: str
58  device_id: str | None
59 
60  def to_string(self) -> str:
61  """Convert the key to a string which can be used as JSON key."""
62  return f"{self.key}___{self.device_id or ''}"
63 
64  @classmethod
65  def from_string(cls, key: str) -> PassiveBluetoothEntityKey:
66  """Convert a string (from JSON) to a key."""
67  key, device_id = key.split("___")
68  return cls(key, device_id or None)
69 
70 
71 @dataclasses.dataclass(slots=True, frozen=False)
73  """Data for the passive bluetooth processor."""
74 
75  coordinators: set[PassiveBluetoothProcessorCoordinator[Any]]
76  all_restore_data: dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]]
77 
78 
80  """Restored PassiveBluetoothDataUpdate."""
81 
82  devices: dict[str, DeviceInfo]
83  entity_descriptions: dict[str, dict[str, Any]]
84  entity_names: dict[str, str | None]
85  entity_data: dict[str, Any]
86 
87 
88 # Fields do not change so we can cache the result
89 # of calling fields() on the dataclass
90 cached_fields = cache(dataclasses.fields)
91 
92 
94  descriptions_class: type[EntityDescription], data: dict[str, Any]
95 ) -> EntityDescription:
96  """Deserialize an entity description."""
97  result: dict[str, Any] = {}
98  if hasattr(descriptions_class, "_dataclass"):
99  descriptions_class = descriptions_class._dataclass # noqa: SLF001
100  for field in cached_fields(descriptions_class):
101  field_name = field.name
102  # It would be nice if field.type returned the actual
103  # type instead of a str so we could avoid writing this
104  # out, but it doesn't. If we end up using this in more
105  # places we can add a `as_dict` and a `from_dict`
106  # method to these classes
107  if field_name == CONF_ENTITY_CATEGORY:
108  value = try_parse_enum(EntityCategory, data.get(field_name))
109  else:
110  value = data.get(field_name)
111  result[field_name] = value
112  return descriptions_class(**result)
113 
114 
115 def serialize_entity_description(description: EntityDescription) -> dict[str, Any]:
116  """Serialize an entity description."""
117  return {
118  field.name: value
119  for field in cached_fields(type(description))
120  if (value := getattr(description, field.name)) != field.default
121  }
122 
123 
124 @dataclasses.dataclass(slots=True, frozen=False)
126  """Generic bluetooth data."""
127 
128  devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict)
129  entity_descriptions: dict[PassiveBluetoothEntityKey, EntityDescription] = (
130  dataclasses.field(default_factory=dict)
131  )
132  entity_names: dict[PassiveBluetoothEntityKey, str | None] = dataclasses.field(
133  default_factory=dict
134  )
135  entity_data: dict[PassiveBluetoothEntityKey, _T] = dataclasses.field(
136  default_factory=dict
137  )
138 
139  def update(
140  self, new_data: PassiveBluetoothDataUpdate[_T]
141  ) -> set[PassiveBluetoothEntityKey] | None:
142  """Update the data and returned changed PassiveBluetoothEntityKey or None on device change.
143 
144  The changed PassiveBluetoothEntityKey can be used to filter
145  which listeners are called.
146  """
147  device_change = False
148  changed_entity_keys: set[PassiveBluetoothEntityKey] = set()
149  for device_key, device_info in new_data.devices.items():
150  if device_change or self.devices.get(device_key, UNDEFINED) != device_info:
151  device_change = True
152  self.devices[device_key] = device_info
153  for incoming, current in (
154  (new_data.entity_descriptions, self.entity_descriptions),
155  (new_data.entity_names, self.entity_names),
156  (new_data.entity_data, self.entity_data),
157  ):
158  for key, data in incoming.items():
159  if current.get(key, UNDEFINED) != data:
160  changed_entity_keys.add(key)
161  current[key] = data # type: ignore[assignment]
162  # If the device changed we don't need to return the changed
163  # entity keys as all entities will be updated
164  return None if device_change else changed_entity_keys
165 
166  def async_get_restore_data(self) -> RestoredPassiveBluetoothDataUpdate:
167  """Serialize restore data to storage."""
168  return {
169  "devices": {
170  key or "": device_info for key, device_info in self.devices.items()
171  },
172  "entity_descriptions": {
173  key.to_string(): serialize_entity_description(description)
174  for key, description in self.entity_descriptions.items()
175  },
176  "entity_names": {
177  key.to_string(): name for key, name in self.entity_names.items()
178  },
179  "entity_data": {
180  key.to_string(): data for key, data in self.entity_data.items()
181  },
182  }
183 
184  @callback
186  self,
187  restore_data: RestoredPassiveBluetoothDataUpdate,
188  entity_description_class: type[EntityDescription],
189  ) -> None:
190  """Set the restored data from storage."""
191  self.devices.update(
192  {
193  key or None: device_info
194  for key, device_info in restore_data["devices"].items()
195  }
196  )
197  self.entity_descriptions.update(
198  {
199  PassiveBluetoothEntityKey.from_string(
200  key
201  ): deserialize_entity_description(entity_description_class, description)
202  for key, description in restore_data["entity_descriptions"].items()
203  if description
204  }
205  )
206  self.entity_names.update(
207  {
208  PassiveBluetoothEntityKey.from_string(key): name
209  for key, name in restore_data["entity_names"].items()
210  }
211  )
212  self.entity_data.update(
213  {
214  PassiveBluetoothEntityKey.from_string(key): cast(_T, data)
215  for key, data in restore_data["entity_data"].items()
216  }
217  )
218 
219 
221  hass: HomeAssistant, coordinator: PassiveBluetoothProcessorCoordinator[Any]
222 ) -> CALLBACK_TYPE:
223  """Register a coordinator to have its processors data restored."""
224  data: PassiveBluetoothProcessorData = hass.data[PASSIVE_UPDATE_PROCESSOR]
225  coordinators = data.coordinators
226  coordinators.add(coordinator)
227  if restore_key := coordinator.restore_key:
228  coordinator.restore_data = data.all_restore_data.setdefault(restore_key, {})
229 
230  @callback
231  def _unregister_coordinator_for_restore() -> None:
232  """Unregister a coordinator."""
233  coordinators.remove(coordinator)
234 
235  return _unregister_coordinator_for_restore
236 
237 
238 async def async_setup(hass: HomeAssistant) -> None:
239  """Set up the passive update processor coordinators."""
240  storage: Store[dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]]] = Store(
241  hass, STORAGE_VERSION, STORAGE_KEY
242  )
243  coordinators: set[PassiveBluetoothProcessorCoordinator[Any]] = set()
244  all_restore_data: dict[str, dict[str, RestoredPassiveBluetoothDataUpdate]] = (
245  await storage.async_load() or {}
246  )
247  hass.data[PASSIVE_UPDATE_PROCESSOR] = PassiveBluetoothProcessorData(
248  coordinators, all_restore_data
249  )
250 
251  async def _async_save_processor_data(_: Any) -> None:
252  """Save the processor data."""
253  await storage.async_save(
254  {
255  coordinator.restore_key: coordinator.async_get_restore_data()
256  for coordinator in coordinators
257  if coordinator.restore_key
258  }
259  )
260 
261  cancel_interval = async_track_time_interval(
262  hass, _async_save_processor_data, STORAGE_SAVE_INTERVAL
263  )
264 
265  async def _async_save_processor_data_at_stop(_event: Event) -> None:
266  """Save the processor data at shutdown."""
267  cancel_interval()
268  await _async_save_processor_data(None)
269 
270  hass.bus.async_listen_once(
271  EVENT_HOMEASSISTANT_STOP,
272  _async_save_processor_data_at_stop,
273  )
274 
275 
276 class PassiveBluetoothProcessorCoordinator[_DataT](BasePassiveBluetoothCoordinator):
277  """Passive bluetooth processor coordinator for bluetooth advertisements.
278 
279  The coordinator is responsible for dispatching the bluetooth data,
280  to each processor, and tracking devices.
281 
282  The update_method should return the data that is dispatched to each processor.
283  This is normally a parsed form of the data, but you can just forward the
284  BluetoothServiceInfoBleak if needed.
285  """
286 
287  def __init__(
288  self,
289  hass: HomeAssistant,
290  logger: logging.Logger,
291  address: str,
292  mode: BluetoothScanningMode,
293  update_method: Callable[[BluetoothServiceInfoBleak], _DataT],
294  connectable: bool = False,
295  ) -> None:
296  """Initialize the coordinator."""
297  super().__init__(hass, logger, address, mode, connectable)
298  self._processors: list[PassiveBluetoothDataProcessor[Any, _DataT]] = []
299  self._update_method_update_method = update_method
300  self.last_update_successlast_update_success = True
301  self.restore_data: dict[str, RestoredPassiveBluetoothDataUpdate] = {}
302  self.restore_keyrestore_key = None
303  if config_entry := config_entries.current_entry.get():
304  self.restore_keyrestore_key = config_entry.entry_id
305  self._on_stop.append(async_register_coordinator_for_restore(self.hasshass, self))
306 
307  @property
308  def available(self) -> bool:
309  """Return if the device is available."""
310  return self._available_available_available and self.last_update_successlast_update_success
311 
312  @callback
314  self,
315  ) -> dict[str, RestoredPassiveBluetoothDataUpdate]:
316  """Generate the restore data."""
317  return {
318  processor.restore_key: processor.data.async_get_restore_data()
319  for processor in self._processors
320  if processor.restore_key
321  }
322 
323  @callback
325  self,
326  processor: PassiveBluetoothDataProcessor[Any, _DataT],
327  entity_description_class: type[EntityDescription] | None = None,
328  ) -> Callable[[], None]:
329  """Register a processor that subscribes to updates."""
330 
331  # entity_description_class will become mandatory
332  # in the future, but is optional for now to allow
333  # for a transition period.
334  processor.async_register_coordinator(self, entity_description_class)
335 
336  @callback
337  def remove_processor() -> None:
338  """Remove a processor."""
339  # Save the data before removing the processor
340  # so if they reload its still there
341  if restore_key := processor.restore_key:
342  self.restore_data[restore_key] = processor.data.async_get_restore_data()
343 
344  self._processors.remove(processor)
345 
346  self._processors.append(processor)
347  return remove_processor
348 
349  @callback
351  self, service_info: BluetoothServiceInfoBleak
352  ) -> None:
353  """Handle the device going unavailable."""
354  super()._async_handle_unavailable(service_info)
355  for processor in self._processors:
356  processor.async_handle_unavailable()
357 
358  @callback
360  self,
361  service_info: BluetoothServiceInfoBleak,
362  change: BluetoothChange,
363  ) -> None:
364  """Handle a Bluetooth event."""
365  was_available = self._available_available_available
366  self._available_available_available = True
367  if self.hasshass.is_stopping:
368  return
369 
370  try:
371  update = self._update_method_update_method(service_info)
372  except Exception:
373  self.last_update_successlast_update_success = False
374  self.loggerlogger.exception("Unexpected error updating %s data", self.namename)
375  return
376 
377  if not self.last_update_successlast_update_success:
378  self.last_update_successlast_update_success = True
379  self.loggerlogger.info("Coordinator %s recovered", self.namename)
380 
381  for processor in self._processors:
382  processor.async_handle_update(update, was_available)
383 
384 
386  """Passive bluetooth data processor for bluetooth advertisements.
387 
388  The processor is responsible for keeping track of the bluetooth data
389  and updating subscribers.
390 
391  The update_method must return a PassiveBluetoothDataUpdate object. Callers
392  are responsible for formatting the data returned from their parser into
393  the appropriate format.
394 
395  The processor will call the update_method every time the bluetooth device
396  receives a new advertisement data from the coordinator with the data
397  returned by the update_method of the coordinator.
398 
399  As the size of each advertisement is limited, the update_method should
400  return a PassiveBluetoothDataUpdate object that contains only data that
401  should be updated. The coordinator will then dispatch subscribers based
402  on the data in the PassiveBluetoothDataUpdate object. The accumulated data
403  is available in the devices, entity_data, and entity_descriptions attributes.
404  """
405 
406  coordinator: PassiveBluetoothProcessorCoordinator[_DataT]
407  data: PassiveBluetoothDataUpdate[_T]
408  entity_names: dict[PassiveBluetoothEntityKey, str | None]
409  entity_data: dict[PassiveBluetoothEntityKey, _T]
410  entity_descriptions: dict[PassiveBluetoothEntityKey, EntityDescription]
411  devices: dict[str | None, DeviceInfo]
412  restore_key: str | None
413 
414  def __init__(
415  self,
416  update_method: Callable[[_DataT], PassiveBluetoothDataUpdate[_T]],
417  restore_key: str | None = None,
418  ) -> None:
419  """Initialize the coordinator."""
420  try:
421  self.restore_keyrestore_key = restore_key or async_get_current_platform().domain
422  except RuntimeError:
423  self.restore_keyrestore_key = None
424  self._listeners: list[
425  Callable[[PassiveBluetoothDataUpdate[_T] | None], None]
426  ] = []
427  self._entity_key_listeners: dict[
428  PassiveBluetoothEntityKey,
429  list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]],
430  ] = {}
431  self.update_methodupdate_method = update_method
432  self.last_update_successlast_update_success = True
433 
434  @callback
436  self,
437  coordinator: PassiveBluetoothProcessorCoordinator[_DataT],
438  entity_description_class: type[EntityDescription] | None,
439  ) -> None:
440  """Register a coordinator."""
441  self.coordinatorcoordinator = coordinator
443  data = self.datadata
444  # These attributes to access the data in
445  # self.data are for backwards compatibility.
446  self.entity_namesentity_names = data.entity_names
447  self.entity_dataentity_data = data.entity_data
448  self.entity_descriptionsentity_descriptions = data.entity_descriptions
449  self.devicesdevices = data.devices
450  if (
451  entity_description_class
452  and (restore_key := self.restore_keyrestore_key)
453  and (restore_data := coordinator.restore_data)
454  and (restored_processor_data := restore_data.get(restore_key))
455  ):
456  data.async_set_restore_data(
457  restored_processor_data,
458  entity_description_class,
459  )
460  self.async_update_listenersasync_update_listeners(data)
461 
462  @property
463  def available(self) -> bool:
464  """Return if the device is available."""
465  return self.coordinatorcoordinator.available and self.last_update_successlast_update_success
466 
467  @callback
468  def async_handle_unavailable(self) -> None:
469  """Handle the device going unavailable."""
470  self.async_update_listenersasync_update_listeners(None)
471 
472  @callback
474  self,
475  entity_class: type[PassiveBluetoothProcessorEntity[Self]],
476  async_add_entities: AddEntitiesCallback,
477  ) -> Callable[[], None]:
478  """Add a listener for new entities."""
479  created: set[PassiveBluetoothEntityKey] = set()
480 
481  @callback
482  def _async_add_or_update_entities(
483  data: PassiveBluetoothDataUpdate[_T] | None,
484  ) -> None:
485  """Listen for new entities."""
486  if data is None or created.issuperset(data.entity_descriptions):
487  return
488  entities: list[PassiveBluetoothProcessorEntity[Self]] = []
489  for entity_key, description in data.entity_descriptions.items():
490  if entity_key not in created:
491  entities.append(entity_class(self, entity_key, description))
492  created.add(entity_key)
493  if entities:
494  async_add_entities(entities)
495 
496  return self.async_add_listenerasync_add_listener(_async_add_or_update_entities)
497 
498  @callback
500  self,
501  update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None],
502  ) -> Callable[[], None]:
503  """Listen for all updates."""
504 
505  @callback
506  def remove_listener() -> None:
507  """Remove update listener."""
508  self._listeners.remove(update_callback)
509 
510  self._listeners.append(update_callback)
511  return remove_listener
512 
513  @callback
515  self,
516  update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None],
517  entity_key: PassiveBluetoothEntityKey,
518  ) -> Callable[[], None]:
519  """Listen for updates by device key."""
520 
521  @callback
522  def remove_listener() -> None:
523  """Remove update listener."""
524  self._entity_key_listeners[entity_key].remove(update_callback)
525  if not self._entity_key_listeners[entity_key]:
526  del self._entity_key_listeners[entity_key]
527 
528  self._entity_key_listeners.setdefault(entity_key, []).append(update_callback)
529  return remove_listener
530 
531  @callback
533  self,
534  data: PassiveBluetoothDataUpdate[_T] | None,
535  was_available: bool | None = None,
536  changed_entity_keys: set[PassiveBluetoothEntityKey] | None = None,
537  ) -> None:
538  """Update all registered listeners."""
539  if was_available is None:
540  was_available = self.coordinatorcoordinator.available
541 
542  # Dispatch to listeners without a filter key
543  for update_callback in self._listeners:
544  update_callback(data)
545 
546  if not was_available or data is None:
547  # When data is None, or was_available is False,
548  # dispatch to all listeners as it means the device
549  # is flipping between available and unavailable
550  for listeners in self._entity_key_listeners.values():
551  for update_callback in listeners:
552  update_callback(data)
553  return
554 
555  # Dispatch to listeners with a filter key
556  # if the key is in the data
557  entity_key_listeners = self._entity_key_listeners
558  for entity_key in data.entity_data:
559  if (
560  was_available
561  and changed_entity_keys is not None
562  and entity_key not in changed_entity_keys
563  ):
564  continue
565  if maybe_listener := entity_key_listeners.get(entity_key):
566  for update_callback in maybe_listener:
567  update_callback(data)
568 
569  @callback
571  self, update: _DataT, was_available: bool | None = None
572  ) -> None:
573  """Handle a Bluetooth event."""
574  try:
575  new_data = self.update_methodupdate_method(update)
576  except Exception:
577  self.last_update_successlast_update_success = False
578  self.coordinatorcoordinator.logger.exception(
579  "Unexpected error updating %s data", self.coordinatorcoordinator.name
580  )
581  return
582 
583  if not isinstance(new_data, PassiveBluetoothDataUpdate):
584  self.last_update_successlast_update_success = False # type: ignore[unreachable]
585  raise TypeError(
586  f"The update_method for {self.coordinator.name} returned"
587  f" {new_data} instead of a PassiveBluetoothDataUpdate"
588  )
589 
590  if not self.last_update_successlast_update_success:
591  self.last_update_successlast_update_success = True
592  self.coordinatorcoordinator.logger.info(
593  "Processing %s data recovered", self.coordinatorcoordinator.name
594  )
595 
596  changed_entity_keys = self.datadata.update(new_data)
597  self.async_update_listenersasync_update_listeners(new_data, was_available, changed_entity_keys)
598 
599 
600 # pylint: disable-next=hass-enforce-class-module
602  _PassiveBluetoothDataProcessorT: PassiveBluetoothDataProcessor[Any, Any]
603 ](Entity):
604  """A class for entities using PassiveBluetoothDataProcessor."""
605 
606  _attr_has_entity_name = True
607  _attr_should_poll = False
608 
609  def __init__(
610  self,
611  processor: _PassiveBluetoothDataProcessorT,
612  entity_key: PassiveBluetoothEntityKey,
613  description: EntityDescription,
614  context: Any = None,
615  ) -> None:
616  """Create the entity with a PassiveBluetoothDataProcessor."""
617  self.entity_description = description
618  self.entity_key = entity_key
619  self.processor = processor
620  self.processor_context = context
621  address = processor.coordinator.address
622  device_id = entity_key.device_id
623  devices = processor.devices
624  key = entity_key.key
625  if device_id in devices:
626  base_device_info = devices[device_id]
627  else:
628  base_device_info = DeviceInfo({})
629  if device_id:
630  self._attr_device_info = base_device_info | DeviceInfo(
631  {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}}
632  )
633  self._attr_unique_id = f"{address}-{key}-{device_id}"
634  else:
635  self._attr_device_info = base_device_info | DeviceInfo(
636  {ATTR_IDENTIFIERS: {(DOMAIN, address)}}
637  )
638  self._attr_unique_id = f"{address}-{key}"
639  if ATTR_NAME not in self._attr_device_info:
640  self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
641  if device_id is None:
642  self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_BLUETOOTH, address)}
643  if (name := processor.entity_names.get(entity_key)) is not None:
644  self._attr_name = name
645 
646  @property
647  def available(self) -> bool:
648  """Return if entity is available."""
649  return self.processor.available
650 
651  async def async_added_to_hass(self) -> None:
652  """When entity is added to hass."""
653  await super().async_added_to_hass()
654  self.async_on_remove(
655  self.processor.async_add_entity_key_listener(
656  self._handle_processor_update, self.entity_key
657  )
658  )
659 
660  @callback
662  self,
663  new_data: PassiveBluetoothDataUpdate[_PassiveBluetoothDataProcessorT] | None,
664  ) -> None:
665  """Handle updated data from the processor."""
666  self.async_write_ha_state()
Callable[[], None] async_add_entity_key_listener(self, Callable[[PassiveBluetoothDataUpdate[_T]|None], None] update_callback, PassiveBluetoothEntityKey entity_key)
None __init__(self, Callable[[_DataT], PassiveBluetoothDataUpdate[_T]] update_method, str|None restore_key=None)
None async_update_listeners(self, PassiveBluetoothDataUpdate[_T]|None data, bool|None was_available=None, set[PassiveBluetoothEntityKey]|None changed_entity_keys=None)
Callable[[], None] async_add_listener(self, Callable[[PassiveBluetoothDataUpdate[_T]|None], None] update_callback)
Callable[[], None] async_add_entities_listener(self, type[PassiveBluetoothProcessorEntity[Self]] entity_class, AddEntitiesCallback async_add_entities)
None async_register_coordinator(self, PassiveBluetoothProcessorCoordinator[_DataT] coordinator, type[EntityDescription]|None entity_description_class)
None async_set_restore_data(self, RestoredPassiveBluetoothDataUpdate restore_data, type[EntityDescription] entity_description_class)
set[PassiveBluetoothEntityKey]|None update(self, PassiveBluetoothDataUpdate[_T] new_data)
None __init__(self, HomeAssistant hass, logging.Logger logger, str address, BluetoothScanningMode mode, Callable[[BluetoothServiceInfoBleak], _DataT] update_method, bool connectable=False)
None _async_handle_bluetooth_event(self, BluetoothServiceInfoBleak service_info, BluetoothChange change)
Callable[[], None] async_register_processor(self, PassiveBluetoothDataProcessor[Any, _DataT] processor, type[EntityDescription]|None entity_description_class=None)
bool remove(self, _T matcher)
Definition: match.py:214
EntityDescription deserialize_entity_description(type[EntityDescription] descriptions_class, dict[str, Any] data)
dict[str, Any] serialize_entity_description(EntityDescription description)
None _handle_processor_update(self, PassiveBluetoothDataUpdate[_PassiveBluetoothDataProcessorT]|None new_data)
None __init__(self, _PassiveBluetoothDataProcessorT processor, PassiveBluetoothEntityKey entity_key, EntityDescription description, Any context=None)
CALLBACK_TYPE async_register_coordinator_for_restore(HomeAssistant hass, PassiveBluetoothProcessorCoordinator[Any] coordinator)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679