Skip to content

Commit

Permalink
Significantly reduce code in august integration (home-assistant#32030)
Browse files Browse the repository at this point in the history
* Significantly reduce code in august integration

* Activity updates can now be processed by py-august
  this allows us to eliminate the activity sync
  code for the door sensors and locks

* Lock and door state can now be consumed from
  the lock detail api which allows us to
  remove the status call apis and reduce
  the number of API calls to august

* Refactor the testing method for locks (part #1)

* Update homeassistant/components/august/binary_sensor.py

Co-Authored-By: Paulus Schoutsen <[email protected]>

* Switch to asynctest instead of unittest for mock.patch

Co-authored-by: Paulus Schoutsen <[email protected]>
  • Loading branch information
bdraco and balloob authored Feb 21, 2020
1 parent a12c4da commit d4075fb
Show file tree
Hide file tree
Showing 17 changed files with 576 additions and 426 deletions.
116 changes: 12 additions & 104 deletions homeassistant/components/august/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, dt
from homeassistant.util import Throttle

_LOGGER = logging.getLogger(__name__)

Expand All @@ -45,11 +45,6 @@
# avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)

# 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)
Expand Down Expand Up @@ -218,16 +213,11 @@ def __init__(self, hass, api, authentication, authenticator, token_refresh_lock)
self._house_ids.add(device.house_id)

self._doorbell_detail_by_id = {}
self._door_last_state_update_time_utc_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 = {}
self._activities_by_id = {}

# We check the locks right away so we can
# remove inoperative ones
self._update_locks_status()
self._update_locks_detail()

self._filter_inoperative_locks()
Expand Down Expand Up @@ -344,8 +334,13 @@ def update_door_state(self, lock_id, door_state, update_start_time_utc):
This is called when newer activity is detected on the activity feed
in order to keep the internal data in sync
"""
self._door_state_by_id[lock_id] = door_state
self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc
# When syncing the door state became available via py-august, this
# function caused to be actively used. It will be again as we will
# update the door state from lock/unlock operations as the august api
# does report the door state on lock/unlock, however py-august does not
# expose this to us yet.
self._lock_detail_by_id[lock_id].door_state = door_state
self._lock_detail_by_id[lock_id].door_state_datetime = update_start_time_utc
return True

def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
Expand All @@ -355,8 +350,8 @@ def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
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
self._lock_detail_by_id[lock_id].lock_status = lock_status
self._lock_detail_by_id[lock_id].lock_status_datetime = update_start_time_utc
return True

def lock_has_doorsense(self, lock_id):
Expand All @@ -367,104 +362,17 @@ def lock_has_doorsense(self, lock_id):
return False
return self._lock_detail_by_id[lock_id].doorsense

async def async_get_lock_status(self, lock_id):
"""Return status if the door is locked or unlocked.
This is status for the lock itself.
"""
await self._async_update_locks()
return self._lock_status_by_id.get(lock_id)

async def async_get_lock_detail(self, lock_id):
"""Return lock detail."""
await self._async_update_locks()
return self._lock_detail_by_id.get(lock_id)
await self._async_update_locks_detail()
return self._lock_detail_by_id[lock_id]

def get_lock_name(self, device_id):
"""Return lock name as August has it stored."""
for lock in self._locks:
if lock.device_id == device_id:
return lock.device_name

async def async_get_door_state(self, lock_id):
"""Return status if the door is open or closed.
This is the status from the door sensor.
"""
await self._async_update_locks_status()
return self._door_state_by_id.get(lock_id)

async def _async_update_locks(self):
await self._async_update_locks_status()
await self._async_update_locks_detail()

@Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES)
async def _async_update_locks_status(self):
await self._hass.async_add_executor_job(self._update_locks_status)

def _update_locks_status(self):
status_by_id = {}
state_by_id = {}
lock_last_status_update_by_id = {}
door_last_state_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:
(
status_by_id[lock.device_id],
state_by_id[lock.device_id],
) = 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/door
# state.
lock_last_status_update_by_id[lock.device_id] = update_start_time_utc
door_last_state_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",
lock.device_name,
ex,
)
status_by_id[lock.device_id] = None
state_by_id[lock.device_id] = None
except Exception:
status_by_id[lock.device_id] = None
state_by_id[lock.device_id] = None
raise

_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._door_last_state_update_time_utc_by_id = door_last_state_update_by_id
self._lock_last_status_update_time_utc_by_id = lock_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]

def get_last_door_state_update_time_utc(self, lock_id):
"""Return the last time that a door 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 door has not
# been updated yet
if lock_id not in self._door_last_state_update_time_utc_by_id:
return dt.utc_from_timestamp(0)

return self._door_last_state_update_time_utc_by_id[lock_id]

@Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES)
async def _async_update_locks_detail(self):
await self._hass.async_add_executor_job(self._update_locks_detail)
Expand Down
100 changes: 21 additions & 79 deletions homeassistant/components/august/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
from datetime import datetime, timedelta
import logging

from august.activity import ACTIVITY_ACTION_STATES, ActivityType
from august.activity import ActivityType
from august.lock import LockDoorStatus
from august.util import update_lock_detail_from_activity

from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.util import dt

from . import DATA_AUGUST

Expand All @@ -15,11 +15,6 @@
SCAN_INTERVAL = timedelta(seconds=5)


async def _async_retrieve_door_state(data, lock):
"""Get the latest state of the DoorSense sensor."""
return await data.async_get_door_state(lock.device_id)


async def _async_retrieve_online_state(data, doorbell):
"""Get the latest state of the sensor."""
detail = await data.async_get_doorbell_detail(doorbell.device_id)
Expand Down Expand Up @@ -61,8 +56,6 @@ async def _async_activity_time_based_state(data, doorbell, activity_types):
SENSOR_STATE_PROVIDER = 2

# sensor_type: [name, device_class, async_state_provider]
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}

SENSOR_TYPES_DOORBELL = {
"doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state],
"doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state],
Expand All @@ -76,21 +69,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
devices = []

for door in data.locks:
for sensor_type in SENSOR_TYPES_DOOR:
if not data.lock_has_doorsense(door.device_id):
_LOGGER.debug(
"Not adding sensor class %s for lock %s ",
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
continue

if not data.lock_has_doorsense(door.device_id):
_LOGGER.debug(
"Adding sensor class %s for %s",
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
"Not adding sensor class door for lock %s ", door.device_name,
)
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
continue

_LOGGER.debug(
"Adding sensor class door for %s", door.device_name,
)
devices.append(AugustDoorBinarySensor(data, "door_open", door))

for doorbell in data.doorbells:
for sensor_type in SENSOR_TYPES_DOORBELL:
Expand Down Expand Up @@ -127,81 +115,35 @@ def is_on(self):

@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS]
"""Return the class of this device."""
return "door"

@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(
self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME]
)
return "{} Open".format(self._door.device_name)

async def async_update(self):
"""Get the latest state of the sensor and update activity."""
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][
SENSOR_STATE_PROVIDER
]
lock_door_state = await async_state_provider(self._data, self._door)
self._available = (
lock_door_state is not None and lock_door_state != LockDoorStatus.UNKNOWN
)
self._state = lock_door_state == LockDoorStatus.OPEN

door_activity = await self._data.async_get_latest_device_activity(
self._door.device_id, ActivityType.DOOR_OPERATION
)
detail = await self._data.async_get_lock_detail(self._door.device_id)

if door_activity is not None:
self._sync_door_activity(door_activity)

def _update_door_state(self, door_state, update_start_time):
new_state = door_state == LockDoorStatus.OPEN
if self._state != new_state:
self._state = new_state
self._data.update_door_state(
self._door.device_id, door_state, update_start_time
)

def _sync_door_activity(self, door_activity):
"""Check the activity for the latest door open/close activity (events).
update_lock_detail_from_activity(detail, door_activity)

We use this to determine the door state in between calls to the lock
api as we update it more frequently
"""
last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc(
self._door.device_id
)
activity_end_time_utc = dt.as_utc(door_activity.activity_end_time)
lock_door_state = None
if detail is not None:
lock_door_state = detail.door_state

if activity_end_time_utc > last_door_state_update_time_utc:
_LOGGER.debug(
"The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]",
self.name,
door_activity.action,
activity_end_time_utc,
last_door_state_update_time_utc,
)
activity_start_time_utc = dt.as_utc(door_activity.activity_start_time)
if door_activity.action in ACTIVITY_ACTION_STATES:
self._update_door_state(
ACTIVITY_ACTION_STATES[door_activity.action],
activity_start_time_utc,
)
else:
_LOGGER.info(
"Unhandled door activity action %s for %s",
door_activity.action,
self.name,
)
self._available = lock_door_state != LockDoorStatus.UNKNOWN
self._state = lock_door_state == LockDoorStatus.OPEN

@property
def unique_id(self) -> str:
"""Get the unique of the door open binary sensor."""
return "{:s}_{:s}".format(
self._door.device_id,
SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME].lower(),
)
return f"{self._door.device_id}_open"


class AugustDoorbellBinarySensor(BinarySensorDevice):
Expand Down
Loading

0 comments on commit d4075fb

Please sign in to comment.