Reduce overhead to update esphome entities (#94930)

This commit is contained in:
J. Nick Koston 2023-06-21 10:00:21 +01:00 committed by GitHub
parent 933ae5198e
commit 804a8ef36a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 158 deletions

View file

@ -356,8 +356,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/eq3btsmart/ @rytilahti /homeassistant/components/eq3btsmart/ @rytilahti
/homeassistant/components/escea/ @lazdavila /homeassistant/components/escea/ @lazdavila
/tests/components/escea/ @lazdavila /tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @OttoWinter @jesserockz /homeassistant/components/esphome/ @OttoWinter @jesserockz @bdraco
/tests/components/esphome/ @OttoWinter @jesserockz /tests/components/esphome/ @OttoWinter @jesserockz @bdraco
/homeassistant/components/eufylife_ble/ @bdr99 /homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99 /tests/components/eufylife_ble/ @bdr99
/homeassistant/components/evil_genius_labs/ @balloob /homeassistant/components/evil_genius_labs/ @balloob

View file

@ -725,11 +725,12 @@ async def platform_async_setup_entry(
# Then update the actual info # Then update the actual info
entry_data.info[component_key] = new_infos entry_data.info[component_key] = new_infos
async_dispatcher_send( for key, new_info in new_infos.items():
hass, async_dispatcher_send(
entry_data.signal_component_static_info_updated(component_key), hass,
new_infos, entry_data.signal_component_key_static_info_updated(component_key, key),
) new_info,
)
if add_entities: if add_entities:
# Add entities to Home Assistant # Add entities to Home Assistant
@ -785,6 +786,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
_attr_should_poll = False _attr_should_poll = False
_static_info: _InfoT _static_info: _InfoT
_state: _StateT
_has_state: bool
def __init__( def __init__(
self, self,
@ -795,150 +798,117 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
self._entry_data = entry_data self._entry_data = entry_data
self._on_entry_data_changed()
self._component_key = component_key self._component_key = component_key
self._key = entity_info.key self._key = entity_info.key
self._static_info = cast(_InfoT, entity_info)
self._state_type = state_type self._state_type = state_type
if entry_data.device_info is not None and entry_data.device_info.friendly_name: self._on_static_info_update(entity_info)
self._attr_has_entity_name = True 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: async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
entry_data = self._entry_data
hass = self.hass
component_key = self._component_key
key = self._key
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, hass,
f"esphome_{self._entry_id}_remove_{self._component_key}_{self._key}", f"esphome_{self._entry_id}_remove_{component_key}_{key}",
functools.partial(self.async_remove, force_remove=True), functools.partial(self.async_remove, force_remove=True),
) )
) )
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, hass,
self._entry_data.signal_device_updated, entry_data.signal_device_updated,
self._on_device_update, self._on_device_update,
) )
) )
self.async_on_remove( self.async_on_remove(
self._entry_data.async_subscribe_state_update( entry_data.async_subscribe_state_update(
self._state_type, self._key, self._on_state_update self._state_type, key, self._on_state_update
) )
) )
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect( async_dispatcher_connect(
self.hass, hass,
self._entry_data.signal_component_static_info_updated( entry_data.signal_component_key_static_info_updated(component_key, key),
self._component_key
),
self._on_static_info_update, self._on_static_info_update,
) )
) )
@callback @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. """Save the static info for this entity when it changes.
This method can be overridden in child classes to know This method can be overridden in child classes to know
when the static info changes. 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 @callback
def _on_state_update(self) -> None: 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() 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 @callback
def _on_device_update(self) -> None: def _on_device_update(self) -> None:
"""Update the entity state when device info has changed.""" """Call when device updates or entry data changes."""
if self._entry_data.available: self._on_entry_data_changed()
# Don't update the HA state yet when the device comes online. if not self._entry_data.available:
# Only update the HA state when the full state arrives # 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. # through the next entity state packet.
return self.async_write_ha_state()
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]
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return if the entity is available.""" """Return if the entity is available."""
device = self._device_info if self._device_info.has_deep_sleep:
if device.has_deep_sleep:
# During deep sleep the ESP will not be connectable (by design) # During deep sleep the ESP will not be connectable (by design)
# For these cases, show it as available # For these cases, show it as available
return True return True
return self._entry_data.available 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): class EsphomeAssistEntity(Entity):
"""Define a base entity for Assist Pipeline entities.""" """Define a base entity for Assist Pipeline entities."""
@ -949,20 +919,14 @@ class EsphomeAssistEntity(Entity):
def __init__(self, entry_data: RuntimeEntryData) -> None: def __init__(self, entry_data: RuntimeEntryData) -> None:
"""Initialize the binary sensor.""" """Initialize the binary sensor."""
self._entry_data: RuntimeEntryData = entry_data 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 = ( self._attr_unique_id = (
f"{self._device_info.mac_address}-{self.entity_description.key}" f"{device_info.mac_address}-{self.entity_description.key}"
) )
self._attr_device_info = DeviceInfo(
@property connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
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)}
) )
@callback @callback

View file

@ -1,7 +1,7 @@
"""Support for ESPHome binary sensors.""" """Support for ESPHome binary sensors."""
from __future__ import annotations from __future__ import annotations
from aioesphomeapi import BinarySensorInfo, BinarySensorState from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityInfo
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
@ -9,7 +9,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry 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.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum
@ -55,10 +55,13 @@ class EsphomeBinarySensor(
return None return None
return self._state.state return self._state.state
@property @callback
def device_class(self) -> BinarySensorDeviceClass | None: def _on_static_info_update(self, static_info: EntityInfo) -> None:
"""Return the class of this device, from component DEVICE_CLASSES.""" """Set attrs from static info."""
return try_parse_enum(BinarySensorDeviceClass, self._static_info.device_class) super()._on_static_info_update(static_info)
self._attr_device_class = try_parse_enum(
BinarySensorDeviceClass, self._static_info.device_class
)
@property @property
def available(self) -> bool: def available(self) -> bool:

View file

@ -129,9 +129,11 @@ class RuntimeEntryData:
"""Return the signal to listen to for updates on static info.""" """Return the signal to listen to for updates on static info."""
return f"esphome_{self.entry_id}_on_list" return f"esphome_{self.entry_id}_on_list"
def signal_component_static_info_updated(self, component_key: str) -> str: def signal_component_key_static_info_updated(
"""Return the signal to listen to for updates on static info for a specific component_key.""" self, component_key: str, key: int
return f"esphome_{self.entry_id}_static_info_updated_{component_key}" ) -> 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 @callback
def async_update_ble_connection_limits(self, free: int, limit: int) -> None: def async_update_ble_connection_limits(self, free: int, limit: int) -> None:

View file

@ -2,7 +2,7 @@
"domain": "esphome", "domain": "esphome",
"name": "ESPHome", "name": "ESPHome",
"after_dependencies": ["zeroconf", "tag"], "after_dependencies": ["zeroconf", "tag"],
"codeowners": ["@OttoWinter", "@jesserockz"], "codeowners": ["@OttoWinter", "@jesserockz", "@bdraco"],
"config_flow": true, "config_flow": true,
"dependencies": ["assist_pipeline", "bluetooth"], "dependencies": ["assist_pipeline", "bluetooth"],
"dhcp": [ "dhcp": [

View file

@ -5,6 +5,7 @@ from datetime import datetime
import math import math
from aioesphomeapi import ( from aioesphomeapi import (
EntityInfo,
SensorInfo, SensorInfo,
SensorState, SensorState,
SensorStateClass as EsphomeSensorStateClass, SensorStateClass as EsphomeSensorStateClass,
@ -19,7 +20,7 @@ from homeassistant.components.sensor import (
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry 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.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum
@ -67,10 +68,27 @@ _STATE_CLASSES: EsphomeEnumMapper[
class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
"""A sensor implementation for esphome.""" """A sensor implementation for esphome."""
@property @callback
def force_update(self) -> bool: def _on_static_info_update(self, static_info: EntityInfo) -> None:
"""Return if this sensor should force a state update.""" """Set attrs from static info."""
return self._static_info.force_update 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 @property
@esphome_state_property @esphome_state_property
@ -80,38 +98,10 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
return None return None
if self._state.missing_state: if self._state.missing_state:
return None 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 dt_util.utc_from_timestamp(self._state.state)
return f"{self._state.state:.{self._static_info.accuracy_decimals}f}" 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): class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity):
"""A text sensor implementation for ESPHome.""" """A text sensor implementation for ESPHome."""