Skip to content

Commit

Permalink
Delay all ZHA polling until initialization of entities has completed (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
dmulcahey committed Dec 27, 2023
1 parent 45fde2d commit 817c717
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 72 deletions.
58 changes: 31 additions & 27 deletions homeassistant/components/zha/core/device.py
Expand Up @@ -166,6 +166,9 @@ def __init__(

if not self.is_coordinator:
keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL)
self.debug(
"starting availability checks - interval: %s", keep_alive_interval
)
self.unsubs.append(
async_track_time_interval(
self.hass,
Expand Down Expand Up @@ -447,35 +450,36 @@ async def _check_available(self, *_: Any) -> None:
self._checkins_missed_count = 0
return

if (
self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS
or self.manufacturer == "LUMI"
or not self._endpoints
):
if self.hass.data[const.DATA_ZHA].allow_polling:
if (
self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS
or self.manufacturer == "LUMI"
or not self._endpoints
):
self.debug(
(
"last_seen is %s seconds ago and ping attempts have been exhausted,"
" marking the device unavailable"
),
difference,
)
self.update_available(False)
return

self._checkins_missed_count += 1
self.debug(
(
"last_seen is %s seconds ago and ping attempts have been exhausted,"
" marking the device unavailable"
),
difference,
"Attempting to checkin with device - missed checkins: %s",
self._checkins_missed_count,
)
self.update_available(False)
return

self._checkins_missed_count += 1
self.debug(
"Attempting to checkin with device - missed checkins: %s",
self._checkins_missed_count,
)
if not self.basic_ch:
self.debug("does not have a mandatory basic cluster")
self.update_available(False)
return
res = await self.basic_ch.get_attribute_value(
ATTR_MANUFACTURER, from_cache=False
)
if res is not None:
self._checkins_missed_count = 0
if not self.basic_ch:
self.debug("does not have a mandatory basic cluster")
self.update_available(False)
return
res = await self.basic_ch.get_attribute_value(
ATTR_MANUFACTURER, from_cache=False
)
if res is not None:
self._checkins_missed_count = 0

def update_available(self, available: bool) -> None:
"""Update device availability and signal entities."""
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/zha/core/gateway.py
Expand Up @@ -47,6 +47,7 @@
ATTR_TYPE,
CONF_RADIO_TYPE,
CONF_ZIGPY,
DATA_ZHA,
DEBUG_COMP_BELLOWS,
DEBUG_COMP_ZHA,
DEBUG_COMP_ZIGPY,
Expand Down Expand Up @@ -292,6 +293,10 @@ async def fetch_updated_state() -> None:
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
self.config_entry.async_create_background_task(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/zha/core/helpers.py
Expand Up @@ -442,6 +442,7 @@ class ZHAData:
device_trigger_cache: dict[str, tuple[str, dict]] = dataclasses.field(
default_factory=dict
)
allow_polling: bool = dataclasses.field(default=False)


def get_zha_data(hass: HomeAssistant) -> ZHAData:
Expand Down
29 changes: 24 additions & 5 deletions homeassistant/components/zha/light.py
Expand Up @@ -47,6 +47,7 @@
CONF_ENABLE_ENHANCED_LIGHT_TRANSITION,
CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
CONF_GROUP_MEMBERS_ASSUME_STATE,
DATA_ZHA,
SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED,
SIGNAL_SET_LEVEL,
Expand Down Expand Up @@ -75,7 +76,6 @@

STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_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_TRANSITION_START = "zha_light_group_transition_start"
SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished"
Expand Down Expand Up @@ -788,6 +788,7 @@ async def async_added_to_hass(self) -> None:
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)
self.async_accept_signal(
None,
SIGNAL_LIGHT_GROUP_STATE_CHANGED,
Expand Down Expand Up @@ -838,6 +839,8 @@ 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()

@callback
Expand Down Expand Up @@ -980,17 +983,33 @@ async def _refresh(self, time):
if self.is_transitioning:
self.debug("skipping _refresh while transitioning")
return
await self.async_get_state()
self.async_write_ha_state()
if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling:
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):
"""Force update the state if the signal contains the entity id for this entity."""
if self.entity_id in signal["entity_ids"]:
if self.is_transitioning:
self.debug("skipping _maybe_force_refresh while transitioning")
return
await self.async_get_state()
self.async_write_ha_state()
if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling:
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
def _assume_group_state(self, signal, update_params) -> None:
Expand Down

0 comments on commit 817c717

Please sign in to comment.