Reduce august polling frequency (#114904)

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
This commit is contained in:
J. Nick Koston 2024-04-04 15:28:36 -10:00 committed by GitHub
parent a66ed1d936
commit 78920e1d71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 58 additions and 21 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from functools import partial from functools import partial
import logging import logging
from time import monotonic
from aiohttp import ClientError from aiohttp import ClientError
from yalexs.activity import Activity, ActivityType from yalexs.activity import Activity, ActivityType
@ -26,9 +27,11 @@ _LOGGER = logging.getLogger(__name__)
ACTIVITY_STREAM_FETCH_LIMIT = 10 ACTIVITY_STREAM_FETCH_LIMIT = 10
ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
INITIAL_LOCK_RESYNC_TIME = 60
# If there is a storm of activity (ie lock, unlock, door open, door close, etc) # If there is a storm of activity (ie lock, unlock, door open, door close, etc)
# we want to debounce the updates so we don't hammer the activity api too much. # we want to debounce the updates so we don't hammer the activity api too much.
ACTIVITY_DEBOUNCE_COOLDOWN = 3 ACTIVITY_DEBOUNCE_COOLDOWN = 4
@callback @callback
@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin):
self.pubnub = pubnub self.pubnub = pubnub
self._update_debounce: dict[str, Debouncer] = {} self._update_debounce: dict[str, Debouncer] = {}
self._update_debounce_jobs: dict[str, HassJob] = {} self._update_debounce_jobs: dict[str, HassJob] = {}
self._start_time: float | None = None
@callback @callback
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None: def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin):
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Token refresh check and catch up the activity stream.""" """Token refresh check and catch up the activity stream."""
self._start_time = monotonic()
update_debounce = self._update_debounce update_debounce = self._update_debounce
update_debounce_jobs = self._update_debounce_jobs update_debounce_jobs = self._update_debounce_jobs
for house_id in self._house_ids: for house_id in self._house_ids:
@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin):
debouncer = self._update_debounce[house_id] debouncer = self._update_debounce[house_id]
debouncer.async_schedule_call() debouncer.async_schedule_call()
# Schedule two updates past the debounce time # Schedule two updates past the debounce time
# to ensure we catch the case where the activity # to ensure we catch the case where the activity
# api does not update right away and we need to poll # api does not update right away and we need to poll
# it again. Sometimes the lock operator or a doorbell # it again. Sometimes the lock operator or a doorbell
# will not show up in the activity stream right away. # will not show up in the activity stream right away.
# Only do additional polls if we are past
# the initial lock resync time to avoid a storm
# of activity at setup.
if (
not self._start_time
or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME
):
_LOGGER.debug(
"Skipping additional updates due to ongoing initial lock resync time"
)
return
_LOGGER.debug("Scheduling additional updates for house id %s", house_id)
job = self._update_debounce_jobs[house_id] job = self._update_debounce_jobs[house_id]
for step in (1, 2): for step in (1, 2):
future_updates.append( future_updates.append(

View file

@ -40,7 +40,7 @@ ATTR_OPERATION_TAG = "tag"
# Limit battery, online, and hardware updates to hourly # Limit battery, online, and hardware updates to hourly
# in order to reduce the number of api requests and # in order to reduce the number of api requests and
# avoid hitting rate limits # avoid hitting rate limits
MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1) MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24)
# Activity needs to be checked more frequently as the # Activity needs to be checked more frequently as the
# doorbell motion and rings are included here # doorbell motion and rings are included here

View file

@ -49,9 +49,17 @@ class AugustSubscriberMixin:
"""Call the refresh method.""" """Call the refresh method."""
self._hass.async_create_task(self._async_refresh(now), eager_start=True) self._hass.async_create_task(self._async_refresh(now), eager_start=True)
@callback
def _async_cancel_update_interval(self, _: Event | None = None) -> None:
"""Cancel the scheduled update."""
if self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
@callback @callback
def _async_setup_listeners(self) -> None: def _async_setup_listeners(self) -> None:
"""Create interval and stop listeners.""" """Create interval and stop listeners."""
self._async_cancel_update_interval()
self._unsub_interval = async_track_time_interval( self._unsub_interval = async_track_time_interval(
self._hass, self._hass,
self._async_scheduled_refresh, self._async_scheduled_refresh,
@ -59,15 +67,10 @@ class AugustSubscriberMixin:
name="august refresh", name="august refresh",
) )
@callback if not self._stop_interval:
def _async_cancel_update_interval(_: Event) -> None:
self._stop_interval = None
if self._unsub_interval:
self._unsub_interval()
self._stop_interval = self._hass.bus.async_listen( self._stop_interval = self._hass.bus.async_listen(
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
_async_cancel_update_interval, self._async_cancel_update_interval,
run_immediately=True, run_immediately=True,
) )
@ -82,13 +85,7 @@ class AugustSubscriberMixin:
if self._subscriptions: if self._subscriptions:
return return
self._async_cancel_update_interval()
if self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
if self._stop_interval:
self._stop_interval()
self._stop_interval = None
@callback @callback
def async_signal_device_id_update(self, device_id: str) -> None: def async_signal_device_id_update(self, device_id: str) -> None:

View file

@ -4,9 +4,11 @@ import datetime
from unittest.mock import Mock from unittest.mock import Mock
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from yalexs.pubnub_async import AugustPubNub from yalexs.pubnub_async import AugustPubNub
from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME
from homeassistant.components.lock import ( from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN, DOMAIN as LOCK_DOMAIN,
STATE_JAMMED, STATE_JAMMED,
@ -155,7 +157,9 @@ async def test_one_lock_operation(
async def test_one_lock_operation_pubnub_connected( async def test_one_lock_operation_pubnub_connected(
hass: HomeAssistant, entity_registry: er.EntityRegistry hass: HomeAssistant,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test lock and unlock operations are async when pubnub is connected.""" """Test lock and unlock operations are async when pubnub is connected."""
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
@ -230,6 +234,23 @@ async def test_one_lock_operation_pubnub_connected(
== STATE_UNKNOWN == STATE_UNKNOWN
) )
freezer.tick(INITIAL_LOCK_RESYNC_TIME)
pubnub.message(
pubnub,
Mock(
channel=lock_one.pubsub_channel,
timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000,
message={
"status": "kAugLockState_Unlocked",
},
),
)
await hass.async_block_till_done()
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
async def test_lock_jammed(hass: HomeAssistant) -> None: async def test_lock_jammed(hass: HomeAssistant) -> None:
"""Test lock gets jammed on unlock.""" """Test lock gets jammed on unlock."""