Skip to content

Commit

Permalink
Reduce august polling frequency (#114904)
Browse files Browse the repository at this point in the history
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
  • Loading branch information
2 people authored and frenck committed Apr 5, 2024
1 parent 87ffd5a commit c39d6f0
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 21 deletions.
21 changes: 20 additions & 1 deletion homeassistant/components/august/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime
from functools import partial
import logging
from time import monotonic

from aiohttp import ClientError
from yalexs.activity import Activity, ActivityType
Expand All @@ -26,9 +27,11 @@
ACTIVITY_STREAM_FETCH_LIMIT = 10
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)
# 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
Expand Down Expand Up @@ -62,6 +65,7 @@ def __init__(
self.pubnub = pubnub
self._update_debounce: dict[str, Debouncer] = {}
self._update_debounce_jobs: dict[str, HassJob] = {}
self._start_time: float | None = None

@callback
def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None:
Expand All @@ -70,6 +74,7 @@ def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> Non

async def async_setup(self) -> None:
"""Token refresh check and catch up the activity stream."""
self._start_time = monotonic()
update_debounce = self._update_debounce
update_debounce_jobs = self._update_debounce_jobs
for house_id in self._house_ids:
Expand Down Expand Up @@ -140,11 +145,25 @@ def async_schedule_house_id_refresh(self, house_id: str) -> None:

debouncer = self._update_debounce[house_id]
debouncer.async_schedule_call()

# Schedule two updates past the debounce time
# to ensure we catch the case where the activity
# api does not update right away and we need to poll
# it again. Sometimes the lock operator or a doorbell
# 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]
for step in (1, 2):
future_updates.append(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/august/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
# Limit battery, online, and hardware updates to hourly
# in order to reduce the number of api requests and
# 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
# doorbell motion and rings are included here
Expand Down
33 changes: 15 additions & 18 deletions homeassistant/components/august/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,30 @@ def _async_scheduled_refresh(self, now: datetime) -> None:
"""Call the refresh method."""
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
def _async_setup_listeners(self) -> None:
"""Create interval and stop listeners."""
self._async_cancel_update_interval()
self._unsub_interval = async_track_time_interval(
self._hass,
self._async_scheduled_refresh,
self._update_interval,
name="august refresh",
)

@callback
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(
EVENT_HOMEASSISTANT_STOP,
_async_cancel_update_interval,
run_immediately=True,
)
if not self._stop_interval:
self._stop_interval = self._hass.bus.async_listen(
EVENT_HOMEASSISTANT_STOP,
self._async_cancel_update_interval,
run_immediately=True,
)

@callback
def async_unsubscribe_device_id(
Expand All @@ -82,13 +85,7 @@ def async_unsubscribe_device_id(

if self._subscriptions:
return

if self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
if self._stop_interval:
self._stop_interval()
self._stop_interval = None
self._async_cancel_update_interval()

@callback
def async_signal_device_id_update(self, device_id: str) -> None:
Expand Down
23 changes: 22 additions & 1 deletion tests/components/august/test_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from unittest.mock import Mock

from aiohttp import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest
from yalexs.pubnub_async import AugustPubNub

from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME
from homeassistant.components.lock import (
DOMAIN as LOCK_DOMAIN,
STATE_JAMMED,
Expand Down Expand Up @@ -155,7 +157,9 @@ async def test_one_lock_operation(


async def test_one_lock_operation_pubnub_connected(
hass: HomeAssistant, entity_registry: er.EntityRegistry
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test lock and unlock operations are async when pubnub is connected."""
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
Expand Down Expand Up @@ -230,6 +234,23 @@ async def test_one_lock_operation_pubnub_connected(
== 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:
"""Test lock gets jammed on unlock."""
Expand Down

0 comments on commit c39d6f0

Please sign in to comment.