Delay all ZHA polling until initialization of entities has completed (#105814)

* Don't update entities until they are initialized

* fix hass reference

* only establish polling once

* fix log level and small cleanup

* start device availability checks after full initialization of network

* add logging

* clean up sensor polling and class hierarchy

* don't attempt restore sensor cleanup in this PR

* put check back

* fix race condition and remove parallel updates

* add sensor polling test

* cleanup switch polling and add a test

* clean up and actually fix race condition

* update light forced refresh

* only use flag

* unused flag

* reduce diff size

* collapse
This commit is contained in:
David F. Mulcahey 2023-12-27 10:25:41 -05:00 committed by GitHub
parent 45fde2db4e
commit 817c71747f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 204 additions and 73 deletions

View File

@ -166,6 +166,9 @@ class ZHADevice(LogMixin):
if not self.is_coordinator: if not self.is_coordinator:
keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL) keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL)
self.debug(
"starting availability checks - interval: %s", keep_alive_interval
)
self.unsubs.append( self.unsubs.append(
async_track_time_interval( async_track_time_interval(
self.hass, self.hass,
@ -447,35 +450,36 @@ class ZHADevice(LogMixin):
self._checkins_missed_count = 0 self._checkins_missed_count = 0
return return
if ( if self.hass.data[const.DATA_ZHA].allow_polling:
self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS if (
or self.manufacturer == "LUMI" self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS
or not self._endpoints or self.manufacturer == "LUMI"
): or not self._endpoints
self.debug( ):
( self.debug(
"last_seen is %s seconds ago and ping attempts have been exhausted," (
" marking the device unavailable" "last_seen is %s seconds ago and ping attempts have been exhausted,"
), " marking the device unavailable"
difference, ),
) difference,
self.update_available(False) )
return self.update_available(False)
return
self._checkins_missed_count += 1 self._checkins_missed_count += 1
self.debug( self.debug(
"Attempting to checkin with device - missed checkins: %s", "Attempting to checkin with device - missed checkins: %s",
self._checkins_missed_count, self._checkins_missed_count,
) )
if not self.basic_ch: if not self.basic_ch:
self.debug("does not have a mandatory basic cluster") self.debug("does not have a mandatory basic cluster")
self.update_available(False) self.update_available(False)
return return
res = await self.basic_ch.get_attribute_value( res = await self.basic_ch.get_attribute_value(
ATTR_MANUFACTURER, from_cache=False ATTR_MANUFACTURER, from_cache=False
) )
if res is not None: if res is not None:
self._checkins_missed_count = 0 self._checkins_missed_count = 0
def update_available(self, available: bool) -> None: def update_available(self, available: bool) -> None:
"""Update device availability and signal entities.""" """Update device availability and signal entities."""

View File

@ -47,6 +47,7 @@ from .const import (
ATTR_TYPE, ATTR_TYPE,
CONF_RADIO_TYPE, CONF_RADIO_TYPE,
CONF_ZIGPY, CONF_ZIGPY,
DATA_ZHA,
DEBUG_COMP_BELLOWS, DEBUG_COMP_BELLOWS,
DEBUG_COMP_ZHA, DEBUG_COMP_ZHA,
DEBUG_COMP_ZIGPY, DEBUG_COMP_ZIGPY,
@ -292,6 +293,10 @@ class ZHAGateway:
if dev.is_mains_powered if dev.is_mains_powered
) )
) )
_LOGGER.debug(
"completed fetching current state for mains powered devices - allowing polled requests"
)
self.hass.data[DATA_ZHA].allow_polling = True
# background the fetching of state for mains powered devices # background the fetching of state for mains powered devices
self.config_entry.async_create_background_task( self.config_entry.async_create_background_task(

View File

@ -442,6 +442,7 @@ class ZHAData:
device_trigger_cache: dict[str, tuple[str, dict]] = dataclasses.field( device_trigger_cache: dict[str, tuple[str, dict]] = dataclasses.field(
default_factory=dict default_factory=dict
) )
allow_polling: bool = dataclasses.field(default=False)
def get_zha_data(hass: HomeAssistant) -> ZHAData: def get_zha_data(hass: HomeAssistant) -> ZHAData:

View File

@ -47,6 +47,7 @@ from .core.const import (
CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION,
CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
CONF_GROUP_MEMBERS_ASSUME_STATE, CONF_GROUP_MEMBERS_ASSUME_STATE,
DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
SIGNAL_SET_LEVEL, SIGNAL_SET_LEVEL,
@ -75,7 +76,6 @@ FLASH_EFFECTS = {
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT)
GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT)
PARALLEL_UPDATES = 0
SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed"
SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start"
SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished"
@ -788,6 +788,7 @@ class Light(BaseLight, ZhaEntity):
self._cancel_refresh_handle = async_track_time_interval( self._cancel_refresh_handle = async_track_time_interval(
self.hass, self._refresh, timedelta(seconds=refresh_interval) self.hass, self._refresh, timedelta(seconds=refresh_interval)
) )
self.debug("started polling with refresh interval of %s", refresh_interval)
self.async_accept_signal( self.async_accept_signal(
None, None,
SIGNAL_LIGHT_GROUP_STATE_CHANGED, SIGNAL_LIGHT_GROUP_STATE_CHANGED,
@ -838,6 +839,8 @@ class Light(BaseLight, ZhaEntity):
"""Disconnect entity object when removed.""" """Disconnect entity object when removed."""
assert self._cancel_refresh_handle assert self._cancel_refresh_handle
self._cancel_refresh_handle() self._cancel_refresh_handle()
self._cancel_refresh_handle = None
self.debug("stopped polling during device removal")
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
@callback @callback
@ -980,8 +983,16 @@ class Light(BaseLight, ZhaEntity):
if self.is_transitioning: if self.is_transitioning:
self.debug("skipping _refresh while transitioning") self.debug("skipping _refresh while transitioning")
return return
await self.async_get_state() if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling:
self.async_write_ha_state() self.debug("polling for updated state")
await self.async_get_state()
self.async_write_ha_state()
else:
self.debug(
"skipping polling for updated state, available: %s, allow polled requests: %s",
self._zha_device.available,
self.hass.data[DATA_ZHA].allow_polling,
)
async def _maybe_force_refresh(self, signal): async def _maybe_force_refresh(self, signal):
"""Force update the state if the signal contains the entity id for this entity.""" """Force update the state if the signal contains the entity id for this entity."""
@ -989,8 +1000,16 @@ class Light(BaseLight, ZhaEntity):
if self.is_transitioning: if self.is_transitioning:
self.debug("skipping _maybe_force_refresh while transitioning") self.debug("skipping _maybe_force_refresh while transitioning")
return return
await self.async_get_state() if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling:
self.async_write_ha_state() self.debug("forcing polling for updated state")
await self.async_get_state()
self.async_write_ha_state()
else:
self.debug(
"skipping _maybe_force_refresh, available: %s, allow polled requests: %s",
self._zha_device.available,
self.hass.data[DATA_ZHA].allow_polling,
)
@callback @callback
def _assume_group_state(self, signal, update_params) -> None: def _assume_group_state(self, signal, update_params) -> None:

View File

@ -1,9 +1,11 @@
"""Sensors on Zigbee Home Automation networks.""" """Sensors on Zigbee Home Automation networks."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
import enum import enum
import functools import functools
import numbers import numbers
import random
from typing import TYPE_CHECKING, Any, Self from typing import TYPE_CHECKING, Any, Self
from zigpy import types from zigpy import types
@ -37,9 +39,10 @@ from homeassistant.const import (
UnitOfVolume, UnitOfVolume,
UnitOfVolumeFlowRate, UnitOfVolumeFlowRate,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from .core import discovery from .core import discovery
@ -57,6 +60,7 @@ from .core.const import (
CLUSTER_HANDLER_SOIL_MOISTURE, CLUSTER_HANDLER_SOIL_MOISTURE,
CLUSTER_HANDLER_TEMPERATURE, CLUSTER_HANDLER_TEMPERATURE,
CLUSTER_HANDLER_THERMOSTAT, CLUSTER_HANDLER_THERMOSTAT,
DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
) )
@ -68,8 +72,6 @@ if TYPE_CHECKING:
from .core.cluster_handlers import ClusterHandler from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
PARALLEL_UPDATES = 5
BATTERY_SIZES = { BATTERY_SIZES = {
0: "No battery", 0: "No battery",
1: "Built in", 1: "Built in",
@ -185,6 +187,55 @@ class Sensor(ZhaEntity, SensorEntity):
return round(float(value * self._multiplier) / self._divisor) return round(float(value * self._multiplier) / self._divisor)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class PollableSensor(Sensor):
"""Base ZHA sensor that polls for state."""
_use_custom_polling: bool = True
def __init__(
self,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> None:
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._cancel_refresh_handle: CALLBACK_TYPE | None = None
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
await super().async_added_to_hass()
if self._use_custom_polling:
refresh_interval = random.randint(30, 60)
self._cancel_refresh_handle = async_track_time_interval(
self.hass, self._refresh, timedelta(seconds=refresh_interval)
)
self.debug("started polling with refresh interval of %s", refresh_interval)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed."""
assert self._cancel_refresh_handle
self._cancel_refresh_handle()
self._cancel_refresh_handle = None
self.debug("stopped polling during device removal")
await super().async_will_remove_from_hass()
async def _refresh(self, time):
"""Call async_update at a constrained random interval."""
if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling:
self.debug("polling for updated state")
await self.async_update()
self.async_write_ha_state()
else:
self.debug(
"skipping polling for updated state, available: %s, allow polled requests: %s",
self._zha_device.available,
self.hass.data[DATA_ZHA].allow_polling,
)
@MULTI_MATCH( @MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT, cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT,
manufacturers="Digi", manufacturers="Digi",
@ -258,9 +309,10 @@ class Battery(Sensor):
models={"VZM31-SN", "SP 234", "outletv4"}, models={"VZM31-SN", "SP 234", "outletv4"},
) )
# pylint: disable-next=hass-invalid-inheritance # needs fixing # pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurement(Sensor): class ElectricalMeasurement(PollableSensor):
"""Active power measurement.""" """Active power measurement."""
_use_custom_polling: bool = False
_attribute_name = "active_power" _attribute_name = "active_power"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
@ -306,22 +358,17 @@ class ElectricalMeasurement(Sensor):
class PolledElectricalMeasurement(ElectricalMeasurement): class PolledElectricalMeasurement(ElectricalMeasurement):
"""Polled active power measurement.""" """Polled active power measurement."""
_attr_should_poll = True # BaseZhaEntity defaults to False _use_custom_polling: bool = True
async def async_update(self) -> None:
"""Retrieve latest state."""
if not self.available:
return
await super().async_update()
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing # pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementApparentPower(ElectricalMeasurement): class ElectricalMeasurementApparentPower(PolledElectricalMeasurement):
"""Apparent power measurement.""" """Apparent power measurement."""
_attribute_name = "apparent_power" _attribute_name = "apparent_power"
_unique_id_suffix = "apparent_power" _unique_id_suffix = "apparent_power"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER _attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER
_attr_native_unit_of_measurement = UnitOfApparentPower.VOLT_AMPERE _attr_native_unit_of_measurement = UnitOfApparentPower.VOLT_AMPERE
_div_mul_prefix = "ac_power" _div_mul_prefix = "ac_power"
@ -329,11 +376,12 @@ class ElectricalMeasurementApparentPower(ElectricalMeasurement):
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing # pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementRMSCurrent(ElectricalMeasurement): class ElectricalMeasurementRMSCurrent(PolledElectricalMeasurement):
"""RMS current measurement.""" """RMS current measurement."""
_attribute_name = "rms_current" _attribute_name = "rms_current"
_unique_id_suffix = "rms_current" _unique_id_suffix = "rms_current"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE _attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
_div_mul_prefix = "ac_current" _div_mul_prefix = "ac_current"
@ -341,11 +389,12 @@ class ElectricalMeasurementRMSCurrent(ElectricalMeasurement):
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing # pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementRMSVoltage(ElectricalMeasurement): class ElectricalMeasurementRMSVoltage(PolledElectricalMeasurement):
"""RMS Voltage measurement.""" """RMS Voltage measurement."""
_attribute_name = "rms_voltage" _attribute_name = "rms_voltage"
_unique_id_suffix = "rms_voltage" _unique_id_suffix = "rms_voltage"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLTAGE _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLTAGE
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT _attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_div_mul_prefix = "ac_voltage" _div_mul_prefix = "ac_voltage"
@ -353,11 +402,12 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement):
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing # pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementFrequency(ElectricalMeasurement): class ElectricalMeasurementFrequency(PolledElectricalMeasurement):
"""Frequency measurement.""" """Frequency measurement."""
_attribute_name = "ac_frequency" _attribute_name = "ac_frequency"
_unique_id_suffix = "ac_frequency" _unique_id_suffix = "ac_frequency"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY _attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY
_attr_translation_key: str = "ac_frequency" _attr_translation_key: str = "ac_frequency"
_attr_native_unit_of_measurement = UnitOfFrequency.HERTZ _attr_native_unit_of_measurement = UnitOfFrequency.HERTZ
@ -366,11 +416,12 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement):
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing # pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementPowerFactor(ElectricalMeasurement): class ElectricalMeasurementPowerFactor(PolledElectricalMeasurement):
"""Frequency measurement.""" """Frequency measurement."""
_attribute_name = "power_factor" _attribute_name = "power_factor"
_unique_id_suffix = "power_factor" _unique_id_suffix = "power_factor"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR
_attr_native_unit_of_measurement = PERCENTAGE _attr_native_unit_of_measurement = PERCENTAGE
@ -440,9 +491,10 @@ class Illuminance(Sensor):
stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING, stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
) )
# pylint: disable-next=hass-invalid-inheritance # needs fixing # pylint: disable-next=hass-invalid-inheritance # needs fixing
class SmartEnergyMetering(Sensor): class SmartEnergyMetering(PollableSensor):
"""Metering sensor.""" """Metering sensor."""
_use_custom_polling: bool = False
_attribute_name = "instantaneous_demand" _attribute_name = "instantaneous_demand"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
@ -540,13 +592,7 @@ class SmartEnergySummation(SmartEnergyMetering):
class PolledSmartEnergySummation(SmartEnergySummation): class PolledSmartEnergySummation(SmartEnergySummation):
"""Polled Smart Energy Metering summation sensor.""" """Polled Smart Energy Metering summation sensor."""
_attr_should_poll = True # BaseZhaEntity defaults to False _use_custom_polling: bool = True
async def async_update(self) -> None:
"""Retrieve latest state."""
if not self.available:
return
await self._cluster_handler.async_force_update()
@MULTI_MATCH( @MULTI_MATCH(
@ -557,6 +603,7 @@ class PolledSmartEnergySummation(SmartEnergySummation):
class Tier1SmartEnergySummation(PolledSmartEnergySummation): class Tier1SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 1 Smart Energy Metering summation sensor.""" """Tier 1 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier1_summ_delivered" _attribute_name = "current_tier1_summ_delivered"
_unique_id_suffix = "tier1_summation_delivered" _unique_id_suffix = "tier1_summation_delivered"
_attr_translation_key: str = "tier1_summation_delivered" _attr_translation_key: str = "tier1_summation_delivered"
@ -570,6 +617,7 @@ class Tier1SmartEnergySummation(PolledSmartEnergySummation):
class Tier2SmartEnergySummation(PolledSmartEnergySummation): class Tier2SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 2 Smart Energy Metering summation sensor.""" """Tier 2 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier2_summ_delivered" _attribute_name = "current_tier2_summ_delivered"
_unique_id_suffix = "tier2_summation_delivered" _unique_id_suffix = "tier2_summation_delivered"
_attr_translation_key: str = "tier2_summation_delivered" _attr_translation_key: str = "tier2_summation_delivered"
@ -583,6 +631,7 @@ class Tier2SmartEnergySummation(PolledSmartEnergySummation):
class Tier3SmartEnergySummation(PolledSmartEnergySummation): class Tier3SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 3 Smart Energy Metering summation sensor.""" """Tier 3 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier3_summ_delivered" _attribute_name = "current_tier3_summ_delivered"
_unique_id_suffix = "tier3_summation_delivered" _unique_id_suffix = "tier3_summation_delivered"
_attr_translation_key: str = "tier3_summation_delivered" _attr_translation_key: str = "tier3_summation_delivered"
@ -596,6 +645,7 @@ class Tier3SmartEnergySummation(PolledSmartEnergySummation):
class Tier4SmartEnergySummation(PolledSmartEnergySummation): class Tier4SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 4 Smart Energy Metering summation sensor.""" """Tier 4 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier4_summ_delivered" _attribute_name = "current_tier4_summ_delivered"
_unique_id_suffix = "tier4_summation_delivered" _unique_id_suffix = "tier4_summation_delivered"
_attr_translation_key: str = "tier4_summation_delivered" _attr_translation_key: str = "tier4_summation_delivered"
@ -609,6 +659,7 @@ class Tier4SmartEnergySummation(PolledSmartEnergySummation):
class Tier5SmartEnergySummation(PolledSmartEnergySummation): class Tier5SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 5 Smart Energy Metering summation sensor.""" """Tier 5 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier5_summ_delivered" _attribute_name = "current_tier5_summ_delivered"
_unique_id_suffix = "tier5_summation_delivered" _unique_id_suffix = "tier5_summation_delivered"
_attr_translation_key: str = "tier5_summation_delivered" _attr_translation_key: str = "tier5_summation_delivered"
@ -622,6 +673,7 @@ class Tier5SmartEnergySummation(PolledSmartEnergySummation):
class Tier6SmartEnergySummation(PolledSmartEnergySummation): class Tier6SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 6 Smart Energy Metering summation sensor.""" """Tier 6 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier6_summ_delivered" _attribute_name = "current_tier6_summ_delivered"
_unique_id_suffix = "tier6_summation_delivered" _unique_id_suffix = "tier6_summation_delivered"
_attr_translation_key: str = "tier6_summation_delivered" _attr_translation_key: str = "tier6_summation_delivered"

View File

@ -108,11 +108,10 @@ class Switch(ZhaEntity, SwitchEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Attempt to retrieve on off state from the switch.""" """Attempt to retrieve on off state from the switch."""
await super().async_update() self.debug("Polling current state")
if self._on_off_cluster_handler: await self._on_off_cluster_handler.get_attribute_value(
await self._on_off_cluster_handler.get_attribute_value( "on_off", from_cache=False
"on_off", from_cache=False )
)
@GROUP_MATCH() @GROUP_MATCH()
@ -255,16 +254,14 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Attempt to retrieve the state of the entity.""" """Attempt to retrieve the state of the entity."""
await super().async_update() self.debug("Polling current state")
self.error("Polling current state") value = await self._cluster_handler.get_attribute_value(
if self._cluster_handler: self._attribute_name, from_cache=False
value = await self._cluster_handler.get_attribute_value( )
self._attribute_name, from_cache=False await self._cluster_handler.get_attribute_value(
) self._inverter_attribute_name, from_cache=False
await self._cluster_handler.get_attribute_value( )
self._inverter_attribute_name, from_cache=False self.debug("read value=%s, inverted=%s", value, self.inverted)
)
self.debug("read value=%s, inverted=%s", value, self.inverted)
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(

View File

@ -1,4 +1,5 @@
"""Test ZHA sensor.""" """Test ZHA sensor."""
from datetime import timedelta
import math import math
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -47,7 +48,10 @@ from .common import (
) )
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from tests.common import async_mock_load_restore_state_from_storage from tests.common import (
async_fire_time_changed,
async_mock_load_restore_state_from_storage,
)
ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}" ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}"
@ -921,6 +925,44 @@ async def test_elec_measurement_sensor_type(
assert state.attributes["measurement_type"] == expected_type assert state.attributes["measurement_type"] == expected_type
async def test_elec_measurement_sensor_polling(
hass: HomeAssistant,
elec_measurement_zigpy_dev,
zha_device_joined_restored,
) -> None:
"""Test ZHA electrical measurement sensor polling."""
entity_id = ENTITY_ID_PREFIX.format("power")
zigpy_dev = elec_measurement_zigpy_dev
zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[
"active_power"
] = 20
await zha_device_joined_restored(zigpy_dev)
# test that the sensor has an initial state of 2.0
state = hass.states.get(entity_id)
assert state.state == "2.0"
# update the value for the power reading
zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[
"active_power"
] = 60
# ensure the state is still 2.0
state = hass.states.get(entity_id)
assert state.state == "2.0"
# let the polling happen
future = dt_util.utcnow() + timedelta(seconds=90)
async_fire_time_changed(hass, future)
await hass.async_block_till_done()
# ensure the state has been updated to 6.0
state = hass.states.get(entity_id)
assert state.state == "6.0"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"supported_attributes", "supported_attributes",
( (

View File

@ -197,6 +197,17 @@ async def test_switch(
tsn=None, tsn=None,
) )
await async_setup_component(hass, "homeassistant", {})
cluster.read_attributes.reset_mock()
await hass.services.async_call(
"homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True
)
assert len(cluster.read_attributes.mock_calls) == 1
assert cluster.read_attributes.call_args == call(
["on_off"], allow_cache=False, only_cache=False, manufacturer=None
)
# test joining a new switch to the network and HA # test joining a new switch to the network and HA
await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) await async_test_rejoin(hass, zigpy_device, [cluster], (1,))