Reduce overhead to update esphome entities (#94930)
This commit is contained in:
parent
933ae5198e
commit
804a8ef36a
6 changed files with 117 additions and 158 deletions
|
@ -356,8 +356,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/eq3btsmart/ @rytilahti
|
||||
/homeassistant/components/escea/ @lazdavila
|
||||
/tests/components/escea/ @lazdavila
|
||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz
|
||||
/tests/components/esphome/ @OttoWinter @jesserockz
|
||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz @bdraco
|
||||
/tests/components/esphome/ @OttoWinter @jesserockz @bdraco
|
||||
/homeassistant/components/eufylife_ble/ @bdr99
|
||||
/tests/components/eufylife_ble/ @bdr99
|
||||
/homeassistant/components/evil_genius_labs/ @balloob
|
||||
|
|
|
@ -725,11 +725,12 @@ async def platform_async_setup_entry(
|
|||
# Then update the actual info
|
||||
entry_data.info[component_key] = new_infos
|
||||
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
entry_data.signal_component_static_info_updated(component_key),
|
||||
new_infos,
|
||||
)
|
||||
for key, new_info in new_infos.items():
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
entry_data.signal_component_key_static_info_updated(component_key, key),
|
||||
new_info,
|
||||
)
|
||||
|
||||
if add_entities:
|
||||
# Add entities to Home Assistant
|
||||
|
@ -785,6 +786,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
|||
|
||||
_attr_should_poll = False
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -795,150 +798,117 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
|||
) -> None:
|
||||
"""Initialize."""
|
||||
self._entry_data = entry_data
|
||||
self._on_entry_data_changed()
|
||||
self._component_key = component_key
|
||||
self._key = entity_info.key
|
||||
self._static_info = cast(_InfoT, entity_info)
|
||||
self._state_type = state_type
|
||||
if entry_data.device_info is not None and entry_data.device_info.friendly_name:
|
||||
self._attr_has_entity_name = True
|
||||
self._on_static_info_update(entity_info)
|
||||
assert entry_data.device_info is not None
|
||||
device_info = entry_data.device_info
|
||||
self._device_info = device_info
|
||||
self._attr_has_entity_name = bool(device_info.friendly_name)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
self._entry_id = entry_data.entry_id
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
entry_data = self._entry_data
|
||||
hass = self.hass
|
||||
component_key = self._component_key
|
||||
key = self._key
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"esphome_{self._entry_id}_remove_{self._component_key}_{self._key}",
|
||||
hass,
|
||||
f"esphome_{self._entry_id}_remove_{component_key}_{key}",
|
||||
functools.partial(self.async_remove, force_remove=True),
|
||||
)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
self._entry_data.signal_device_updated,
|
||||
hass,
|
||||
entry_data.signal_device_updated,
|
||||
self._on_device_update,
|
||||
)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
self._entry_data.async_subscribe_state_update(
|
||||
self._state_type, self._key, self._on_state_update
|
||||
entry_data.async_subscribe_state_update(
|
||||
self._state_type, key, self._on_state_update
|
||||
)
|
||||
)
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
self._entry_data.signal_component_static_info_updated(
|
||||
self._component_key
|
||||
),
|
||||
hass,
|
||||
entry_data.signal_component_key_static_info_updated(component_key, key),
|
||||
self._on_static_info_update,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _on_static_info_update(self, static_infos: dict[int, EntityInfo]) -> None:
|
||||
def _on_static_info_update(self, static_info: EntityInfo) -> None:
|
||||
"""Save the static info for this entity when it changes.
|
||||
|
||||
This method can be overridden in child classes to know
|
||||
when the static info changes.
|
||||
"""
|
||||
self._static_info = cast(_InfoT, static_infos[self._key])
|
||||
static_info = cast(_InfoT, static_info)
|
||||
self._static_info = static_info
|
||||
self._attr_unique_id = static_info.unique_id
|
||||
self._attr_entity_registry_enabled_default = not static_info.disabled_by_default
|
||||
self._attr_name = static_info.name
|
||||
if entity_category := static_info.entity_category:
|
||||
self._attr_entity_category = ENTITY_CATEGORIES.from_esphome(entity_category)
|
||||
else:
|
||||
self._attr_entity_category = None
|
||||
if icon := static_info.icon:
|
||||
self._attr_icon = cast(str, ICON_SCHEMA(icon))
|
||||
else:
|
||||
self._attr_icon = None
|
||||
|
||||
@callback
|
||||
def _on_state_update(self) -> None:
|
||||
# Behavior can be changed in child classes
|
||||
"""Call when state changed.
|
||||
|
||||
Behavior can be changed in child classes
|
||||
"""
|
||||
state = self._entry_data.state
|
||||
key = self._key
|
||||
state_type = self._state_type
|
||||
has_state = key in state[state_type]
|
||||
if has_state:
|
||||
self._state = cast(_StateT, state[state_type][key])
|
||||
self._has_state = has_state
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _on_entry_data_changed(self) -> None:
|
||||
entry_data = self._entry_data
|
||||
self._api_version = entry_data.api_version
|
||||
self._client = entry_data.client
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
"""Update the entity state when device info has changed."""
|
||||
if self._entry_data.available:
|
||||
# Don't update the HA state yet when the device comes online.
|
||||
# Only update the HA state when the full state arrives
|
||||
"""Call when device updates or entry data changes."""
|
||||
self._on_entry_data_changed()
|
||||
if not self._entry_data.available:
|
||||
# Only write state if the device has gone unavailable
|
||||
# since _on_state_update will be called if the device
|
||||
# is available when the full state arrives
|
||||
# through the next entity state packet.
|
||||
return
|
||||
self._on_state_update()
|
||||
|
||||
@property
|
||||
def _entry_id(self) -> str:
|
||||
return self._entry_data.entry_id
|
||||
|
||||
@property
|
||||
def _api_version(self) -> APIVersion:
|
||||
return self._entry_data.api_version
|
||||
|
||||
@property
|
||||
def _device_info(self) -> EsphomeDeviceInfo:
|
||||
assert self._entry_data.device_info is not None
|
||||
return self._entry_data.device_info
|
||||
|
||||
@property
|
||||
def _client(self) -> APIClient:
|
||||
return self._entry_data.client
|
||||
|
||||
@property
|
||||
def _state(self) -> _StateT:
|
||||
return cast(_StateT, self._entry_data.state[self._state_type][self._key])
|
||||
|
||||
@property
|
||||
def _has_state(self) -> bool:
|
||||
return self._key in self._entry_data.state[self._state_type]
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
device = self._device_info
|
||||
|
||||
if device.has_deep_sleep:
|
||||
if self._device_info.has_deep_sleep:
|
||||
# During deep sleep the ESP will not be connectable (by design)
|
||||
# For these cases, show it as available
|
||||
return True
|
||||
|
||||
return self._entry_data.available
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str | None:
|
||||
"""Return a unique id identifying the entity."""
|
||||
if not self._static_info.unique_id:
|
||||
return None
|
||||
return self._static_info.unique_id
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)}
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._static_info.name
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon."""
|
||||
if not self._static_info.icon:
|
||||
return None
|
||||
|
||||
return cast(str, ICON_SCHEMA(self._static_info.icon))
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added.
|
||||
|
||||
This only applies when fist added to the entity registry.
|
||||
"""
|
||||
return not self._static_info.disabled_by_default
|
||||
|
||||
@property
|
||||
def entity_category(self) -> EntityCategory | None:
|
||||
"""Return the category of the entity, if any."""
|
||||
if not self._static_info.entity_category:
|
||||
return None
|
||||
return ENTITY_CATEGORIES.from_esphome(self._static_info.entity_category)
|
||||
|
||||
|
||||
class EsphomeAssistEntity(Entity):
|
||||
"""Define a base entity for Assist Pipeline entities."""
|
||||
|
@ -949,20 +919,14 @@ class EsphomeAssistEntity(Entity):
|
|||
def __init__(self, entry_data: RuntimeEntryData) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
self._entry_data: RuntimeEntryData = entry_data
|
||||
assert entry_data.device_info is not None
|
||||
device_info = entry_data.device_info
|
||||
self._device_info = device_info
|
||||
self._attr_unique_id = (
|
||||
f"{self._device_info.mac_address}-{self.entity_description.key}"
|
||||
f"{device_info.mac_address}-{self.entity_description.key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def _device_info(self) -> EsphomeDeviceInfo:
|
||||
assert self._entry_data.device_info is not None
|
||||
return self._entry_data.device_info
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)}
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
|
||||
@callback
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Support for ESPHome binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aioesphomeapi import BinarySensorInfo, BinarySensorState
|
||||
from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityInfo
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
|
@ -9,7 +9,7 @@ from homeassistant.components.binary_sensor import (
|
|||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
|
@ -55,10 +55,13 @@ class EsphomeBinarySensor(
|
|||
return None
|
||||
return self._state.state
|
||||
|
||||
@property
|
||||
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return try_parse_enum(BinarySensorDeviceClass, self._static_info.device_class)
|
||||
@callback
|
||||
def _on_static_info_update(self, static_info: EntityInfo) -> None:
|
||||
"""Set attrs from static info."""
|
||||
super()._on_static_info_update(static_info)
|
||||
self._attr_device_class = try_parse_enum(
|
||||
BinarySensorDeviceClass, self._static_info.device_class
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
|
|
@ -129,9 +129,11 @@ class RuntimeEntryData:
|
|||
"""Return the signal to listen to for updates on static info."""
|
||||
return f"esphome_{self.entry_id}_on_list"
|
||||
|
||||
def signal_component_static_info_updated(self, component_key: str) -> str:
|
||||
"""Return the signal to listen to for updates on static info for a specific component_key."""
|
||||
return f"esphome_{self.entry_id}_static_info_updated_{component_key}"
|
||||
def signal_component_key_static_info_updated(
|
||||
self, component_key: str, key: int
|
||||
) -> str:
|
||||
"""Return the signal to listen to for updates on static info for a specific component_key and key."""
|
||||
return f"esphome_{self.entry_id}_static_info_updated_{component_key}_{key}"
|
||||
|
||||
@callback
|
||||
def async_update_ble_connection_limits(self, free: int, limit: int) -> None:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "esphome",
|
||||
"name": "ESPHome",
|
||||
"after_dependencies": ["zeroconf", "tag"],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz", "@bdraco"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["assist_pipeline", "bluetooth"],
|
||||
"dhcp": [
|
||||
|
|
|
@ -5,6 +5,7 @@ from datetime import datetime
|
|||
import math
|
||||
|
||||
from aioesphomeapi import (
|
||||
EntityInfo,
|
||||
SensorInfo,
|
||||
SensorState,
|
||||
SensorStateClass as EsphomeSensorStateClass,
|
||||
|
@ -19,7 +20,7 @@ from homeassistant.components.sensor import (
|
|||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
@ -67,10 +68,27 @@ _STATE_CLASSES: EsphomeEnumMapper[
|
|||
class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
"""A sensor implementation for esphome."""
|
||||
|
||||
@property
|
||||
def force_update(self) -> bool:
|
||||
"""Return if this sensor should force a state update."""
|
||||
return self._static_info.force_update
|
||||
@callback
|
||||
def _on_static_info_update(self, static_info: EntityInfo) -> None:
|
||||
"""Set attrs from static info."""
|
||||
super()._on_static_info_update(static_info)
|
||||
static_info = self._static_info
|
||||
self._attr_force_update = static_info.force_update
|
||||
self._attr_native_unit_of_measurement = static_info.unit_of_measurement
|
||||
self._attr_device_class = try_parse_enum(
|
||||
SensorDeviceClass, static_info.device_class
|
||||
)
|
||||
if not (state_class := static_info.state_class):
|
||||
return
|
||||
if (
|
||||
state_class == EsphomeSensorStateClass.MEASUREMENT
|
||||
and static_info.last_reset_type == LastResetType.AUTO
|
||||
):
|
||||
# Legacy, last_reset_type auto was the equivalent to the
|
||||
# TOTAL_INCREASING state class
|
||||
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
else:
|
||||
self._attr_state_class = _STATE_CLASSES.from_esphome(state_class)
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
|
@ -80,38 +98,10 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
|||
return None
|
||||
if self._state.missing_state:
|
||||
return None
|
||||
if self.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
if self._attr_device_class == SensorDeviceClass.TIMESTAMP:
|
||||
return dt_util.utc_from_timestamp(self._state.state)
|
||||
return f"{self._state.state:.{self._static_info.accuracy_decimals}f}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit the value is expressed in."""
|
||||
if not self._static_info.unit_of_measurement:
|
||||
return None
|
||||
return self._static_info.unit_of_measurement
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return try_parse_enum(SensorDeviceClass, self._static_info.device_class)
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | None:
|
||||
"""Return the state class of this entity."""
|
||||
if not self._static_info.state_class:
|
||||
return None
|
||||
state_class = self._static_info.state_class
|
||||
reset_type = self._static_info.last_reset_type
|
||||
if (
|
||||
state_class == EsphomeSensorStateClass.MEASUREMENT
|
||||
and reset_type == LastResetType.AUTO
|
||||
):
|
||||
# Legacy, last_reset_type auto was the equivalent to the
|
||||
# TOTAL_INCREASING state class
|
||||
return SensorStateClass.TOTAL_INCREASING
|
||||
return _STATE_CLASSES.from_esphome(self._static_info.state_class)
|
||||
|
||||
|
||||
class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity):
|
||||
"""A text sensor implementation for ESPHome."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue