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/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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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": [

View File

@ -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."""