1 """Runtime entry data for ESPHome stored in hass.data."""
3 from __future__
import annotations
6 from collections
import defaultdict
7 from collections.abc
import Callable, Iterable
8 from dataclasses
import dataclass, field
9 from functools
import partial
11 from typing
import TYPE_CHECKING, Any, Final, TypedDict, cast
13 from aioesphomeapi
import (
14 COMPONENT_TYPE_TO_INFO,
15 AlarmControlPanelInfo,
34 MediaPlayerSupportedFormat,
48 from aioesphomeapi.model
import ButtonInfo
49 from bleak_esphome.backend.device
import ESPHomeBluetoothDevice
58 from .const
import DOMAIN
59 from .dashboard
import async_get_dashboard
61 type ESPHomeConfigEntry = ConfigEntry[RuntimeEntryData]
64 INFO_TO_COMPONENT_TYPE: Final = {v: k
for k, v
in COMPONENT_TYPE_TO_INFO.items()}
68 _LOGGER = logging.getLogger(__name__)
71 INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
72 AlarmControlPanelInfo: Platform.ALARM_CONTROL_PANEL,
73 BinarySensorInfo: Platform.BINARY_SENSOR,
74 ButtonInfo: Platform.BUTTON,
75 CameraInfo: Platform.CAMERA,
76 ClimateInfo: Platform.CLIMATE,
77 CoverInfo: Platform.COVER,
78 DateInfo: Platform.DATE,
79 DateTimeInfo: Platform.DATETIME,
80 EventInfo: Platform.EVENT,
81 FanInfo: Platform.FAN,
82 LightInfo: Platform.LIGHT,
83 LockInfo: Platform.LOCK,
84 MediaPlayerInfo: Platform.MEDIA_PLAYER,
85 NumberInfo: Platform.NUMBER,
86 SelectInfo: Platform.SELECT,
87 SensorInfo: Platform.SENSOR,
88 SwitchInfo: Platform.SWITCH,
89 TextInfo: Platform.TEXT,
90 TextSensorInfo: Platform.SENSOR,
91 TimeInfo: Platform.TIME,
92 UpdateInfo: Platform.UPDATE,
93 ValveInfo: Platform.VALVE,
98 """ESPHome storage data."""
100 device_info: dict[str, Any]
101 services: list[dict[str, Any]]
102 api_version: dict[str, Any]
106 """ESPHome Storage."""
109 @dataclass(slots=True)
111 """Store runtime data for esphome config entries."""
116 store: ESPHomeStorage
117 state: defaultdict[type[EntityState], dict[int, EntityState]] = field(
118 default_factory=
lambda: defaultdict(dict)
123 stale_state: set[tuple[type[EntityState], int]] = field(default_factory=set)
124 info: dict[type[EntityInfo], dict[int, EntityInfo]] = field(default_factory=dict)
125 services: dict[int, UserService] = field(default_factory=dict)
126 available: bool =
False
127 expected_disconnect: bool =
False
128 device_info: DeviceInfo |
None =
None
129 bluetooth_device: ESPHomeBluetoothDevice |
None =
None
130 api_version: APIVersion = field(default_factory=APIVersion)
131 cleanup_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
132 disconnect_callbacks: set[CALLBACK_TYPE] = field(default_factory=set)
133 state_subscriptions: dict[tuple[type[EntityState], int], CALLBACK_TYPE] = field(
136 device_update_subscriptions: set[CALLBACK_TYPE] = field(default_factory=set)
137 static_info_update_subscriptions: set[Callable[[list[EntityInfo]],
None]] = field(
140 loaded_platforms: set[Platform] = field(default_factory=set)
141 platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
142 _storage_contents: StoreData |
None =
None
143 _pending_storage: Callable[[], StoreData] |
None =
None
144 assist_pipeline_update_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
145 assist_pipeline_state: bool =
False
146 entity_info_callbacks: dict[
147 type[EntityInfo], list[Callable[[list[EntityInfo]],
None]]
148 ] = field(default_factory=dict)
149 entity_info_key_updated_callbacks: dict[
150 tuple[type[EntityInfo], int], list[Callable[[EntityInfo],
None]]
151 ] = field(default_factory=dict)
152 original_options: dict[str, Any] = field(default_factory=dict)
153 media_player_formats: dict[str, list[MediaPlayerSupportedFormat]] = field(
154 default_factory=
lambda: defaultdict(list)
156 assist_satellite_config_update_callbacks: list[
157 Callable[[AssistSatelliteConfiguration],
None]
158 ] = field(default_factory=list)
159 assist_satellite_set_wake_word_callbacks: list[Callable[[str],
None]] = field(
165 """Return the name of the device."""
167 return (device_info
and device_info.name)
or self.title
171 """Return the friendly name of the device."""
173 return (device_info
and device_info.friendly_name)
or self.
namename.title().replace(
180 entity_info_type: type[EntityInfo],
181 callback_: Callable[[list[EntityInfo]],
None],
183 """Register to receive callbacks when static info changes for an EntityInfo type."""
184 callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
185 callbacks.append(callback_)
193 callbacks: list[Callable[[list[EntityInfo]],
None]],
194 callback_: Callable[[list[EntityInfo]],
None],
196 """Unsubscribe to when static info is registered."""
197 callbacks.remove(callback_)
202 static_info: EntityInfo,
203 callback_: Callable[[EntityInfo],
None],
205 """Register to receive callbacks when static info is updated for a specific key."""
206 callback_key = (type(static_info), static_info.key)
207 callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
208 callbacks.append(callback_)
216 callbacks: list[Callable[[EntityInfo],
None]],
217 callback_: Callable[[EntityInfo],
None],
219 """Unsubscribe to when static info is updated ."""
220 callbacks.remove(callback_)
224 """Set the assist pipeline state."""
226 for update_callback
in self.assist_pipeline_update_callbacks:
231 self, update_callback: CALLBACK_TYPE
233 """Subscribe to assist pipeline updates."""
234 self.assist_pipeline_update_callbacks.append(update_callback)
239 self, update_callback: CALLBACK_TYPE
241 """Unsubscribe to assist pipeline updates."""
242 self.assist_pipeline_update_callbacks.
remove(update_callback)
246 self, hass: HomeAssistant, static_infos: Iterable[EntityInfo], mac: str
248 """Schedule the removal of an entity."""
250 ent_reg = er.async_get(hass)
251 for info
in static_infos:
252 if entry := ent_reg.async_get_entity_id(
253 INFO_TYPE_TO_PLATFORM[type(info)], DOMAIN, build_unique_id(mac, info)
255 ent_reg.async_remove(entry)
259 """Call static info updated callbacks."""
260 callbacks = self.entity_info_key_updated_callbacks
261 for static_info
in static_infos:
262 for callback_
in callbacks.get((type(static_info), static_info.key), ()):
263 callback_(static_info)
268 entry: ESPHomeConfigEntry,
269 platforms: set[Platform],
271 async
with self.platform_load_lock:
272 if needed := platforms - self.loaded_platforms:
273 await hass.config_entries.async_forward_entry_setups(entry, needed)
274 self.loaded_platforms |= needed
279 entry: ESPHomeConfigEntry,
280 infos: list[EntityInfo],
283 """Distribute an update of static infos to all platforms."""
285 needed_platforms = set()
287 needed_platforms.add(Platform.UPDATE)
292 needed_platforms.add(Platform.BINARY_SENSOR)
293 needed_platforms.add(Platform.SELECT)
295 ent_reg = er.async_get(hass)
296 registry_get_entity = ent_reg.async_get_entity_id
298 platform = INFO_TYPE_TO_PLATFORM[type(info)]
299 needed_platforms.add(platform)
304 (old_unique_id := info.unique_id)
305 and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
306 and (new_unique_id := build_unique_id(mac, info)) != old_unique_id
307 and not registry_get_entity(platform, DOMAIN, new_unique_id)
309 ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
315 infos_by_type: dict[type[EntityInfo], list[EntityInfo]] = {}
317 info_type = type(info)
318 if info_type
not in infos_by_type:
319 infos_by_type[info_type] = []
320 infos_by_type[info_type].append(info)
322 callbacks_by_type = self.entity_info_callbacks
323 for type_, entity_infos
in infos_by_type.items():
324 if callbacks_ := callbacks_by_type.get(type_):
325 for callback_
in callbacks_:
326 callback_(entity_infos)
329 for callback_
in self.static_info_update_subscriptions:
334 """Subscribe to state updates."""
335 self.device_update_subscriptions.
add(callback_)
340 """Unsubscribe to device updates."""
341 self.device_update_subscriptions.
remove(callback_)
345 self, callback_: Callable[[list[EntityInfo]],
None]
347 """Subscribe to static info updates."""
348 self.static_info_update_subscriptions.
add(callback_)
353 self, callback_: Callable[[list[EntityInfo]],
None]
355 """Unsubscribe to static info updates."""
356 self.static_info_update_subscriptions.
remove(callback_)
361 state_type: type[EntityState],
363 entity_callback: CALLBACK_TYPE,
365 """Subscribe to state updates."""
366 subscription_key = (state_type, state_key)
367 self.state_subscriptions[subscription_key] = entity_callback
372 self, subscription_key: tuple[type[EntityState], int]
374 """Unsubscribe to state updates."""
375 self.state_subscriptions.pop(subscription_key)
379 """Distribute an update of state information to the target."""
381 state_type = type(state)
382 stale_state = self.stale_state
383 current_state_by_type = self.state[state_type]
384 current_state = current_state_by_type.get(key, _SENTINEL)
385 subscription_key = (state_type, key)
387 current_state == state
388 and subscription_key
not in stale_state
389 and state_type
not in (CameraState, Event)
391 state_type
is SensorState
392 and (platform_info := self.info.
get(SensorInfo))
393 and (entity_info := platform_info.get(state.key))
394 and (cast(SensorInfo, entity_info)).force_update
398 stale_state.discard(subscription_key)
399 current_state_by_type[key] = state
400 if subscription := self.state_subscriptions.
get(subscription_key):
407 _LOGGER.exception(
"Error while calling subscription")
411 """Distribute an update of a core device state like availability."""
412 for callback_
in self.device_update_subscriptions.copy():
416 """Load the retained data from store and return de-serialized data."""
417 if (restored := await self.store.
async_load())
is None:
421 self.
device_infodevice_info = DeviceInfo.from_dict(restored.pop(
"device_info"))
422 self.
api_versionapi_version = APIVersion.from_dict(restored.pop(
"api_version", {}))
423 infos: list[EntityInfo] = []
424 for comp_type, restored_infos
in restored.items():
426 restored_infos = cast(list[dict[str, Any]], restored_infos)
427 if comp_type
not in COMPONENT_TYPE_TO_INFO:
429 for info
in restored_infos:
430 cls = COMPONENT_TYPE_TO_INFO[comp_type]
431 infos.append(cls.from_dict(info))
433 UserService.from_dict(service)
for service
in restored.pop(
"services", [])
435 return infos, services
438 """Generate dynamic data to store and save it to the filesystem."""
441 store_data: StoreData = {
442 "device_info": self.
device_infodevice_info.to_dict(),
444 "api_version": self.
api_versionapi_version.to_dict(),
446 for info_type, infos
in self.info.items():
447 comp_type = INFO_TO_COMPONENT_TYPE[info_type]
448 store_data[comp_type] = [info.to_dict()
for info
in infos.values()]
450 store_data[
"services"] = [
451 service.to_dict()
for service
in self.services.values()
456 def _memorized_storage() -> StoreData:
465 """Cleanup the entry data when disconnected or unloading."""
472 self, hass: HomeAssistant, entry: ESPHomeConfigEntry
474 """Handle options update."""
477 hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
481 """Call when the entry has been disconnected.
483 Safe to call multiple times.
486 if self.bluetooth_device:
487 self.bluetooth_device.available =
False
499 self, device_info: DeviceInfo, api_version: APIVersion
501 """Call when the entry has been connected."""
503 if self.bluetooth_device:
504 self.bluetooth_device.available =
True
518 callback_: Callable[[AssistSatelliteConfiguration],
None],
520 """Register to receive callbacks when the Assist satellite's configuration is updated."""
521 self.assist_satellite_config_update_callbacks.append(callback_)
522 return lambda: self.assist_satellite_config_update_callbacks.
remove(callback_)
526 self, config: AssistSatelliteConfiguration
528 """Notify listeners that the Assist satellite configuration has been updated."""
529 for callback_
in self.assist_satellite_config_update_callbacks.copy():
535 callback_: Callable[[str],
None],
537 """Register to receive callbacks when the Assist satellite's wake word is set."""
538 self.assist_satellite_set_wake_word_callbacks.append(callback_)
539 return lambda: self.assist_satellite_set_wake_word_callbacks.
remove(callback_)
543 """Notify listeners that the Assist satellite wake word has been set."""
544 for callback_
in self.assist_satellite_set_wake_word_callbacks.copy():
545 callback_(wake_word_id)
None async_update_entity_infos(self, Iterable[EntityInfo] static_infos)
CALLBACK_TYPE async_subscribe_device_updated(self, CALLBACK_TYPE callback_)
None async_update_device_state(self)
None _async_unsubscribe_assist_pipeline_update(self, CALLBACK_TYPE update_callback)
None _async_unsubscribe_static_key_info_updated(self, list[Callable[[EntityInfo], None]] callbacks, Callable[[EntityInfo], None] callback_)
CALLBACK_TYPE async_register_assist_satellite_config_updated_callback(self, Callable[[AssistSatelliteConfiguration], None] callback_)
None async_on_connect(self, DeviceInfo device_info, APIVersion api_version)
CALLBACK_TYPE async_register_static_info_callback(self, type[EntityInfo] entity_info_type, Callable[[list[EntityInfo]], None] callback_)
None async_remove_entities(self, HomeAssistant hass, Iterable[EntityInfo] static_infos, str mac)
CALLBACK_TYPE async_subscribe_state_update(self, type[EntityState] state_type, int state_key, CALLBACK_TYPE entity_callback)
CALLBACK_TYPE async_subscribe_assist_pipeline_update(self, CALLBACK_TYPE update_callback)
None _async_unsubscribe_static_info_updated(self, Callable[[list[EntityInfo]], None] callback_)
None _ensure_platforms_loaded(self, HomeAssistant hass, ESPHomeConfigEntry entry, set[Platform] platforms)
CALLBACK_TYPE async_register_assist_satellite_set_wake_word_callback(self, Callable[[str], None] callback_)
None async_update_state(self, EntityState state)
tuple[list[EntityInfo], list[UserService]] async_load_from_store(self)
None async_set_assist_pipeline_state(self, bool state)
None _async_unsubscribe_state_update(self, tuple[type[EntityState], int] subscription_key)
None async_save_to_store(self)
None _async_unsubscribe_device_update(self, CALLBACK_TYPE callback_)
None async_on_disconnect(self)
None async_update_static_infos(self, HomeAssistant hass, ESPHomeConfigEntry entry, list[EntityInfo] infos, str mac)
None async_update_listener(self, HomeAssistant hass, ESPHomeConfigEntry entry)
None async_assist_satellite_config_updated(self, AssistSatelliteConfiguration config)
CALLBACK_TYPE async_subscribe_static_info_updated(self, Callable[[list[EntityInfo]], None] callback_)
None _async_unsubscribe_register_static_info(self, list[Callable[[list[EntityInfo]], None]] callbacks, Callable[[list[EntityInfo]], None] callback_)
CALLBACK_TYPE async_register_key_static_info_updated_callback(self, EntityInfo static_info, Callable[[EntityInfo], None] callback_)
None async_assist_satellite_set_wake_word(self, str wake_word_id)
bool add(self, _T matcher)
bool remove(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
ESPHomeDashboardCoordinator|None async_get_dashboard(HomeAssistant hass)
None async_load(HomeAssistant hass)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
None async_save(self, _T data)