diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index a52df5e361c..a646ee2bad5 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -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): diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index d336e21653b..908e20e68ca 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -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):