2019-05-30 16:48:58 +00:00
|
|
|
"""Runtime entry data for ESPHome stored in hass.data."""
|
2021-03-17 22:49:01 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-05-30 16:48:58 +00:00
|
|
|
import asyncio
|
2023-06-26 02:31:31 +00:00
|
|
|
from collections.abc import Callable, Coroutine, Iterable
|
2021-06-29 17:53:57 +00:00
|
|
|
from dataclasses import dataclass, field
|
2022-06-30 17:05:29 +00:00
|
|
|
import logging
|
2023-06-26 02:31:31 +00:00
|
|
|
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
|
2019-05-30 16:48:58 +00:00
|
|
|
|
|
|
|
from aioesphomeapi import (
|
2019-07-31 19:25:30 +00:00
|
|
|
COMPONENT_TYPE_TO_INFO,
|
2023-06-20 00:19:17 +00:00
|
|
|
AlarmControlPanelInfo,
|
2021-07-12 20:56:10 +00:00
|
|
|
APIClient,
|
2021-06-28 11:43:45 +00:00
|
|
|
APIVersion,
|
2019-06-18 15:41:45 +00:00
|
|
|
BinarySensorInfo,
|
2019-07-31 19:25:30 +00:00
|
|
|
CameraInfo,
|
2023-07-05 12:17:28 +00:00
|
|
|
CameraState,
|
2019-07-31 19:25:30 +00:00
|
|
|
ClimateInfo,
|
|
|
|
CoverInfo,
|
2019-11-26 02:00:58 +00:00
|
|
|
DeviceInfo,
|
|
|
|
EntityInfo,
|
|
|
|
EntityState,
|
2019-07-31 19:25:30 +00:00
|
|
|
FanInfo,
|
|
|
|
LightInfo,
|
2022-02-14 17:31:46 +00:00
|
|
|
LockInfo,
|
2022-05-18 16:46:13 +00:00
|
|
|
MediaPlayerInfo,
|
2021-06-29 12:33:04 +00:00
|
|
|
NumberInfo,
|
2021-07-27 09:45:04 +00:00
|
|
|
SelectInfo,
|
2019-07-31 19:25:30 +00:00
|
|
|
SensorInfo,
|
2023-05-05 02:21:42 +00:00
|
|
|
SensorState,
|
2019-07-31 19:25:30 +00:00
|
|
|
SwitchInfo,
|
|
|
|
TextSensorInfo,
|
2019-11-26 02:00:58 +00:00
|
|
|
UserService,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2021-11-29 08:57:37 +00:00
|
|
|
from aioesphomeapi.model import ButtonInfo
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2019-06-18 15:41:45 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2022-07-01 05:19:40 +00:00
|
|
|
from homeassistant.const import Platform
|
2023-06-22 07:39:48 +00:00
|
|
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
2019-05-30 16:48:58 +00:00
|
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
|
|
from homeassistant.helpers.storage import Store
|
|
|
|
|
2023-01-11 21:26:13 +00:00
|
|
|
from .dashboard import async_get_dashboard
|
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
INFO_TO_COMPONENT_TYPE: Final = {v: k for k, v in COMPONENT_TYPE_TO_INFO.items()}
|
|
|
|
|
2023-03-20 19:04:46 +00:00
|
|
|
_SENTINEL = object()
|
2020-10-30 08:02:00 +00:00
|
|
|
SAVE_DELAY = 120
|
2022-06-30 17:05:29 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2020-10-30 08:02:00 +00:00
|
|
|
|
2019-06-18 15:41:45 +00:00
|
|
|
# Mapping from ESPHome info type to HA platform
|
2023-01-12 21:55:18 +00:00
|
|
|
INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
|
2023-06-20 00:19:17 +00:00
|
|
|
AlarmControlPanelInfo: Platform.ALARM_CONTROL_PANEL,
|
2022-07-01 05:19:40 +00:00
|
|
|
BinarySensorInfo: Platform.BINARY_SENSOR,
|
2022-07-03 20:48:34 +00:00
|
|
|
ButtonInfo: Platform.BUTTON,
|
|
|
|
CameraInfo: Platform.CAMERA,
|
2022-07-01 05:19:40 +00:00
|
|
|
ClimateInfo: Platform.CLIMATE,
|
|
|
|
CoverInfo: Platform.COVER,
|
|
|
|
FanInfo: Platform.FAN,
|
|
|
|
LightInfo: Platform.LIGHT,
|
|
|
|
LockInfo: Platform.LOCK,
|
|
|
|
MediaPlayerInfo: Platform.MEDIA_PLAYER,
|
|
|
|
NumberInfo: Platform.NUMBER,
|
|
|
|
SelectInfo: Platform.SELECT,
|
|
|
|
SensorInfo: Platform.SENSOR,
|
|
|
|
SwitchInfo: Platform.SWITCH,
|
|
|
|
TextSensorInfo: Platform.SENSOR,
|
|
|
|
}
|
|
|
|
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
class StoreData(TypedDict, total=False):
|
|
|
|
"""ESPHome storage data."""
|
|
|
|
|
|
|
|
device_info: dict[str, Any]
|
|
|
|
services: list[dict[str, Any]]
|
|
|
|
api_version: dict[str, Any]
|
|
|
|
|
|
|
|
|
|
|
|
class ESPHomeStorage(Store[StoreData]):
|
|
|
|
"""ESPHome Storage."""
|
|
|
|
|
|
|
|
|
2021-06-29 17:53:57 +00:00
|
|
|
@dataclass
|
2019-05-30 16:48:58 +00:00
|
|
|
class RuntimeEntryData:
|
|
|
|
"""Store runtime data for esphome config entries."""
|
|
|
|
|
2021-06-29 17:53:57 +00:00
|
|
|
entry_id: str
|
|
|
|
client: APIClient
|
2023-06-26 02:31:31 +00:00
|
|
|
store: ESPHomeStorage
|
2022-07-03 20:48:34 +00:00
|
|
|
state: dict[type[EntityState], dict[int, EntityState]] = field(default_factory=dict)
|
2023-04-06 20:32:02 +00:00
|
|
|
# When the disconnect callback is called, we mark all states
|
|
|
|
# as stale so we will always dispatch a state update when the
|
|
|
|
# device reconnects. This is the same format as state_subscriptions.
|
|
|
|
stale_state: set[tuple[type[EntityState], int]] = field(default_factory=set)
|
2023-06-26 02:31:31 +00:00
|
|
|
info: dict[type[EntityInfo], dict[int, EntityInfo]] = field(default_factory=dict)
|
2021-06-29 17:53:57 +00:00
|
|
|
services: dict[int, UserService] = field(default_factory=dict)
|
|
|
|
available: bool = False
|
2023-06-26 12:21:45 +00:00
|
|
|
expected_disconnect: bool = False # Last disconnect was expected (e.g. deep sleep)
|
2021-06-29 17:53:57 +00:00
|
|
|
device_info: DeviceInfo | None = None
|
|
|
|
api_version: APIVersion = field(default_factory=APIVersion)
|
|
|
|
cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list)
|
|
|
|
disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list)
|
2022-07-03 20:48:34 +00:00
|
|
|
state_subscriptions: dict[
|
|
|
|
tuple[type[EntityState], int], Callable[[], None]
|
|
|
|
] = field(default_factory=dict)
|
2023-01-12 21:55:18 +00:00
|
|
|
loaded_platforms: set[Platform] = field(default_factory=set)
|
2021-06-29 17:53:57 +00:00
|
|
|
platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
2023-06-26 02:31:31 +00:00
|
|
|
_storage_contents: StoreData | None = None
|
|
|
|
_pending_storage: Callable[[], StoreData] | None = None
|
2022-09-28 18:06:30 +00:00
|
|
|
ble_connections_free: int = 0
|
|
|
|
ble_connections_limit: int = 0
|
2022-09-29 12:42:55 +00:00
|
|
|
_ble_connection_free_futures: list[asyncio.Future[int]] = field(
|
|
|
|
default_factory=list
|
|
|
|
)
|
2023-04-17 23:52:37 +00:00
|
|
|
assist_pipeline_update_callbacks: list[Callable[[], None]] = field(
|
|
|
|
default_factory=list
|
|
|
|
)
|
|
|
|
assist_pipeline_state: bool = False
|
2023-06-22 07:39:48 +00:00
|
|
|
entity_info_callbacks: dict[
|
|
|
|
type[EntityInfo], list[Callable[[list[EntityInfo]], None]]
|
|
|
|
] = field(default_factory=dict)
|
2023-06-26 02:31:31 +00:00
|
|
|
entity_info_key_remove_callbacks: dict[
|
|
|
|
tuple[type[EntityInfo], int], list[Callable[[], Coroutine[Any, Any, None]]]
|
|
|
|
] = field(default_factory=dict)
|
|
|
|
entity_info_key_updated_callbacks: dict[
|
|
|
|
tuple[type[EntityInfo], int], list[Callable[[EntityInfo], None]]
|
|
|
|
] = field(default_factory=dict)
|
2023-06-26 01:18:21 +00:00
|
|
|
original_options: dict[str, Any] = field(default_factory=dict)
|
2022-10-30 23:02:54 +00:00
|
|
|
|
2022-10-31 05:31:37 +00:00
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Return the name of the device."""
|
|
|
|
return self.device_info.name if self.device_info else self.entry_id
|
|
|
|
|
2023-01-16 07:33:44 +00:00
|
|
|
@property
|
|
|
|
def friendly_name(self) -> str:
|
|
|
|
"""Return the friendly name of the device."""
|
|
|
|
if self.device_info and self.device_info.friendly_name:
|
|
|
|
return self.device_info.friendly_name
|
|
|
|
return self.name
|
|
|
|
|
2023-02-07 21:15:54 +00:00
|
|
|
@property
|
|
|
|
def signal_device_updated(self) -> str:
|
|
|
|
"""Return the signal to listen to for core device state update."""
|
|
|
|
return f"esphome_{self.entry_id}_on_device_update"
|
|
|
|
|
2023-02-01 03:13:41 +00:00
|
|
|
@property
|
|
|
|
def signal_static_info_updated(self) -> str:
|
|
|
|
"""Return the signal to listen to for updates on static info."""
|
|
|
|
return f"esphome_{self.entry_id}_on_list"
|
|
|
|
|
2023-06-22 07:39:48 +00:00
|
|
|
@callback
|
|
|
|
def async_register_static_info_callback(
|
|
|
|
self,
|
|
|
|
entity_info_type: type[EntityInfo],
|
|
|
|
callback_: Callable[[list[EntityInfo]], None],
|
|
|
|
) -> CALLBACK_TYPE:
|
|
|
|
"""Register to receive callbacks when static info changes for an EntityInfo type."""
|
|
|
|
callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
|
|
|
|
callbacks.append(callback_)
|
|
|
|
|
|
|
|
def _unsub() -> None:
|
|
|
|
callbacks.remove(callback_)
|
|
|
|
|
|
|
|
return _unsub
|
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
@callback
|
|
|
|
def async_register_key_static_info_remove_callback(
|
|
|
|
self,
|
|
|
|
static_info: EntityInfo,
|
|
|
|
callback_: Callable[[], Coroutine[Any, Any, None]],
|
|
|
|
) -> CALLBACK_TYPE:
|
|
|
|
"""Register to receive callbacks when static info is removed for a specific key."""
|
|
|
|
callback_key = (type(static_info), static_info.key)
|
|
|
|
callbacks = self.entity_info_key_remove_callbacks.setdefault(callback_key, [])
|
|
|
|
callbacks.append(callback_)
|
|
|
|
|
|
|
|
def _unsub() -> None:
|
|
|
|
callbacks.remove(callback_)
|
|
|
|
|
|
|
|
return _unsub
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_register_key_static_info_updated_callback(
|
|
|
|
self,
|
|
|
|
static_info: EntityInfo,
|
|
|
|
callback_: Callable[[EntityInfo], None],
|
|
|
|
) -> CALLBACK_TYPE:
|
|
|
|
"""Register to receive callbacks when static info is updated for a specific key."""
|
|
|
|
callback_key = (type(static_info), static_info.key)
|
|
|
|
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
|
|
|
|
callbacks.append(callback_)
|
|
|
|
|
|
|
|
def _unsub() -> None:
|
|
|
|
callbacks.remove(callback_)
|
|
|
|
|
|
|
|
return _unsub
|
|
|
|
|
2022-09-28 18:06:30 +00:00
|
|
|
@callback
|
|
|
|
def async_update_ble_connection_limits(self, free: int, limit: int) -> None:
|
|
|
|
"""Update the BLE connection limits."""
|
2022-10-31 05:31:37 +00:00
|
|
|
_LOGGER.debug(
|
2022-12-13 22:57:29 +00:00
|
|
|
"%s [%s]: BLE connection limits: used=%s free=%s limit=%s",
|
2022-10-31 05:31:37 +00:00
|
|
|
self.name,
|
2022-12-13 22:57:29 +00:00
|
|
|
self.device_info.mac_address if self.device_info else "unknown",
|
2022-10-31 05:31:37 +00:00
|
|
|
limit - free,
|
|
|
|
free,
|
|
|
|
limit,
|
|
|
|
)
|
2022-09-28 18:06:30 +00:00
|
|
|
self.ble_connections_free = free
|
|
|
|
self.ble_connections_limit = limit
|
Handle cancelation of wait_for_ble_connections_free in esphome bluetooth (#90014)
Handle cancelation in wait_for_ble_connections_free
If `wait_for_ble_connections_free` was canceled due to timeout or
the esp disconnecting from Home Assistant the future would get
canceled. When we reconnect and get the next callback we need
to handle it being done.
fixes
```
2023-03-21 02:34:36.876 ERROR (MainThread) [homeassistant] Error doing job: Fatal error: protocol.data_received() call failed.
Traceback (most recent call last):
File "/usr/local/lib/python3.10/asyncio/selector_events.py", line 868, in _read_ready__data_received
self._protocol.data_received(data)
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/_frame_helper.py", line 195, in data_received
self._callback_packet(msg_type_int, bytes(packet_data))
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/_frame_helper.py", line 110, in _callback_packet
self._on_pkt(Packet(type_, data))
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/connection.py", line 688, in _process_packet
handler(msg)
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/client.py", line 482, in on_msg
on_bluetooth_connections_free_update(resp.free, resp.limit)
File "/usr/src/homeassistant/homeassistant/components/esphome/entry_data.py", line 136, in async_update_ble_connection_limits
fut.set_result(free)
asyncio.exceptions.InvalidStateError: invalid state
```
2023-03-21 03:49:59 +00:00
|
|
|
if not free:
|
|
|
|
return
|
|
|
|
for fut in self._ble_connection_free_futures:
|
|
|
|
# If wait_for_ble_connections_free gets cancelled, it will
|
|
|
|
# leave a future in the list. We need to check if it's done
|
|
|
|
# before setting the result.
|
|
|
|
if not fut.done():
|
2022-09-29 12:42:55 +00:00
|
|
|
fut.set_result(free)
|
Handle cancelation of wait_for_ble_connections_free in esphome bluetooth (#90014)
Handle cancelation in wait_for_ble_connections_free
If `wait_for_ble_connections_free` was canceled due to timeout or
the esp disconnecting from Home Assistant the future would get
canceled. When we reconnect and get the next callback we need
to handle it being done.
fixes
```
2023-03-21 02:34:36.876 ERROR (MainThread) [homeassistant] Error doing job: Fatal error: protocol.data_received() call failed.
Traceback (most recent call last):
File "/usr/local/lib/python3.10/asyncio/selector_events.py", line 868, in _read_ready__data_received
self._protocol.data_received(data)
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/_frame_helper.py", line 195, in data_received
self._callback_packet(msg_type_int, bytes(packet_data))
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/_frame_helper.py", line 110, in _callback_packet
self._on_pkt(Packet(type_, data))
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/connection.py", line 688, in _process_packet
handler(msg)
File "/usr/local/lib/python3.10/site-packages/aioesphomeapi/client.py", line 482, in on_msg
on_bluetooth_connections_free_update(resp.free, resp.limit)
File "/usr/src/homeassistant/homeassistant/components/esphome/entry_data.py", line 136, in async_update_ble_connection_limits
fut.set_result(free)
asyncio.exceptions.InvalidStateError: invalid state
```
2023-03-21 03:49:59 +00:00
|
|
|
self._ble_connection_free_futures.clear()
|
2022-09-29 12:42:55 +00:00
|
|
|
|
|
|
|
async def wait_for_ble_connections_free(self) -> int:
|
|
|
|
"""Wait until there are free BLE connections."""
|
|
|
|
if self.ble_connections_free > 0:
|
|
|
|
return self.ble_connections_free
|
|
|
|
fut: asyncio.Future[int] = asyncio.Future()
|
|
|
|
self._ble_connection_free_futures.append(fut)
|
|
|
|
return await fut
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2023-04-17 23:52:37 +00:00
|
|
|
@callback
|
|
|
|
def async_set_assist_pipeline_state(self, state: bool) -> None:
|
|
|
|
"""Set the assist pipeline state."""
|
|
|
|
self.assist_pipeline_state = state
|
|
|
|
for update_callback in self.assist_pipeline_update_callbacks:
|
|
|
|
update_callback()
|
|
|
|
|
|
|
|
def async_subscribe_assist_pipeline_update(
|
|
|
|
self, update_callback: Callable[[], None]
|
|
|
|
) -> Callable[[], None]:
|
|
|
|
"""Subscribe to assist pipeline updates."""
|
|
|
|
|
|
|
|
def _unsubscribe() -> None:
|
|
|
|
self.assist_pipeline_update_callbacks.remove(update_callback)
|
|
|
|
|
|
|
|
self.assist_pipeline_update_callbacks.append(update_callback)
|
|
|
|
return _unsubscribe
|
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
async def async_remove_entities(self, static_infos: Iterable[EntityInfo]) -> None:
|
2019-05-30 16:48:58 +00:00
|
|
|
"""Schedule the removal of an entity."""
|
2023-06-26 02:31:31 +00:00
|
|
|
callbacks: list[Coroutine[Any, Any, None]] = []
|
|
|
|
for static_info in static_infos:
|
|
|
|
callback_key = (type(static_info), static_info.key)
|
|
|
|
if key_callbacks := self.entity_info_key_remove_callbacks.get(callback_key):
|
|
|
|
callbacks.extend([callback_() for callback_ in key_callbacks])
|
|
|
|
if callbacks:
|
|
|
|
await asyncio.gather(*callbacks)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_update_entity_infos(self, static_infos: Iterable[EntityInfo]) -> None:
|
|
|
|
"""Call static info updated callbacks."""
|
|
|
|
for static_info in static_infos:
|
|
|
|
callback_key = (type(static_info), static_info.key)
|
|
|
|
for callback_ in self.entity_info_key_updated_callbacks.get(
|
|
|
|
callback_key, []
|
|
|
|
):
|
|
|
|
callback_(static_info)
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def _ensure_platforms_loaded(
|
2023-01-12 21:55:18 +00:00
|
|
|
self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform]
|
2021-07-12 20:56:10 +00:00
|
|
|
) -> None:
|
2019-06-18 15:41:45 +00:00
|
|
|
async with self.platform_load_lock:
|
|
|
|
needed = platforms - self.loaded_platforms
|
2022-07-09 15:27:42 +00:00
|
|
|
if needed:
|
|
|
|
await hass.config_entries.async_forward_entry_setups(entry, needed)
|
2019-06-18 15:41:45 +00:00
|
|
|
self.loaded_platforms |= needed
|
|
|
|
|
|
|
|
async def async_update_static_infos(
|
2021-04-21 10:18:42 +00:00
|
|
|
self, hass: HomeAssistant, entry: ConfigEntry, infos: list[EntityInfo]
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> None:
|
2019-05-30 16:48:58 +00:00
|
|
|
"""Distribute an update of static infos to all platforms."""
|
2019-06-18 15:41:45 +00:00
|
|
|
# First, load all platforms
|
|
|
|
needed_platforms = set()
|
2023-01-11 21:26:13 +00:00
|
|
|
|
|
|
|
if async_get_dashboard(hass):
|
2023-01-12 21:55:18 +00:00
|
|
|
needed_platforms.add(Platform.UPDATE)
|
2023-01-11 21:26:13 +00:00
|
|
|
|
2023-04-17 23:52:37 +00:00
|
|
|
if self.device_info is not None and self.device_info.voice_assistant_version:
|
|
|
|
needed_platforms.add(Platform.BINARY_SENSOR)
|
2023-04-18 02:22:11 +00:00
|
|
|
needed_platforms.add(Platform.SELECT)
|
2023-04-17 23:52:37 +00:00
|
|
|
|
2019-06-18 15:41:45 +00:00
|
|
|
for info in infos:
|
|
|
|
for info_type, platform in INFO_TYPE_TO_PLATFORM.items():
|
|
|
|
if isinstance(info, info_type):
|
|
|
|
needed_platforms.add(platform)
|
|
|
|
break
|
|
|
|
await self._ensure_platforms_loaded(hass, entry, needed_platforms)
|
|
|
|
|
2023-06-22 07:39:48 +00:00
|
|
|
# Make a dict of the EntityInfo by type and send
|
|
|
|
# them to the listeners for each specific EntityInfo type
|
|
|
|
infos_by_type: dict[type[EntityInfo], list[EntityInfo]] = {}
|
|
|
|
for info in infos:
|
|
|
|
info_type = type(info)
|
|
|
|
if info_type not in infos_by_type:
|
|
|
|
infos_by_type[info_type] = []
|
|
|
|
infos_by_type[info_type].append(info)
|
|
|
|
|
|
|
|
callbacks_by_type = self.entity_info_callbacks
|
|
|
|
for type_, entity_infos in infos_by_type.items():
|
|
|
|
if callbacks_ := callbacks_by_type.get(type_):
|
|
|
|
for callback_ in callbacks_:
|
|
|
|
callback_(entity_infos)
|
|
|
|
|
2019-06-18 15:41:45 +00:00
|
|
|
# Then send dispatcher event
|
2023-02-01 03:13:41 +00:00
|
|
|
async_dispatcher_send(hass, self.signal_static_info_updated, infos)
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2020-01-29 21:59:45 +00:00
|
|
|
@callback
|
2022-07-01 05:19:40 +00:00
|
|
|
def async_subscribe_state_update(
|
|
|
|
self,
|
2022-07-03 20:48:34 +00:00
|
|
|
state_type: type[EntityState],
|
2022-07-01 05:19:40 +00:00
|
|
|
state_key: int,
|
|
|
|
entity_callback: Callable[[], None],
|
|
|
|
) -> Callable[[], None]:
|
|
|
|
"""Subscribe to state updates."""
|
|
|
|
|
|
|
|
def _unsubscribe() -> None:
|
2022-07-03 20:48:34 +00:00
|
|
|
self.state_subscriptions.pop((state_type, state_key))
|
2022-07-01 05:19:40 +00:00
|
|
|
|
2022-07-03 20:48:34 +00:00
|
|
|
self.state_subscriptions[(state_type, state_key)] = entity_callback
|
2022-07-01 05:19:40 +00:00
|
|
|
return _unsubscribe
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_update_state(self, state: EntityState) -> None:
|
2022-06-13 03:12:49 +00:00
|
|
|
"""Distribute an update of state information to the target."""
|
2023-03-20 19:04:46 +00:00
|
|
|
key = state.key
|
|
|
|
state_type = type(state)
|
2023-04-06 20:32:02 +00:00
|
|
|
stale_state = self.stale_state
|
2023-03-20 19:04:46 +00:00
|
|
|
current_state_by_type = self.state[state_type]
|
|
|
|
current_state = current_state_by_type.get(key, _SENTINEL)
|
2023-04-06 20:32:02 +00:00
|
|
|
subscription_key = (state_type, key)
|
2023-05-05 02:21:42 +00:00
|
|
|
if (
|
|
|
|
current_state == state
|
|
|
|
and subscription_key not in stale_state
|
2023-07-05 12:17:28 +00:00
|
|
|
and state_type is not CameraState
|
2023-05-05 02:21:42 +00:00
|
|
|
and not (
|
2023-07-05 12:17:28 +00:00
|
|
|
state_type is SensorState # pylint: disable=unidiomatic-typecheck
|
2023-06-26 02:31:31 +00:00
|
|
|
and (platform_info := self.info.get(SensorInfo))
|
2023-05-05 02:21:42 +00:00
|
|
|
and (entity_info := platform_info.get(state.key))
|
|
|
|
and (cast(SensorInfo, entity_info)).force_update
|
|
|
|
)
|
|
|
|
):
|
2023-03-20 19:04:46 +00:00
|
|
|
_LOGGER.debug(
|
2023-05-05 02:21:42 +00:00
|
|
|
"%s: ignoring duplicate update with key %s: %s",
|
2023-03-20 19:04:46 +00:00
|
|
|
self.name,
|
|
|
|
key,
|
|
|
|
state,
|
|
|
|
)
|
|
|
|
return
|
2022-06-30 17:05:29 +00:00
|
|
|
_LOGGER.debug(
|
2022-10-31 05:31:37 +00:00
|
|
|
"%s: dispatching update with key %s: %s",
|
|
|
|
self.name,
|
2023-03-20 19:04:46 +00:00
|
|
|
key,
|
2022-06-30 17:05:29 +00:00
|
|
|
state,
|
|
|
|
)
|
2023-04-06 20:32:02 +00:00
|
|
|
stale_state.discard(subscription_key)
|
2023-03-20 19:04:46 +00:00
|
|
|
current_state_by_type[key] = state
|
2023-05-29 18:41:50 +00:00
|
|
|
if subscription := self.state_subscriptions.get(subscription_key):
|
|
|
|
try:
|
|
|
|
subscription()
|
|
|
|
except Exception as ex: # pylint: disable=broad-except
|
|
|
|
# If we allow this exception to raise it will
|
|
|
|
# make it all the way to data_received in aioesphomeapi
|
|
|
|
# which will cause the connection to be closed.
|
|
|
|
_LOGGER.exception("Error while calling subscription: %s", ex)
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2020-01-29 21:59:45 +00:00
|
|
|
@callback
|
2021-04-21 10:18:42 +00:00
|
|
|
def async_update_device_state(self, hass: HomeAssistant) -> None:
|
2019-05-30 16:48:58 +00:00
|
|
|
"""Distribute an update of a core device state like availability."""
|
2023-02-07 21:15:54 +00:00
|
|
|
async_dispatcher_send(hass, self.signal_device_updated)
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2021-03-17 22:49:01 +00:00
|
|
|
async def async_load_from_store(self) -> tuple[list[EntityInfo], list[UserService]]:
|
2019-05-30 16:48:58 +00:00
|
|
|
"""Load the retained data from store and return de-serialized data."""
|
2021-10-30 14:29:07 +00:00
|
|
|
if (restored := await self.store.async_load()) is None:
|
2019-05-30 16:48:58 +00:00
|
|
|
return [], []
|
2021-04-30 22:19:27 +00:00
|
|
|
self._storage_contents = restored.copy()
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2021-06-29 17:53:57 +00:00
|
|
|
self.device_info = DeviceInfo.from_dict(restored.pop("device_info"))
|
2021-06-29 20:50:29 +00:00
|
|
|
self.api_version = APIVersion.from_dict(restored.pop("api_version", {}))
|
2023-06-26 02:31:31 +00:00
|
|
|
infos: list[EntityInfo] = []
|
2019-05-30 16:48:58 +00:00
|
|
|
for comp_type, restored_infos in restored.items():
|
2023-06-26 02:31:31 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
restored_infos = cast(list[dict[str, Any]], restored_infos)
|
2019-05-30 16:48:58 +00:00
|
|
|
if comp_type not in COMPONENT_TYPE_TO_INFO:
|
|
|
|
continue
|
|
|
|
for info in restored_infos:
|
|
|
|
cls = COMPONENT_TYPE_TO_INFO[comp_type]
|
2021-06-29 17:53:57 +00:00
|
|
|
infos.append(cls.from_dict(info))
|
2023-06-26 02:31:31 +00:00
|
|
|
services = [
|
|
|
|
UserService.from_dict(service) for service in restored.pop("services", [])
|
|
|
|
]
|
2019-05-30 16:48:58 +00:00
|
|
|
return infos, services
|
|
|
|
|
|
|
|
async def async_save_to_store(self) -> None:
|
|
|
|
"""Generate dynamic data to store and save it to the filesystem."""
|
2021-07-12 20:56:10 +00:00
|
|
|
if self.device_info is None:
|
|
|
|
raise ValueError("device_info is not set yet")
|
2023-06-26 02:31:31 +00:00
|
|
|
store_data: StoreData = {
|
2021-06-29 17:53:57 +00:00
|
|
|
"device_info": self.device_info.to_dict(),
|
2021-06-28 11:43:45 +00:00
|
|
|
"services": [],
|
2021-06-29 17:53:57 +00:00
|
|
|
"api_version": self.api_version.to_dict(),
|
2021-06-28 11:43:45 +00:00
|
|
|
}
|
2023-06-26 02:31:31 +00:00
|
|
|
for info_type, infos in self.info.items():
|
|
|
|
comp_type = INFO_TO_COMPONENT_TYPE[info_type]
|
|
|
|
store_data[comp_type] = [info.to_dict() for info in infos.values()] # type: ignore[literal-required]
|
2019-05-30 16:48:58 +00:00
|
|
|
for service in self.services.values():
|
2019-07-31 19:25:30 +00:00
|
|
|
store_data["services"].append(service.to_dict())
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2021-04-30 22:19:27 +00:00
|
|
|
if store_data == self._storage_contents:
|
|
|
|
return
|
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
def _memorized_storage() -> StoreData:
|
|
|
|
self._pending_storage = None
|
2021-04-30 22:19:27 +00:00
|
|
|
self._storage_contents = store_data
|
|
|
|
return store_data
|
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
self._pending_storage = _memorized_storage
|
2021-04-30 22:19:27 +00:00
|
|
|
self.store.async_delay_save(_memorized_storage, SAVE_DELAY)
|
2023-06-26 01:18:21 +00:00
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
async def async_cleanup(self) -> None:
|
|
|
|
"""Cleanup the entry data when disconnected or unloading."""
|
|
|
|
if self._pending_storage:
|
|
|
|
# Ensure we save the data if we are unloading before the
|
|
|
|
# save delay has passed.
|
|
|
|
await self.store.async_save(self._pending_storage())
|
|
|
|
|
2023-06-26 01:18:21 +00:00
|
|
|
async def async_update_listener(
|
|
|
|
self, hass: HomeAssistant, entry: ConfigEntry
|
|
|
|
) -> None:
|
|
|
|
"""Handle options update."""
|
|
|
|
if self.original_options == entry.options:
|
|
|
|
return
|
|
|
|
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
|