diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index c7b81646b42..29a268f4dd7 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -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 = {} diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 10b3e98bb2e..07cc2508ce8 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -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 diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index aa23d9b7874..afc22716421 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -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 diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 4e9f5191b2c..1e64ef59944 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -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 ) diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 97d7d4e613b..77e6ca41b4e 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -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