2019-05-30 16:48:58 +00:00
|
|
|
"""Runtime entry data for ESPHome stored in hass.data."""
|
2024-03-08 13:15:26 +00:00
|
|
|
|
2021-03-17 22:49:01 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-05-30 16:48:58 +00:00
|
|
|
import asyncio
|
2024-03-26 20:32:16 +00:00
|
|
|
from collections.abc import Callable, Iterable
|
2021-06-29 17:53:57 +00:00
|
|
|
from dataclasses import dataclass, field
|
2024-01-07 17:39:33 +00:00
|
|
|
from functools import partial
|
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,
|
2024-03-13 07:02:52 +00:00
|
|
|
DateInfo,
|
2024-04-22 11:12:22 +00:00
|
|
|
DateTimeInfo,
|
2019-11-26 02:00:58 +00:00
|
|
|
DeviceInfo,
|
|
|
|
EntityInfo,
|
|
|
|
EntityState,
|
2024-04-24 01:22:03 +00:00
|
|
|
Event,
|
|
|
|
EventInfo,
|
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,
|
2023-10-25 04:14:58 +00:00
|
|
|
TextInfo,
|
2019-07-31 19:25:30 +00:00
|
|
|
TextSensorInfo,
|
2024-03-21 03:59:43 +00:00
|
|
|
TimeInfo,
|
2019-11-26 02:00:58 +00:00
|
|
|
UserService,
|
2024-04-18 01:47:15 +00:00
|
|
|
ValveInfo,
|
2023-10-16 03:05:20 +00:00
|
|
|
build_unique_id,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2021-11-29 08:57:37 +00:00
|
|
|
from aioesphomeapi.model import ButtonInfo
|
2023-12-14 17:21:31 +00:00
|
|
|
from bleak_esphome.backend.device import ESPHomeBluetoothDevice
|
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
|
2023-10-16 03:05:20 +00:00
|
|
|
from homeassistant.helpers import entity_registry as er
|
2019-05-30 16:48:58 +00:00
|
|
|
from homeassistant.helpers.storage import Store
|
|
|
|
|
2023-10-16 03:05:20 +00:00
|
|
|
from .const import DOMAIN
|
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,
|
2024-03-13 07:02:52 +00:00
|
|
|
DateInfo: Platform.DATE,
|
2024-04-22 11:12:22 +00:00
|
|
|
DateTimeInfo: Platform.DATETIME,
|
2024-04-24 01:22:03 +00:00
|
|
|
EventInfo: Platform.EVENT,
|
2022-07-01 05:19:40 +00:00
|
|
|
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,
|
2023-10-25 04:14:58 +00:00
|
|
|
TextInfo: Platform.TEXT,
|
2022-07-01 05:19:40 +00:00
|
|
|
TextSensorInfo: Platform.SENSOR,
|
2024-03-21 03:59:43 +00:00
|
|
|
TimeInfo: Platform.TIME,
|
2024-04-18 01:47:15 +00:00
|
|
|
ValveInfo: Platform.VALVE,
|
2022-07-01 05:19:40 +00:00
|
|
|
}
|
|
|
|
|
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."""
|
|
|
|
|
|
|
|
|
2023-07-21 20:41:50 +00:00
|
|
|
@dataclass(slots=True)
|
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
|
2023-07-23 08:45:48 +00:00
|
|
|
title: str
|
2021-06-29 17:53:57 +00:00
|
|
|
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
|
2023-07-21 20:41:50 +00:00
|
|
|
bluetooth_device: ESPHomeBluetoothDevice | None = None
|
2021-06-29 17:53:57 +00:00
|
|
|
api_version: APIVersion = field(default_factory=APIVersion)
|
2024-03-10 06:30:17 +00:00
|
|
|
cleanup_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
|
|
|
|
disconnect_callbacks: set[CALLBACK_TYPE] = field(default_factory=set)
|
|
|
|
state_subscriptions: dict[tuple[type[EntityState], int], CALLBACK_TYPE] = field(
|
|
|
|
default_factory=dict
|
|
|
|
)
|
|
|
|
device_update_subscriptions: set[CALLBACK_TYPE] = field(default_factory=set)
|
2024-04-24 14:14:44 +00:00
|
|
|
static_info_update_subscriptions: set[Callable[[list[EntityInfo]], None]] = field(
|
|
|
|
default_factory=set
|
|
|
|
)
|
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
|
2024-03-10 06:30:17 +00:00
|
|
|
assist_pipeline_update_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
|
2023-04-17 23:52:37 +00:00
|
|
|
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_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."""
|
2023-07-23 08:45:48 +00:00
|
|
|
device_info = self.device_info
|
|
|
|
return (device_info and device_info.name) or self.title
|
2022-10-31 05:31:37 +00:00
|
|
|
|
2023-01-16 07:33:44 +00:00
|
|
|
@property
|
|
|
|
def friendly_name(self) -> str:
|
|
|
|
"""Return the friendly name of the device."""
|
2023-07-23 08:45:48 +00:00
|
|
|
device_info = self.device_info
|
|
|
|
return (device_info and device_info.friendly_name) or self.name.title().replace(
|
|
|
|
"_", " "
|
|
|
|
)
|
2023-01-16 07:33:44 +00:00
|
|
|
|
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_)
|
2024-01-07 17:39:33 +00:00
|
|
|
return partial(
|
|
|
|
self._async_unsubscribe_register_static_info, callbacks, callback_
|
|
|
|
)
|
2023-06-22 07:39:48 +00:00
|
|
|
|
2024-01-07 17:39:33 +00:00
|
|
|
@callback
|
|
|
|
def _async_unsubscribe_register_static_info(
|
|
|
|
self,
|
|
|
|
callbacks: list[Callable[[list[EntityInfo]], None]],
|
|
|
|
callback_: Callable[[list[EntityInfo]], None],
|
|
|
|
) -> None:
|
|
|
|
"""Unsubscribe to when static info is registered."""
|
|
|
|
callbacks.remove(callback_)
|
2023-06-22 07:39:48 +00:00
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
@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_)
|
2024-01-07 17:39:33 +00:00
|
|
|
return partial(
|
|
|
|
self._async_unsubscribe_static_key_info_updated, callbacks, callback_
|
|
|
|
)
|
2023-06-26 02:31:31 +00:00
|
|
|
|
2024-01-07 17:39:33 +00:00
|
|
|
@callback
|
|
|
|
def _async_unsubscribe_static_key_info_updated(
|
|
|
|
self,
|
|
|
|
callbacks: list[Callable[[EntityInfo], None]],
|
|
|
|
callback_: Callable[[EntityInfo], None],
|
|
|
|
) -> None:
|
|
|
|
"""Unsubscribe to when static info is updated ."""
|
|
|
|
callbacks.remove(callback_)
|
2023-06-26 02:31:31 +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()
|
|
|
|
|
2024-01-07 17:39:33 +00:00
|
|
|
@callback
|
2023-04-17 23:52:37 +00:00
|
|
|
def async_subscribe_assist_pipeline_update(
|
2024-03-10 06:30:17 +00:00
|
|
|
self, update_callback: CALLBACK_TYPE
|
|
|
|
) -> CALLBACK_TYPE:
|
2023-04-17 23:52:37 +00:00
|
|
|
"""Subscribe to assist pipeline updates."""
|
|
|
|
self.assist_pipeline_update_callbacks.append(update_callback)
|
2024-01-07 17:39:33 +00:00
|
|
|
return partial(self._async_unsubscribe_assist_pipeline_update, update_callback)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_unsubscribe_assist_pipeline_update(
|
2024-03-10 06:30:17 +00:00
|
|
|
self, update_callback: CALLBACK_TYPE
|
2024-01-07 17:39:33 +00:00
|
|
|
) -> None:
|
|
|
|
"""Unsubscribe to assist pipeline updates."""
|
|
|
|
self.assist_pipeline_update_callbacks.remove(update_callback)
|
2023-04-17 23:52:37 +00:00
|
|
|
|
2024-02-22 21:39:53 +00:00
|
|
|
@callback
|
|
|
|
def async_remove_entities(
|
2024-01-25 03:29:11 +00:00
|
|
|
self, hass: HomeAssistant, static_infos: Iterable[EntityInfo], mac: str
|
|
|
|
) -> None:
|
2019-05-30 16:48:58 +00:00
|
|
|
"""Schedule the removal of an entity."""
|
2024-01-25 03:29:11 +00:00
|
|
|
# Remove from entity registry first so the entity is fully removed
|
|
|
|
ent_reg = er.async_get(hass)
|
|
|
|
for info in static_infos:
|
|
|
|
if entry := ent_reg.async_get_entity_id(
|
|
|
|
INFO_TYPE_TO_PLATFORM[type(info)], DOMAIN, build_unique_id(mac, info)
|
|
|
|
):
|
|
|
|
ent_reg.async_remove(entry)
|
|
|
|
|
2023-06-26 02:31:31 +00:00
|
|
|
@callback
|
|
|
|
def async_update_entity_infos(self, static_infos: Iterable[EntityInfo]) -> None:
|
|
|
|
"""Call static info updated callbacks."""
|
2024-01-07 17:39:33 +00:00
|
|
|
callbacks = self.entity_info_key_updated_callbacks
|
2023-06-26 02:31:31 +00:00
|
|
|
for static_info in static_infos:
|
2024-01-07 17:39:33 +00:00
|
|
|
for callback_ in callbacks.get((type(static_info), static_info.key), ()):
|
2023-06-26 02:31:31 +00:00
|
|
|
callback_(static_info)
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def _ensure_platforms_loaded(
|
Ensure config entries are not unloaded while their platforms are setting up (#118767)
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* run with error on to find them
* cert_exp, hold lock
* cert_exp, hold lock
* shelly async_late_forward_entry_setups
* compact
* compact
* found another
* patch up mobileapp
* patch up hue tests
* patch up smartthings
* fix mqtt
* fix esphome
* zwave_js
* mqtt
* rework
* fixes
* fix mocking
* fix mocking
* do not call async_forward_entry_setup directly
* docstrings
* docstrings
* docstrings
* add comments
* doc strings
* fixed all in core, turn off strict
* coverage
* coverage
* missing
* coverage
2024-06-05 01:34:39 +00:00
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entry: ConfigEntry,
|
|
|
|
platforms: set[Platform],
|
|
|
|
late: bool,
|
2021-07-12 20:56:10 +00:00
|
|
|
) -> None:
|
2019-06-18 15:41:45 +00:00
|
|
|
async with self.platform_load_lock:
|
2024-01-07 17:39:33 +00:00
|
|
|
if needed := platforms - self.loaded_platforms:
|
Ensure config entries are not unloaded while their platforms are setting up (#118767)
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* run with error on to find them
* cert_exp, hold lock
* cert_exp, hold lock
* shelly async_late_forward_entry_setups
* compact
* compact
* found another
* patch up mobileapp
* patch up hue tests
* patch up smartthings
* fix mqtt
* fix esphome
* zwave_js
* mqtt
* rework
* fixes
* fix mocking
* fix mocking
* do not call async_forward_entry_setup directly
* docstrings
* docstrings
* docstrings
* add comments
* doc strings
* fixed all in core, turn off strict
* coverage
* coverage
* missing
* coverage
2024-06-05 01:34:39 +00:00
|
|
|
if late:
|
|
|
|
await hass.config_entries.async_late_forward_entry_setups(
|
|
|
|
entry, needed
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
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(
|
Ensure config entries are not unloaded while their platforms are setting up (#118767)
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* run with error on to find them
* cert_exp, hold lock
* cert_exp, hold lock
* shelly async_late_forward_entry_setups
* compact
* compact
* found another
* patch up mobileapp
* patch up hue tests
* patch up smartthings
* fix mqtt
* fix esphome
* zwave_js
* mqtt
* rework
* fixes
* fix mocking
* fix mocking
* do not call async_forward_entry_setup directly
* docstrings
* docstrings
* docstrings
* add comments
* doc strings
* fixed all in core, turn off strict
* coverage
* coverage
* missing
* coverage
2024-06-05 01:34:39 +00:00
|
|
|
self,
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entry: ConfigEntry,
|
|
|
|
infos: list[EntityInfo],
|
|
|
|
mac: str,
|
|
|
|
late: bool = False,
|
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
|
|
|
|
2024-04-09 14:55:59 +00:00
|
|
|
if self.device_info and self.device_info.voice_assistant_feature_flags_compat(
|
|
|
|
self.api_version
|
|
|
|
):
|
2023-04-17 23:52:37 +00:00
|
|
|
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
|
|
|
|
2023-10-16 03:05:20 +00:00
|
|
|
ent_reg = er.async_get(hass)
|
|
|
|
registry_get_entity = ent_reg.async_get_entity_id
|
2019-06-18 15:41:45 +00:00
|
|
|
for info in infos:
|
2023-10-16 03:05:20 +00:00
|
|
|
platform = INFO_TYPE_TO_PLATFORM[type(info)]
|
|
|
|
needed_platforms.add(platform)
|
|
|
|
# If the unique id is in the old format, migrate it
|
|
|
|
# except if they downgraded and upgraded, there might be a duplicate
|
|
|
|
# so we want to keep the one that was already there.
|
|
|
|
if (
|
|
|
|
(old_unique_id := info.unique_id)
|
|
|
|
and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
|
|
|
|
and (new_unique_id := build_unique_id(mac, info)) != old_unique_id
|
|
|
|
and not registry_get_entity(platform, DOMAIN, new_unique_id)
|
|
|
|
):
|
|
|
|
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
|
|
|
|
|
Ensure config entries are not unloaded while their platforms are setting up (#118767)
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* Report non-awaited/non-locked config entry platform forwards
Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.
In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.
If config platform forwards are happening during setup, they should be awaited
If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup
* run with error on to find them
* cert_exp, hold lock
* cert_exp, hold lock
* shelly async_late_forward_entry_setups
* compact
* compact
* found another
* patch up mobileapp
* patch up hue tests
* patch up smartthings
* fix mqtt
* fix esphome
* zwave_js
* mqtt
* rework
* fixes
* fix mocking
* fix mocking
* do not call async_forward_entry_setup directly
* docstrings
* docstrings
* docstrings
* add comments
* doc strings
* fixed all in core, turn off strict
* coverage
* coverage
* missing
* coverage
2024-06-05 01:34:39 +00:00
|
|
|
await self._ensure_platforms_loaded(hass, entry, needed_platforms, late)
|
2019-06-18 15:41:45 +00:00
|
|
|
|
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)
|
|
|
|
|
2024-04-24 14:14:44 +00:00
|
|
|
# Finally update static info subscriptions
|
|
|
|
for callback_ in self.static_info_update_subscriptions:
|
|
|
|
callback_(infos)
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2024-03-10 06:30:17 +00:00
|
|
|
@callback
|
|
|
|
def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE:
|
|
|
|
"""Subscribe to state updates."""
|
|
|
|
self.device_update_subscriptions.add(callback_)
|
|
|
|
return partial(self._async_unsubscribe_device_update, callback_)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_unsubscribe_device_update(self, callback_: CALLBACK_TYPE) -> None:
|
|
|
|
"""Unsubscribe to device updates."""
|
|
|
|
self.device_update_subscriptions.remove(callback_)
|
|
|
|
|
2024-04-24 14:14:44 +00:00
|
|
|
@callback
|
|
|
|
def async_subscribe_static_info_updated(
|
|
|
|
self, callback_: Callable[[list[EntityInfo]], None]
|
|
|
|
) -> CALLBACK_TYPE:
|
|
|
|
"""Subscribe to static info updates."""
|
|
|
|
self.static_info_update_subscriptions.add(callback_)
|
|
|
|
return partial(self._async_unsubscribe_static_info_updated, callback_)
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_unsubscribe_static_info_updated(
|
|
|
|
self, callback_: Callable[[list[EntityInfo]], None]
|
|
|
|
) -> None:
|
|
|
|
"""Unsubscribe to static info updates."""
|
|
|
|
self.static_info_update_subscriptions.remove(callback_)
|
|
|
|
|
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,
|
2024-03-10 06:30:17 +00:00
|
|
|
entity_callback: CALLBACK_TYPE,
|
|
|
|
) -> CALLBACK_TYPE:
|
2022-07-01 05:19:40 +00:00
|
|
|
"""Subscribe to state updates."""
|
2024-01-07 17:39:33 +00:00
|
|
|
subscription_key = (state_type, state_key)
|
|
|
|
self.state_subscriptions[subscription_key] = entity_callback
|
|
|
|
return partial(self._async_unsubscribe_state_update, subscription_key)
|
2022-07-01 05:19:40 +00:00
|
|
|
|
2024-01-07 17:39:33 +00:00
|
|
|
@callback
|
|
|
|
def _async_unsubscribe_state_update(
|
|
|
|
self, subscription_key: tuple[type[EntityState], int]
|
|
|
|
) -> None:
|
|
|
|
"""Unsubscribe to state updates."""
|
|
|
|
self.state_subscriptions.pop(subscription_key)
|
2022-07-01 05:19:40 +00:00
|
|
|
|
|
|
|
@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
|
2024-04-24 01:22:03 +00:00
|
|
|
and state_type not in (CameraState, Event)
|
2023-05-05 02:21:42 +00:00
|
|
|
and not (
|
2024-03-16 19:48:37 +00:00
|
|
|
state_type is SensorState
|
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-07-23 08:45:48 +00:00
|
|
|
return
|
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()
|
2024-05-07 12:00:27 +00:00
|
|
|
except Exception:
|
2023-05-29 18:41:50 +00:00
|
|
|
# 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.
|
2024-03-29 06:20:36 +00:00
|
|
|
_LOGGER.exception("Error while calling subscription")
|
2019-05-30 16:48:58 +00:00
|
|
|
|
2020-01-29 21:59:45 +00:00
|
|
|
@callback
|
2024-03-10 06:30:17 +00:00
|
|
|
def async_update_device_state(self) -> None:
|
2019-05-30 16:48:58 +00:00
|
|
|
"""Distribute an update of a core device state like availability."""
|
2024-03-10 06:30:17 +00:00
|
|
|
for callback_ in self.device_update_subscriptions.copy():
|
|
|
|
callback_()
|
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
|
|
|
|
|
2024-02-19 09:38:28 +00:00
|
|
|
def async_save_to_store(self) -> None:
|
2019-05-30 16:48:58 +00:00
|
|
|
"""Generate dynamic data to store and save it to the filesystem."""
|
2023-07-23 08:45:48 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert self.device_info is not None
|
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
|
|
|
|
2023-07-23 08:45:48 +00:00
|
|
|
store_data["services"] = [
|
|
|
|
service.to_dict() for service in self.services.values()
|
|
|
|
]
|
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))
|
2023-11-21 06:58:22 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_on_disconnect(self) -> None:
|
|
|
|
"""Call when the entry has been disconnected.
|
|
|
|
|
|
|
|
Safe to call multiple times.
|
|
|
|
"""
|
|
|
|
self.available = False
|
2023-12-17 14:42:28 +00:00
|
|
|
if self.bluetooth_device:
|
|
|
|
self.bluetooth_device.available = False
|
2023-11-21 06:58:22 +00:00
|
|
|
# Make a copy since calling the disconnect callbacks
|
|
|
|
# may also try to discard/remove themselves.
|
|
|
|
for disconnect_cb in self.disconnect_callbacks.copy():
|
|
|
|
disconnect_cb()
|
|
|
|
# Make sure to clear the set to give up the reference
|
|
|
|
# to it and make sure all the callbacks can be GC'd.
|
|
|
|
self.disconnect_callbacks.clear()
|
|
|
|
self.disconnect_callbacks = set()
|
2023-12-17 14:42:28 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_on_connect(
|
|
|
|
self, device_info: DeviceInfo, api_version: APIVersion
|
|
|
|
) -> None:
|
|
|
|
"""Call when the entry has been connected."""
|
|
|
|
self.available = True
|
|
|
|
if self.bluetooth_device:
|
|
|
|
self.bluetooth_device.available = True
|
|
|
|
|
|
|
|
self.device_info = device_info
|
|
|
|
self.api_version = api_version
|
|
|
|
# Reset expected disconnect flag on successful reconnect
|
|
|
|
# as it will be flipped to False on unexpected disconnect.
|
|
|
|
#
|
|
|
|
# We use this to determine if a deep sleep device should
|
|
|
|
# be marked as unavailable or not.
|
|
|
|
self.expected_disconnect = True
|