Convert august to async so a token refresh lock can be used (#31848)
* Convert august to async so a token refresh lock can be used * Update comment since we now have a lock * Do not mock the lock * Address review items
This commit is contained in:
parent
fb8cbc2e93
commit
f6d9e6b6c5
5 changed files with 119 additions and 93 deletions
|
@ -1,5 +1,7 @@
|
|||
"""Support for August devices."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from august.api import Api
|
||||
|
@ -78,7 +80,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
|
||||
|
||||
|
||||
def request_configuration(hass, config, api, authenticator):
|
||||
def request_configuration(hass, config, api, authenticator, token_refresh_lock):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = hass.components.configurator
|
||||
|
||||
|
@ -92,7 +94,7 @@ def request_configuration(hass, config, api, authenticator):
|
|||
_CONFIGURING[DOMAIN], "Invalid verification code"
|
||||
)
|
||||
elif result == ValidationResult.VALIDATED:
|
||||
setup_august(hass, config, api, authenticator)
|
||||
setup_august(hass, config, api, authenticator, token_refresh_lock)
|
||||
|
||||
if DOMAIN not in _CONFIGURING:
|
||||
authenticator.send_verification_code()
|
||||
|
@ -113,7 +115,7 @@ def request_configuration(hass, config, api, authenticator):
|
|||
)
|
||||
|
||||
|
||||
def setup_august(hass, config, api, authenticator):
|
||||
def setup_august(hass, config, api, authenticator, token_refresh_lock):
|
||||
"""Set up the August component."""
|
||||
|
||||
authentication = None
|
||||
|
@ -136,7 +138,9 @@ def setup_august(hass, config, api, authenticator):
|
|||
if DOMAIN in _CONFIGURING:
|
||||
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
|
||||
|
||||
hass.data[DATA_AUGUST] = AugustData(hass, api, authentication, authenticator)
|
||||
hass.data[DATA_AUGUST] = AugustData(
|
||||
hass, api, authentication, authenticator, token_refresh_lock
|
||||
)
|
||||
|
||||
for component in AUGUST_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
@ -146,13 +150,13 @@ def setup_august(hass, config, api, authenticator):
|
|||
_LOGGER.error("Invalid password provided")
|
||||
return False
|
||||
if state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
request_configuration(hass, config, api, authenticator)
|
||||
request_configuration(hass, config, api, authenticator, token_refresh_lock)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the August component."""
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
@ -184,16 +188,20 @@ def setup(hass, config):
|
|||
|
||||
_LOGGER.debug("August HTTP session closed.")
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
|
||||
_LOGGER.debug("Registered for Home Assistant stop event")
|
||||
|
||||
return setup_august(hass, config, api, authenticator)
|
||||
token_refresh_lock = asyncio.Lock()
|
||||
|
||||
return await hass.async_add_executor_job(
|
||||
setup_august, hass, config, api, authenticator, token_refresh_lock
|
||||
)
|
||||
|
||||
|
||||
class AugustData:
|
||||
"""August data object."""
|
||||
|
||||
def __init__(self, hass, api, authentication, authenticator):
|
||||
def __init__(self, hass, api, authentication, authenticator, token_refresh_lock):
|
||||
"""Init August data object."""
|
||||
self._hass = hass
|
||||
self._api = api
|
||||
|
@ -201,6 +209,7 @@ class AugustData:
|
|||
self._access_token = authentication.access_token
|
||||
self._access_token_expires = authentication.access_token_expires
|
||||
|
||||
self._token_refresh_lock = token_refresh_lock
|
||||
self._doorbells = self._api.get_doorbells(self._access_token) or []
|
||||
self._locks = self._api.get_operable_locks(self._access_token) or []
|
||||
self._house_ids = set()
|
||||
|
@ -230,50 +239,48 @@ class AugustData:
|
|||
"""Return a list of locks."""
|
||||
return self._locks
|
||||
|
||||
def _refresh_access_token_if_needed(self):
|
||||
async def _async_refresh_access_token_if_needed(self):
|
||||
"""Refresh the august access token if needed."""
|
||||
|
||||
if self._authenticator.should_refresh():
|
||||
refreshed_authentication = self._authenticator.refresh_access_token(
|
||||
force=False
|
||||
)
|
||||
_LOGGER.info(
|
||||
"Refreshed august access token. The old token expired at %s, and the new token expires at %s",
|
||||
self._access_token_expires,
|
||||
refreshed_authentication.access_token_expires,
|
||||
)
|
||||
self._access_token = refreshed_authentication.access_token
|
||||
self._access_token_expires = refreshed_authentication.access_token_expires
|
||||
async with self._token_refresh_lock:
|
||||
await self._hass.async_add_executor_job(self._refresh_access_token)
|
||||
|
||||
def get_device_activities(self, device_id, *activity_types):
|
||||
def _refresh_access_token(self):
|
||||
refreshed_authentication = self._authenticator.refresh_access_token(force=False)
|
||||
_LOGGER.info(
|
||||
"Refreshed august access token. The old token expired at %s, and the new token expires at %s",
|
||||
self._access_token_expires,
|
||||
refreshed_authentication.access_token_expires,
|
||||
)
|
||||
self._access_token = refreshed_authentication.access_token
|
||||
self._access_token_expires = refreshed_authentication.access_token_expires
|
||||
|
||||
async def async_get_device_activities(self, device_id, *activity_types):
|
||||
"""Return a list of activities."""
|
||||
_LOGGER.debug("Getting device activities")
|
||||
self._update_device_activities()
|
||||
_LOGGER.debug("Getting device activities for %s", device_id)
|
||||
await self._async_update_device_activities()
|
||||
|
||||
activities = self._activities_by_id.get(device_id, [])
|
||||
if activity_types:
|
||||
return [a for a in activities if a.activity_type in activity_types]
|
||||
return activities
|
||||
|
||||
def get_latest_device_activity(self, device_id, *activity_types):
|
||||
async def async_get_latest_device_activity(self, device_id, *activity_types):
|
||||
"""Return latest activity."""
|
||||
activities = self.get_device_activities(device_id, *activity_types)
|
||||
activities = await self.async_get_device_activities(device_id, *activity_types)
|
||||
return next(iter(activities or []), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
"""Update data object with latest from August API."""
|
||||
|
||||
# This is the only place we refresh the api token
|
||||
# in order to avoid multiple threads from doing it at the same time
|
||||
# since there will only be one activity refresh at a time
|
||||
#
|
||||
# In the future when this module is converted to async we should
|
||||
# use a lock to prevent all api calls while the token
|
||||
# is being refreshed as this is a better solution
|
||||
#
|
||||
self._refresh_access_token_if_needed()
|
||||
await self._async_refresh_access_token_if_needed()
|
||||
return await self._hass.async_add_executor_job(
|
||||
partial(self._update_device_activities, limit=ACTIVITY_FETCH_LIMIT)
|
||||
)
|
||||
|
||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
_LOGGER.debug("Start retrieving device activities")
|
||||
for house_id in self.house_ids:
|
||||
_LOGGER.debug("Updating device activity for house id %s", house_id)
|
||||
|
@ -290,12 +297,15 @@ class AugustData:
|
|||
|
||||
_LOGGER.debug("Completed retrieving device activities")
|
||||
|
||||
def get_doorbell_detail(self, doorbell_id):
|
||||
async def async_get_doorbell_detail(self, doorbell_id):
|
||||
"""Return doorbell detail."""
|
||||
self._update_doorbells()
|
||||
await self._async_update_doorbells()
|
||||
return self._doorbell_detail_by_id.get(doorbell_id)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
|
||||
async def _async_update_doorbells(self):
|
||||
await self._hass.async_add_executor_job(self._update_doorbells)
|
||||
|
||||
def _update_doorbells(self):
|
||||
detail_by_id = {}
|
||||
|
||||
|
@ -341,32 +351,35 @@ class AugustData:
|
|||
self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
|
||||
return True
|
||||
|
||||
def get_lock_status(self, lock_id):
|
||||
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.
|
||||
"""
|
||||
self._update_locks()
|
||||
await self._async_update_locks()
|
||||
return self._lock_status_by_id.get(lock_id)
|
||||
|
||||
def get_lock_detail(self, lock_id):
|
||||
async def async_get_lock_detail(self, lock_id):
|
||||
"""Return lock detail."""
|
||||
self._update_locks()
|
||||
await self._async_update_locks()
|
||||
return self._lock_detail_by_id.get(lock_id)
|
||||
|
||||
def get_door_state(self, lock_id):
|
||||
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.
|
||||
"""
|
||||
self._update_locks_status()
|
||||
await self._async_update_locks_status()
|
||||
return self._door_state_by_id.get(lock_id)
|
||||
|
||||
def _update_locks(self):
|
||||
self._update_locks_status()
|
||||
self._update_locks_detail()
|
||||
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 = {}
|
||||
|
@ -431,6 +444,9 @@ class AugustData:
|
|||
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)
|
||||
|
||||
def _update_locks_detail(self):
|
||||
detail_by_id = {}
|
||||
|
||||
|
|
|
@ -15,35 +15,39 @@ _LOGGER = logging.getLogger(__name__)
|
|||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
|
||||
def _retrieve_door_state(data, lock):
|
||||
async def _async_retrieve_door_state(data, lock):
|
||||
"""Get the latest state of the DoorSense sensor."""
|
||||
return data.get_door_state(lock.device_id)
|
||||
return await data.async_get_door_state(lock.device_id)
|
||||
|
||||
|
||||
def _retrieve_online_state(data, doorbell):
|
||||
async def _async_retrieve_online_state(data, doorbell):
|
||||
"""Get the latest state of the sensor."""
|
||||
detail = data.get_doorbell_detail(doorbell.device_id)
|
||||
detail = await data.async_get_doorbell_detail(doorbell.device_id)
|
||||
if detail is None:
|
||||
return None
|
||||
|
||||
return detail.is_online
|
||||
|
||||
|
||||
def _retrieve_motion_state(data, doorbell):
|
||||
async def _async_retrieve_motion_state(data, doorbell):
|
||||
|
||||
return _activity_time_based_state(
|
||||
return await _async_activity_time_based_state(
|
||||
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
|
||||
)
|
||||
|
||||
|
||||
def _retrieve_ding_state(data, doorbell):
|
||||
async def _async_retrieve_ding_state(data, doorbell):
|
||||
|
||||
return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING])
|
||||
return await _async_activity_time_based_state(
|
||||
data, doorbell, [ActivityType.DOORBELL_DING]
|
||||
)
|
||||
|
||||
|
||||
def _activity_time_based_state(data, doorbell, activity_types):
|
||||
async def _async_activity_time_based_state(data, doorbell, activity_types):
|
||||
"""Get the latest state of the sensor."""
|
||||
latest = data.get_latest_device_activity(doorbell.device_id, *activity_types)
|
||||
latest = await data.async_get_latest_device_activity(
|
||||
doorbell.device_id, *activity_types
|
||||
)
|
||||
|
||||
if latest is not None:
|
||||
start = latest.activity_start_time
|
||||
|
@ -52,25 +56,25 @@ def _activity_time_based_state(data, doorbell, activity_types):
|
|||
return None
|
||||
|
||||
|
||||
# Sensor types: Name, device_class, state_provider
|
||||
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _retrieve_door_state]}
|
||||
# Sensor types: Name, device_class, async_state_provider
|
||||
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}
|
||||
|
||||
SENSOR_TYPES_DOORBELL = {
|
||||
"doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state],
|
||||
"doorbell_motion": ["Motion", "motion", _retrieve_motion_state],
|
||||
"doorbell_online": ["Online", "connectivity", _retrieve_online_state],
|
||||
"doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state],
|
||||
"doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state],
|
||||
"doorbell_online": ["Online", "connectivity", _async_retrieve_online_state],
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the August binary sensors."""
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
|
||||
for door in data.locks:
|
||||
for sensor_type in SENSOR_TYPES_DOOR:
|
||||
state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
|
||||
if state_provider(data, door) is LockDoorStatus.UNKNOWN:
|
||||
async_state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
|
||||
if await async_state_provider(data, door) is LockDoorStatus.UNKNOWN:
|
||||
_LOGGER.debug(
|
||||
"Not adding sensor class %s for lock %s ",
|
||||
SENSOR_TYPES_DOOR[sensor_type][1],
|
||||
|
@ -94,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
)
|
||||
devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
|
||||
|
||||
add_entities(devices, True)
|
||||
async_add_entities(devices, True)
|
||||
|
||||
|
||||
class AugustDoorBinarySensor(BinarySensorDevice):
|
||||
|
@ -130,15 +134,15 @@ class AugustDoorBinarySensor(BinarySensorDevice):
|
|||
self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][0]
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._door)
|
||||
async def async_update(self):
|
||||
"""Get the latest state of the sensor and update activity."""
|
||||
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
|
||||
self._state = await async_state_provider(self._data, self._door)
|
||||
self._available = self._state is not None
|
||||
|
||||
self._state = self._state == LockDoorStatus.OPEN
|
||||
|
||||
door_activity = self._data.get_latest_device_activity(
|
||||
door_activity = await self._data.async_get_latest_device_activity(
|
||||
self._door.device_id, ActivityType.DOOR_OPERATION
|
||||
)
|
||||
|
||||
|
@ -226,10 +230,10 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
|
|||
self._doorbell.device_name, SENSOR_TYPES_DOORBELL[self._sensor_type][0]
|
||||
)
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
|
||||
self._state = state_provider(self._data, self._doorbell)
|
||||
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
|
||||
self._state = await async_state_provider(self._data, self._doorbell)
|
||||
self._available = self._doorbell.is_online
|
||||
|
||||
@property
|
||||
|
|
|
@ -10,7 +10,7 @@ from . import DATA_AUGUST, DEFAULT_TIMEOUT
|
|||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up August cameras."""
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
|
@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
for doorbell in data.doorbells:
|
||||
devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT))
|
||||
|
||||
add_entities(devices, True)
|
||||
async_add_entities(devices, True)
|
||||
|
||||
|
||||
class AugustCamera(Camera):
|
||||
|
@ -58,9 +58,9 @@ class AugustCamera(Camera):
|
|||
"""Return the camera model."""
|
||||
return "Doorbell"
|
||||
|
||||
def camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
latest = self._data.get_doorbell_detail(self._doorbell.device_id)
|
||||
latest = await self._data.async_get_doorbell_detail(self._doorbell.device_id)
|
||||
|
||||
if self._image_url is not latest.image_url:
|
||||
self._image_url = latest.image_url
|
||||
|
|
|
@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up August locks."""
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
|
@ -25,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
_LOGGER.debug("Adding lock for %s", lock.device_name)
|
||||
devices.append(AugustLock(data, lock))
|
||||
|
||||
add_entities(devices, True)
|
||||
async_add_entities(devices, True)
|
||||
|
||||
|
||||
class AugustLock(LockDevice):
|
||||
|
@ -40,16 +40,20 @@ class AugustLock(LockDevice):
|
|||
self._changed_by = None
|
||||
self._available = False
|
||||
|
||||
def lock(self, **kwargs):
|
||||
async def async_lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
update_start_time_utc = dt.utcnow()
|
||||
lock_status = self._data.lock(self._lock.device_id)
|
||||
lock_status = await self.hass.async_add_executor_job(
|
||||
self._data.lock, self._lock.device_id
|
||||
)
|
||||
self._update_lock_status(lock_status, update_start_time_utc)
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
async def async_unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
update_start_time_utc = dt.utcnow()
|
||||
lock_status = self._data.unlock(self._lock.device_id)
|
||||
lock_status = await self.hass.async_add_executor_job(
|
||||
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):
|
||||
|
@ -60,14 +64,13 @@ class AugustLock(LockDevice):
|
|||
)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
self._lock_status = self._data.get_lock_status(self._lock.device_id)
|
||||
async def async_update(self):
|
||||
"""Get the latest state of the sensor and update activity."""
|
||||
self._lock_status = await self._data.async_get_lock_status(self._lock.device_id)
|
||||
self._available = self._lock_status is not None
|
||||
self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id)
|
||||
|
||||
self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
|
||||
|
||||
lock_activity = self._data.get_latest_device_activity(
|
||||
lock_activity = await self._data.async_get_latest_device_activity(
|
||||
self._lock.device_id, ActivityType.LOCK_OPERATION
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""The tests for the august platform."""
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components import august
|
||||
|
@ -9,21 +10,23 @@ from tests.components.august.mocks import (
|
|||
)
|
||||
|
||||
|
||||
def test__refresh_access_token():
|
||||
async def test__refresh_access_token(hass):
|
||||
"""Set up things to be run when tests are started."""
|
||||
authentication = _mock_august_authentication("original_token", 1234)
|
||||
authenticator = _mock_august_authenticator()
|
||||
token_refresh_lock = asyncio.Lock()
|
||||
|
||||
data = august.AugustData(
|
||||
MagicMock(name="hass"), MagicMock(name="api"), authentication, authenticator
|
||||
hass, MagicMock(name="api"), authentication, authenticator, token_refresh_lock
|
||||
)
|
||||
data._refresh_access_token_if_needed()
|
||||
await data._async_refresh_access_token_if_needed()
|
||||
authenticator.refresh_access_token.assert_not_called()
|
||||
|
||||
authenticator.should_refresh.return_value = 1
|
||||
authenticator.refresh_access_token.return_value = _mock_august_authentication(
|
||||
"new_token", 5678
|
||||
)
|
||||
data._refresh_access_token_if_needed()
|
||||
await data._async_refresh_access_token_if_needed()
|
||||
authenticator.refresh_access_token.assert_called()
|
||||
assert data._access_token == "new_token"
|
||||
assert data._access_token_expires == 5678
|
||||
|
|
Loading…
Add table
Reference in a new issue