Significantly reduce the number of API calls that the august integration (#31685)

* Significantly reduce the number of API calls that the august integration
makes.

The poll interval for the lock status API is now 15 minutes
instead of every 10 seconds because we can use the activity
API to see changes in lock state.  The interval for the
activity API is 10 seconds which allows for the same
frequency of state monitoring without all the additional API calls.
With four locks, this change results in an ~80% reduction in the number
of API calls.

The result of the lock and unlock APIs now update the lock state instead
of waiting for the next poll.  This change also has the added benefit of
making the UI appear far more responsive.

* Convert to using UTC times
This commit is contained in:
J. Nick Koston 2020-02-11 10:57:26 -06:00 committed by GitHub
parent 81b159f424
commit ecd7ec385d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 96 additions and 6 deletions

View file

@ -15,7 +15,7 @@ from homeassistant.const import (
)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.util import Throttle, dt
_LOGGER = logging.getLogger(__name__)
@ -42,9 +42,22 @@ DEFAULT_ENTITY_NAMESPACE = "august"
# avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
# Limit locks status check to 900 seconds now that
# we get the state from the lock and unlock api calls
# and the lock and unlock activities are now captured
MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES = timedelta(seconds=900)
# Doorbells need to update more frequently than locks
# since we get an image from the doorbell api
MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20)
# Activity needs to be checked more frequently as the
# doorbell motion and rings are included here
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
LOGIN_METHODS = ["phone", "email"]
CONFIG_SCHEMA = vol.Schema(
@ -192,6 +205,7 @@ class AugustData:
self._house_ids.add(device.house_id)
self._doorbell_detail_by_id = {}
self._lock_last_status_update_time_utc_by_id = {}
self._lock_status_by_id = {}
self._lock_detail_by_id = {}
self._door_state_by_id = {}
@ -243,6 +257,7 @@ class AugustData:
self._activities_by_id[device_id] = [
a for a in activities if a.device_id == device_id
]
_LOGGER.debug("Completed retrieving device activities")
def get_doorbell_detail(self, doorbell_id):
@ -250,7 +265,7 @@ class AugustData:
self._update_doorbells()
return self._doorbell_detail_by_id.get(doorbell_id)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
@Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
def _update_doorbells(self):
detail_by_id = {}
@ -275,6 +290,17 @@ class AugustData:
_LOGGER.debug("Completed retrieving doorbell details")
self._doorbell_detail_by_id = detail_by_id
def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
"""Set the lock status and last status update time.
This is used when the lock, unlock apis are called
or newer activity is detected on the activity feed
in order to keep the internal data in sync
"""
self._lock_status_by_id[lock_id] = lock_status
self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
return True
def get_lock_status(self, lock_id):
"""Return status if the door is locked or unlocked.
@ -300,13 +326,15 @@ class AugustData:
self._update_locks_status()
self._update_locks_detail()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
@Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES)
def _update_locks_status(self):
status_by_id = {}
state_by_id = {}
last_status_update_by_id = {}
_LOGGER.debug("Start retrieving lock and door status")
for lock in self._locks:
update_start_time_utc = dt.utcnow()
_LOGGER.debug("Updating lock and door status for %s", lock.device_name)
try:
(
@ -315,6 +343,12 @@ class AugustData:
) = self._api.get_lock_status(
self._access_token, lock.device_id, door_status=True
)
# Since there is a a race condition between calling the
# lock and activity apis, we set the last update time
# BEFORE making the api call since we will compare this
# to activity later we want activity to win over stale lock
# state.
last_status_update_by_id[lock.device_id] = update_start_time_utc
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve lock and door status for %s. %s",
@ -331,6 +365,17 @@ class AugustData:
_LOGGER.debug("Completed retrieving lock and door status")
self._lock_status_by_id = status_by_id
self._door_state_by_id = state_by_id
self._lock_last_status_update_time_utc_by_id = last_status_update_by_id
def get_last_lock_status_update_time_utc(self, lock_id):
"""Return the last time that a lock status update was seen from the august API."""
# Since the activity api is called more frequently than
# the lock api it is possible that the lock has not
# been updated yet
if lock_id not in self._lock_last_status_update_time_utc_by_id:
return dt.utc_from_timestamp(0)
return self._lock_last_status_update_time_utc_by_id[lock_id]
@Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES)
def _update_locks_detail(self):

View file

@ -7,6 +7,7 @@ from august.lock import LockStatus
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.util import dt
from . import DATA_AUGUST
@ -41,11 +42,23 @@ class AugustLock(LockDevice):
def lock(self, **kwargs):
"""Lock the device."""
self._data.lock(self._lock.device_id)
update_start_time_utc = dt.utcnow()
lock_status = self._data.lock(self._lock.device_id)
self._update_lock_status(lock_status, update_start_time_utc)
def unlock(self, **kwargs):
"""Unlock the device."""
self._data.unlock(self._lock.device_id)
update_start_time_utc = dt.utcnow()
lock_status = self._data.unlock(self._lock.device_id)
self._update_lock_status(lock_status, update_start_time_utc)
def _update_lock_status(self, lock_status, update_start_time_utc):
if self._lock_status != lock_status:
self._lock_status = lock_status
self._data.update_lock_status(
self._lock.device_id, lock_status, update_start_time_utc
)
self.schedule_update_ha_state()
def update(self):
"""Get the latest state of the sensor."""
@ -60,6 +73,38 @@ class AugustLock(LockDevice):
if activity is not None:
self._changed_by = activity.operated_by
self._sync_lock_activity(activity)
def _sync_lock_activity(self, activity):
"""Check the activity for the latest lock/unlock activity (events).
We use this to determine the lock state in between calls to the lock
api as we update it more frequently
"""
last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc(
self._lock.device_id
)
activity_end_time_utc = dt.as_utc(activity.activity_end_time)
if activity_end_time_utc > last_lock_status_update_time_utc:
_LOGGER.debug(
"The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]",
self.name,
activity.action,
activity_end_time_utc,
last_lock_status_update_time_utc,
)
activity_start_time_utc = dt.as_utc(activity.activity_start_time)
if activity.action == "lock" or activity.action == "onetouchlock":
self._update_lock_status(LockStatus.LOCKED, activity_start_time_utc)
elif activity.action == "unlock":
self._update_lock_status(LockStatus.UNLOCKED, activity_start_time_utc)
else:
_LOGGER.info(
"Unhandled lock activity action %s for %s",
activity.action,
self.name,
)
@property
def name(self):