From a2c4b438ea8ff795cfa7854d919d3981ac60de89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Mar 2021 19:35:12 -1000 Subject: [PATCH] Convert august to be push instead of poll (#47544) --- homeassistant/components/august/__init__.py | 149 ++++++++++++----- homeassistant/components/august/activity.py | 157 ++++++++++++------ .../components/august/binary_sensor.py | 99 ++++++----- homeassistant/components/august/camera.py | 6 +- .../components/august/config_flow.py | 2 +- homeassistant/components/august/gateway.py | 4 +- homeassistant/components/august/lock.py | 16 +- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/august/sensor.py | 4 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/august/mocks.py | 60 ++++--- tests/components/august/test_binary_sensor.py | 125 ++++++++++++++ tests/components/august/test_config_flow.py | 2 +- tests/components/august/test_gateway.py | 2 +- tests/components/august/test_init.py | 32 +++- tests/components/august/test_lock.py | 121 ++++++++++++++ .../august/get_activity.bridge_offline.json | 34 ++++ .../august/get_activity.bridge_online.json | 34 ++++ tests/fixtures/august/get_doorbell.json | 2 +- .../get_lock.online_with_doorsense.json | 3 +- 21 files changed, 683 insertions(+), 183 deletions(-) create mode 100644 tests/fixtures/august/get_activity.bridge_offline.json create mode 100644 tests/fixtures/august/get_activity.bridge_online.json diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 73f1cc6a1b5..70c88d02901 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,14 +1,16 @@ """Support for August devices.""" import asyncio -import itertools +from itertools import chain import logging from aiohttp import ClientError, ClientResponseError -from august.exceptions import AugustApiAIOHTTPError +from yalexs.exceptions import AugustApiAIOHTTPError +from yalexs.pubnub_activity import activities_from_pubnub_message +from yalexs.pubnub_async import AugustPubNub, async_create_pubnub from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, HTTP_UNAUTHORIZED -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from .activity import ActivityStream @@ -19,6 +21,13 @@ from .subscriber import AugustSubscriberMixin _LOGGER = logging.getLogger(__name__) +API_CACHED_ATTRS = ( + "door_state", + "door_state_datetime", + "lock_status", + "lock_status_datetime", +) + async def async_setup(hass: HomeAssistant, config: dict): """Set up the August component from YAML.""" @@ -60,6 +69,9 @@ def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + + hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() + unload_ok = all( await asyncio.gather( *[ @@ -114,25 +126,27 @@ class AugustData(AugustSubscriberMixin): self._doorbells_by_id = {} self._locks_by_id = {} self._house_ids = set() + self._pubnub_unsub = None async def async_setup(self): """Async setup of august device data and activities.""" - locks = ( - await self._api.async_get_operable_locks(self._august_gateway.access_token) - or [] - ) - doorbells = ( - await self._api.async_get_doorbells(self._august_gateway.access_token) or [] + token = self._august_gateway.access_token + user_data, locks, doorbells = await asyncio.gather( + self._api.async_get_user(token), + self._api.async_get_operable_locks(token), + self._api.async_get_doorbells(token), ) + if not doorbells: + doorbells = [] + if not locks: + locks = [] self._doorbells_by_id = {device.device_id: device for device in doorbells} self._locks_by_id = {device.device_id: device for device in locks} - self._house_ids = { - device.house_id for device in itertools.chain(locks, doorbells) - } + self._house_ids = {device.house_id for device in chain(locks, doorbells)} await self._async_refresh_device_detail_by_ids( - [device.device_id for device in itertools.chain(locks, doorbells)] + [device.device_id for device in chain(locks, doorbells)] ) # We remove all devices that we are missing @@ -142,10 +156,32 @@ class AugustData(AugustSubscriberMixin): self._remove_inoperative_locks() self._remove_inoperative_doorbells() + pubnub = AugustPubNub() + for device in self._device_detail_by_id.values(): + pubnub.register_device(device) + self.activity_stream = ActivityStream( - self._hass, self._api, self._august_gateway, self._house_ids + self._hass, self._api, self._august_gateway, self._house_ids, pubnub ) await self.activity_stream.async_setup() + pubnub.subscribe(self.async_pubnub_message) + self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub) + + @callback + def async_pubnub_message(self, device_id, date_time, message): + """Process a pubnub message.""" + device = self.get_device_detail(device_id) + activities = activities_from_pubnub_message(device, date_time, message) + if activities: + self.activity_stream.async_process_newer_device_activities(activities) + self.async_signal_device_id_update(device.device_id) + self.activity_stream.async_schedule_house_id_refresh(device.house_id) + + @callback + def async_stop(self): + """Stop the subscriptions.""" + self._pubnub_unsub() + self.activity_stream.async_stop() @property def doorbells(self): @@ -165,27 +201,38 @@ class AugustData(AugustSubscriberMixin): await self._async_refresh_device_detail_by_ids(self._subscriptions.keys()) async def _async_refresh_device_detail_by_ids(self, device_ids_list): - for device_id in device_ids_list: - if device_id in self._locks_by_id: - await self._async_update_device_detail( - self._locks_by_id[device_id], self._api.async_get_lock_detail - ) - # keypads are always attached to locks - if ( - device_id in self._device_detail_by_id - and self._device_detail_by_id[device_id].keypad is not None - ): - keypad = self._device_detail_by_id[device_id].keypad - self._device_detail_by_id[keypad.device_id] = keypad - elif device_id in self._doorbells_by_id: - await self._async_update_device_detail( - self._doorbells_by_id[device_id], - self._api.async_get_doorbell_detail, - ) - _LOGGER.debug( - "async_signal_device_id_update (from detail updates): %s", device_id + await asyncio.gather( + *[ + self._async_refresh_device_detail_by_id(device_id) + for device_id in device_ids_list + ] + ) + + async def _async_refresh_device_detail_by_id(self, device_id): + if device_id in self._locks_by_id: + if self.activity_stream and self.activity_stream.pubnub.connected: + saved_attrs = _save_live_attrs(self._device_detail_by_id[device_id]) + await self._async_update_device_detail( + self._locks_by_id[device_id], self._api.async_get_lock_detail ) - self.async_signal_device_id_update(device_id) + if self.activity_stream and self.activity_stream.pubnub.connected: + _restore_live_attrs(self._device_detail_by_id[device_id], saved_attrs) + # keypads are always attached to locks + if ( + device_id in self._device_detail_by_id + and self._device_detail_by_id[device_id].keypad is not None + ): + keypad = self._device_detail_by_id[device_id].keypad + self._device_detail_by_id[keypad.device_id] = keypad + elif device_id in self._doorbells_by_id: + await self._async_update_device_detail( + self._doorbells_by_id[device_id], + self._api.async_get_doorbell_detail, + ) + _LOGGER.debug( + "async_signal_device_id_update (from detail updates): %s", device_id + ) + self.async_signal_device_id_update(device_id) async def _async_update_device_detail(self, device, api_call): _LOGGER.debug( @@ -213,9 +260,9 @@ class AugustData(AugustSubscriberMixin): def _get_device_name(self, device_id): """Return doorbell or lock name as August has it stored.""" - if self._locks_by_id.get(device_id): + if device_id in self._locks_by_id: return self._locks_by_id[device_id].device_name - if self._doorbells_by_id.get(device_id): + if device_id in self._doorbells_by_id: return self._doorbells_by_id[device_id].device_name async def async_lock(self, device_id): @@ -252,8 +299,7 @@ class AugustData(AugustSubscriberMixin): return ret def _remove_inoperative_doorbells(self): - doorbells = list(self.doorbells) - for doorbell in doorbells: + for doorbell in list(self.doorbells): device_id = doorbell.device_id doorbell_is_operative = False doorbell_detail = self._device_detail_by_id.get(device_id) @@ -273,9 +319,7 @@ class AugustData(AugustSubscriberMixin): # Remove non-operative locks as there must # be a bridge (August Connect) for them to # be usable - locks = list(self.locks) - - for lock in locks: + for lock in list(self.locks): device_id = lock.device_id lock_is_operative = False lock_detail = self._device_detail_by_id.get(device_id) @@ -289,14 +333,27 @@ class AugustData(AugustSubscriberMixin): "The lock %s could not be setup because it does not have a bridge (Connect)", lock.device_name, ) - elif not lock_detail.bridge.operative: - _LOGGER.info( - "The lock %s could not be setup because the bridge (Connect) is not operative", - lock.device_name, - ) + # Bridge may come back online later so we still add the device since we will + # have a pubnub subscription to tell use when it recovers else: lock_is_operative = True if not lock_is_operative: del self._locks_by_id[device_id] del self._device_detail_by_id[device_id] + + +def _save_live_attrs(lock_detail): + """Store the attributes that the lock detail api may have an invalid cache for. + + Since we are connected to pubnub we may have more current data + then the api so we want to restore the most current data after + updating battery state etc. + """ + return {attr: getattr(lock_detail, attr) for attr in API_CACHED_ATTRS} + + +def _restore_live_attrs(lock_detail, attrs): + """Restore the non-cache attributes after a cached update.""" + for attr, value in attrs.items(): + setattr(lock_detail, attr, value) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index d972fbf5281..18f390b4f8f 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -1,8 +1,12 @@ """Consume the august activity stream.""" +import asyncio import logging from aiohttp import ClientError +from homeassistant.core import callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow from .const import ACTIVITY_UPDATE_INTERVAL @@ -17,27 +21,58 @@ ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 class ActivityStream(AugustSubscriberMixin): """August activity stream handler.""" - def __init__(self, hass, api, august_gateway, house_ids): + def __init__(self, hass, api, august_gateway, house_ids, pubnub): """Init August activity stream object.""" super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) self._hass = hass + self._schedule_updates = {} self._august_gateway = august_gateway self._api = api self._house_ids = house_ids - self._latest_activities_by_id_type = {} + self._latest_activities = {} self._last_update_time = None self._abort_async_track_time_interval = None + self.pubnub = pubnub + self._update_debounce = {} async def async_setup(self): """Token refresh check and catch up the activity stream.""" - await self._async_refresh(utcnow) + for house_id in self._house_ids: + self._update_debounce[house_id] = self._async_create_debouncer(house_id) + + await self._async_refresh(utcnow()) + + @callback + def _async_create_debouncer(self, house_id): + """Create a debouncer for the house id.""" + + async def _async_update_house_id(): + await self._async_update_house_id(house_id) + + return Debouncer( + self._hass, + _LOGGER, + cooldown=ACTIVITY_UPDATE_INTERVAL.seconds, + immediate=True, + function=_async_update_house_id, + ) + + @callback + def async_stop(self): + """Cleanup any debounces.""" + for debouncer in self._update_debounce.values(): + debouncer.async_cancel() + for house_id in self._schedule_updates: + if self._schedule_updates[house_id] is not None: + self._schedule_updates[house_id]() + self._schedule_updates[house_id] = None def get_latest_device_activity(self, device_id, activity_types): """Return latest activity that is one of the acitivty_types.""" - if device_id not in self._latest_activities_by_id_type: + if device_id not in self._latest_activities: return None - latest_device_activities = self._latest_activities_by_id_type[device_id] + latest_device_activities = self._latest_activities[device_id] latest_activity = None for activity_type in activity_types: @@ -54,62 +89,86 @@ class ActivityStream(AugustSubscriberMixin): async def _async_refresh(self, time): """Update the activity stream from August.""" - # This is the only place we refresh the api token await self._august_gateway.async_refresh_access_token_if_needed() + if self.pubnub.connected: + _LOGGER.debug("Skipping update because pubnub is connected") + return await self._async_update_device_activities(time) async def _async_update_device_activities(self, time): _LOGGER.debug("Start retrieving device activities") - - limit = ( - ACTIVITY_STREAM_FETCH_LIMIT - if self._last_update_time - else ACTIVITY_CATCH_UP_FETCH_LIMIT + await asyncio.gather( + *[ + self._update_debounce[house_id].async_call() + for house_id in self._house_ids + ] ) - - for house_id in self._house_ids: - _LOGGER.debug("Updating device activity for house id %s", house_id) - try: - activities = await self._api.async_get_house_activities( - self._august_gateway.access_token, house_id, limit=limit - ) - except ClientError as ex: - _LOGGER.error( - "Request error trying to retrieve activity for house id %s: %s", - house_id, - ex, - ) - # Make sure we process the next house if one of them fails - continue - - _LOGGER.debug( - "Completed retrieving device activities for house id %s", house_id - ) - - updated_device_ids = self._process_newer_device_activities(activities) - - if updated_device_ids: - for device_id in updated_device_ids: - _LOGGER.debug( - "async_signal_device_id_update (from activity stream): %s", - device_id, - ) - self.async_signal_device_id_update(device_id) - self._last_update_time = time - def _process_newer_device_activities(self, activities): + @callback + def async_schedule_house_id_refresh(self, house_id): + """Update for a house activities now and once in the future.""" + if self._schedule_updates.get(house_id): + self._schedule_updates[house_id]() + self._schedule_updates[house_id] = None + + async def _update_house_activities(_): + await self._update_debounce[house_id].async_call() + + self._hass.async_create_task(self._update_debounce[house_id].async_call()) + # Schedule an update past the debounce to ensure + # we catch the case where the lock operator is + # not updated or the lock failed + self._schedule_updates[house_id] = async_call_later( + self._hass, ACTIVITY_UPDATE_INTERVAL.seconds + 1, _update_house_activities + ) + + async def _async_update_house_id(self, house_id): + """Update device activities for a house.""" + if self._last_update_time: + limit = ACTIVITY_STREAM_FETCH_LIMIT + else: + limit = ACTIVITY_CATCH_UP_FETCH_LIMIT + + _LOGGER.debug("Updating device activity for house id %s", house_id) + try: + activities = await self._api.async_get_house_activities( + self._august_gateway.access_token, house_id, limit=limit + ) + except ClientError as ex: + _LOGGER.error( + "Request error trying to retrieve activity for house id %s: %s", + house_id, + ex, + ) + # Make sure we process the next house if one of them fails + return + + _LOGGER.debug( + "Completed retrieving device activities for house id %s", house_id + ) + + updated_device_ids = self.async_process_newer_device_activities(activities) + + if not updated_device_ids: + return + + for device_id in updated_device_ids: + _LOGGER.debug( + "async_signal_device_id_update (from activity stream): %s", + device_id, + ) + self.async_signal_device_id_update(device_id) + + def async_process_newer_device_activities(self, activities): + """Process activities if they are newer than the last one.""" updated_device_ids = set() for activity in activities: device_id = activity.device_id activity_type = activity.activity_type - - self._latest_activities_by_id_type.setdefault(device_id, {}) - - lastest_activity = self._latest_activities_by_id_type[device_id].get( - activity_type - ) + device_activities = self._latest_activities.setdefault(device_id, {}) + lastest_activity = device_activities.get(activity_type) # Ignore activities that are older than the latest one if ( @@ -118,7 +177,7 @@ class ActivityStream(AugustSubscriberMixin): ): continue - self._latest_activities_by_id_type[device_id][activity_type] = activity + device_activities[activity_type] = activity updated_device_ids.add(device_id) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 226cbf655f9..6dccec57a09 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,9 +2,9 @@ from datetime import datetime, timedelta import logging -from august.activity import ActivityType -from august.lock import LockDoorStatus -from august.util import update_lock_detail_from_activity +from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, ActivityType +from yalexs.lock import LockDoorStatus +from yalexs.util import update_lock_detail_from_activity from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -14,15 +14,15 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.core import callback -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow +from homeassistant.helpers.event import async_call_later -from .const import DATA_AUGUST, DOMAIN +from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN from .entity import AugustEntityMixin _LOGGER = logging.getLogger(__name__) -TIME_TO_DECLARE_DETECTION = timedelta(seconds=60) +TIME_TO_DECLARE_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds) +TIME_TO_RECHECK_DETECTION = timedelta(seconds=ACTIVITY_UPDATE_INTERVAL.seconds * 3) def _retrieve_online_state(data, detail): @@ -35,30 +35,43 @@ def _retrieve_online_state(data, detail): def _retrieve_motion_state(data, detail): - - return _activity_time_based_state( - data, - detail.device_id, - [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING], + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_MOTION} ) + if latest is None: + return False + + return _activity_time_based_state(latest) + def _retrieve_ding_state(data, detail): - - return _activity_time_based_state( - data, detail.device_id, [ActivityType.DOORBELL_DING] + latest = data.activity_stream.get_latest_device_activity( + detail.device_id, {ActivityType.DOORBELL_DING} ) + if latest is None: + return False -def _activity_time_based_state(data, device_id, activity_types): + if ( + data.activity_stream.pubnub.connected + and latest.action == ACTION_DOORBELL_CALL_MISSED + ): + return False + + return _activity_time_based_state(latest) + + +def _activity_time_based_state(latest): """Get the latest state of the sensor.""" - latest = data.activity_stream.get_latest_device_activity(device_id, activity_types) + start = latest.activity_start_time + end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION + return start <= _native_datetime() <= end - if latest is not None: - start = latest.activity_start_time - end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION - return start <= datetime.now() <= end - return None + +def _native_datetime(): + """Return time in the format august uses without timezone.""" + return datetime.now() SENSOR_NAME = 0 @@ -143,12 +156,19 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity): def _update_from_data(self): """Get the latest state of the sensor and update activity.""" door_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.DOOR_OPERATION] + self._device_id, {ActivityType.DOOR_OPERATION} ) if door_activity is not None: update_lock_detail_from_activity(self._detail, door_activity) + bridge_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.BRIDGE_OPERATION} + ) + + if bridge_activity is not None: + update_lock_detail_from_activity(self._detail, bridge_activity) + @property def unique_id(self) -> str: """Get the unique of the door open binary sensor.""" @@ -179,25 +199,30 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Return true if the binary sensor is on.""" return self._state + @property + def _sensor_config(self): + """Return the config for the sensor.""" + return SENSOR_TYPES_DOORBELL[self._sensor_type] + @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS] + return self._sensor_config[SENSOR_DEVICE_CLASS] @property def name(self): """Return the name of the binary sensor.""" - return f"{self._device.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}" + return f"{self._device.device_name} {self._sensor_config[SENSOR_NAME]}" @property def _state_provider(self): """Return the state provider for the binary sensor.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_PROVIDER] + return self._sensor_config[SENSOR_STATE_PROVIDER] @property def _is_time_based(self): """Return true of false if the sensor is time based.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_IS_TIME_BASED] + return self._sensor_config[SENSOR_STATE_IS_TIME_BASED] @callback def _update_from_data(self): @@ -228,17 +253,20 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): """Timer callback for sensor update.""" self._check_for_off_update_listener = None self._update_from_data() + if not self._state: + self.async_write_ha_state() - self._check_for_off_update_listener = async_track_point_in_utc_time( - self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION + self._check_for_off_update_listener = async_call_later( + self.hass, TIME_TO_RECHECK_DETECTION.seconds, _scheduled_update ) def _cancel_any_pending_updates(self): """Cancel any updates to recheck a sensor to see if it is ready to turn off.""" - if self._check_for_off_update_listener: - _LOGGER.debug("%s: canceled pending update", self.entity_id) - self._check_for_off_update_listener() - self._check_for_off_update_listener = None + if not self._check_for_off_update_listener: + return + _LOGGER.debug("%s: canceled pending update", self.entity_id) + self._check_for_off_update_listener() + self._check_for_off_update_listener = None async def async_added_to_hass(self): """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed.""" @@ -248,7 +276,4 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): @property def unique_id(self) -> str: """Get the unique id of the doorbell sensor.""" - return ( - f"{self._device_id}_" - f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}" - ) + return f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}" diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 4037489fa22..e002e0b2517 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -1,7 +1,7 @@ """Support for August doorbell camera.""" -from august.activity import ActivityType -from august.util import update_doorbell_image_from_activity +from yalexs.activity import ActivityType +from yalexs.util import update_doorbell_image_from_activity from homeassistant.components.camera import Camera from homeassistant.core import callback @@ -63,7 +63,7 @@ class AugustCamera(AugustEntityMixin, Camera): def _update_from_data(self): """Get the latest state of the sensor.""" doorbell_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.DOORBELL_MOTION] + self._device_id, {ActivityType.DOORBELL_MOTION} ) if doorbell_activity is not None: diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index 29bb41947a0..7176592e37e 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -1,8 +1,8 @@ """Config flow for August integration.""" import logging -from august.authenticator import ValidationResult import voluptuous as vol +from yalexs.authenticator import ValidationResult from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index f71541e82fc..5499246a187 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -5,8 +5,8 @@ import logging import os from aiohttp import ClientError, ClientResponseError -from august.api_async import ApiAsync -from august.authenticator_async import AuthenticationState, AuthenticatorAsync +from yalexs.api_async import ApiAsync +from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync from homeassistant.const import ( CONF_PASSWORD, diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 4b4ae906190..59c97190d7f 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,9 +1,9 @@ """Support for August lock.""" import logging -from august.activity import ActivityType -from august.lock import LockStatus -from august.util import update_lock_detail_from_activity +from yalexs.activity import ActivityType +from yalexs.lock import LockStatus +from yalexs.util import update_lock_detail_from_activity from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity from homeassistant.const import ATTR_BATTERY_LEVEL @@ -73,13 +73,21 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): def _update_from_data(self): """Get the latest state of the sensor and update activity.""" lock_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.LOCK_OPERATION] + self._device_id, + {ActivityType.LOCK_OPERATION, ActivityType.LOCK_OPERATION_WITHOUT_OPERATOR}, ) if lock_activity is not None: self._changed_by = lock_activity.operated_by update_lock_detail_from_activity(self._detail, lock_activity) + bridge_activity = self._data.activity_stream.get_latest_device_activity( + self._device_id, {ActivityType.BRIDGE_OPERATION} + ) + + if bridge_activity is not None: + update_lock_detail_from_activity(self._detail, bridge_activity) + self._update_lock_status_from_detail() @property diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 91733b6822e..3a156a189c7 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.25.2"], + "requirements": ["yalexs==1.1.4"], "codeowners": ["@bdraco"], "dhcp": [ {"hostname":"connect","macaddress":"D86162*"}, diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 9dba5bb2766..1841b2cef56 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -1,7 +1,7 @@ """Support for August sensors.""" import logging -from august.activity import ActivityType +from yalexs.activity import ActivityType from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE @@ -154,7 +154,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): def _update_from_data(self): """Get the latest state of the sensor and update activity.""" lock_activity = self._data.activity_stream.get_latest_device_activity( - self._device_id, [ActivityType.LOCK_OPERATION] + self._device_id, {ActivityType.LOCK_OPERATION} ) self._available = True diff --git a/requirements_all.txt b/requirements_all.txt index 0ee7a4edb72..dbaee574184 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1191,9 +1191,6 @@ pushover_complete==1.1.1 # homeassistant.components.rpi_gpio_pwm pwmled==1.6.7 -# homeassistant.components.august -py-august==0.25.2 - # homeassistant.components.canary py-canary==0.5.1 @@ -2347,6 +2344,9 @@ xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm yalesmartalarmclient==0.1.6 +# homeassistant.components.august +yalexs==1.1.4 + # homeassistant.components.yeelight yeelight==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ceeccec6b5..20bd6547b05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,9 +616,6 @@ pure-python-adb[async]==0.3.0.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 -# homeassistant.components.august -py-august==0.25.2 - # homeassistant.components.canary py-canary==0.5.1 @@ -1208,6 +1205,9 @@ xbox-webapi==2.0.8 # homeassistant.components.zestimate xmltodict==0.12.0 +# homeassistant.components.august +yalexs==1.1.4 + # homeassistant.components.yeelight yeelight==0.5.4 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 928b753af52..9a54a708a4f 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -6,21 +6,26 @@ import time # from unittest.mock import AsyncMock from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch -from august.activity import ( +from yalexs.activity import ( + ACTIVITY_ACTIONS_BRIDGE_OPERATION, ACTIVITY_ACTIONS_DOOR_OPERATION, ACTIVITY_ACTIONS_DOORBELL_DING, ACTIVITY_ACTIONS_DOORBELL_MOTION, ACTIVITY_ACTIONS_DOORBELL_VIEW, ACTIVITY_ACTIONS_LOCK_OPERATION, + SOURCE_LOCK_OPERATE, + SOURCE_LOG, + BridgeOperationActivity, DoorbellDingActivity, DoorbellMotionActivity, DoorbellViewActivity, DoorOperationActivity, LockOperationActivity, ) -from august.authenticator import AuthenticationState -from august.doorbell import Doorbell, DoorbellDetail -from august.lock import Lock, LockDetail +from yalexs.authenticator import AuthenticationState +from yalexs.doorbell import Doorbell, DoorbellDetail +from yalexs.lock import Lock, LockDetail +from yalexs.pubnub_async import AugustPubNub from homeassistant.components.august.const import CONF_LOGIN_METHOD, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -48,7 +53,9 @@ def _mock_authenticator(auth_state): @patch("homeassistant.components.august.gateway.ApiAsync") @patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") -async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock): +async def _mock_setup_august( + hass, api_instance, pubnub_mock, authenticate_mock, api_mock +): """Set up august integration.""" authenticate_mock.side_effect = MagicMock( return_value=_mock_august_authentication( @@ -62,16 +69,21 @@ async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock): options={}, ) entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - return True + with patch("homeassistant.components.august.async_create_pubnub"), patch( + "homeassistant.components.august.AugustPubNub", return_value=pubnub_mock + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry async def _create_august_with_devices( - hass, devices, api_call_side_effects=None, activities=None + hass, devices, api_call_side_effects=None, activities=None, pubnub=None ): if api_call_side_effects is None: api_call_side_effects = {} + if pubnub is None: + pubnub = AugustPubNub() device_data = {"doorbells": [], "locks": []} for device in devices: @@ -152,10 +164,12 @@ async def _create_august_with_devices( "unlock_return_activities" ] = unlock_return_activities_side_effect - return await _mock_setup_august_with_api_side_effects(hass, api_call_side_effects) + return await _mock_setup_august_with_api_side_effects( + hass, api_call_side_effects, pubnub + ) -async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects): +async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects, pubnub): api_instance = MagicMock(name="Api") if api_call_side_effects["get_lock_detail"]: @@ -193,11 +207,13 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects): side_effect=api_call_side_effects["unlock_return_activities"] ) - return await _mock_setup_august(hass, api_instance) + api_instance.async_get_user = AsyncMock(return_value={"UserID": "abc"}) + + return await _mock_setup_august(hass, api_instance, pubnub) def _mock_august_authentication(token_text, token_timestamp, state): - authentication = MagicMock(name="august.authentication") + authentication = MagicMock(name="yalexs.authentication") type(authentication).state = PropertyMock(return_value=state) type(authentication).access_token = PropertyMock(return_value=token_text) type(authentication).access_token_expires = PropertyMock( @@ -301,23 +317,25 @@ async def _mock_doorsense_missing_august_lock_detail(hass): def _mock_lock_operation_activity(lock, action, offset): return LockOperationActivity( + SOURCE_LOCK_OPERATE, { "dateTime": (time.time() + offset) * 1000, "deviceID": lock.device_id, "deviceType": "lock", "action": action, - } + }, ) def _mock_door_operation_activity(lock, action, offset): return DoorOperationActivity( + SOURCE_LOCK_OPERATE, { "dateTime": (time.time() + offset) * 1000, "deviceID": lock.device_id, "deviceType": "lock", "action": action, - } + }, ) @@ -327,13 +345,15 @@ def _activity_from_dict(activity_dict): activity_dict["dateTime"] = time.time() * 1000 if action in ACTIVITY_ACTIONS_DOORBELL_DING: - return DoorbellDingActivity(activity_dict) + return DoorbellDingActivity(SOURCE_LOG, activity_dict) if action in ACTIVITY_ACTIONS_DOORBELL_MOTION: - return DoorbellMotionActivity(activity_dict) + return DoorbellMotionActivity(SOURCE_LOG, activity_dict) if action in ACTIVITY_ACTIONS_DOORBELL_VIEW: - return DoorbellViewActivity(activity_dict) + return DoorbellViewActivity(SOURCE_LOG, activity_dict) if action in ACTIVITY_ACTIONS_LOCK_OPERATION: - return LockOperationActivity(activity_dict) + return LockOperationActivity(SOURCE_LOG, activity_dict) if action in ACTIVITY_ACTIONS_DOOR_OPERATION: - return DoorOperationActivity(activity_dict) + return DoorOperationActivity(SOURCE_LOG, activity_dict) + if action in ACTIVITY_ACTIONS_BRIDGE_OPERATION: + return BridgeOperationActivity(SOURCE_LOG, activity_dict) return None diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 0e337813f52..0912b05bec1 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -1,4 +1,9 @@ """The binary_sensor tests for the august platform.""" +import datetime +from unittest.mock import Mock, patch + +from yalexs.pubnub_async import AugustPubNub + from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -9,7 +14,9 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.helpers import device_registry as dr +import homeassistant.util.dt as dt_util +from tests.common import async_fire_time_changed from tests.components.august.mocks import ( _create_august_with_devices, _mock_activities_from_fixture, @@ -52,6 +59,22 @@ async def test_doorsense(hass): assert binary_sensor_online_with_doorsense_name.state == STATE_OFF +async def test_lock_bridge_offline(hass): + """Test creation of a lock with doorsense and bridge that goes offline.""" + lock_one = await _mock_lock_from_fixture( + hass, "get_lock.online_with_doorsense.json" + ) + activities = await _mock_activities_from_fixture( + hass, "get_activity.bridge_offline.json" + ) + await _create_august_with_devices(hass, [lock_one], activities=activities) + + binary_sensor_online_with_doorsense_name = hass.states.get( + "binary_sensor.online_with_doorsense_name_open" + ) + assert binary_sensor_online_with_doorsense_name.state == STATE_UNAVAILABLE + + async def test_create_doorbell(hass): """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") @@ -112,6 +135,108 @@ async def test_create_doorbell_with_motion(hass): "binary_sensor.k98gidt45gul_name_ding" ) assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF + new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) + native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) + with patch( + "homeassistant.components.august.binary_sensor._native_datetime", + return_value=native_time, + ): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() + binary_sensor_k98gidt45gul_name_motion = hass.states.get( + "binary_sensor.k98gidt45gul_name_motion" + ) + assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + + +async def test_doorbell_update_via_pubnub(hass): + """Test creation of a doorbell that can be updated via pubnub.""" + doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") + pubnub = AugustPubNub() + + await _create_august_with_devices(hass, [doorbell_one], pubnub=pubnub) + assert doorbell_one.pubsub_channel == "7c7a6672-59c8-3333-ffff-dcd98705cccc" + + binary_sensor_k98gidt45gul_name_motion = hass.states.get( + "binary_sensor.k98gidt45gul_name_motion" + ) + assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + binary_sensor_k98gidt45gul_name_ding = hass.states.get( + "binary_sensor.k98gidt45gul_name_ding" + ) + assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF + + pubnub.message( + pubnub, + Mock( + channel=doorbell_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={ + "status": "imagecapture", + "data": { + "result": { + "created_at": "2021-03-16T01:07:08.817Z", + "secure_url": "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg", + }, + }, + }, + ), + ) + + await hass.async_block_till_done() + + binary_sensor_k98gidt45gul_name_motion = hass.states.get( + "binary_sensor.k98gidt45gul_name_motion" + ) + assert binary_sensor_k98gidt45gul_name_motion.state == STATE_ON + binary_sensor_k98gidt45gul_name_ding = hass.states.get( + "binary_sensor.k98gidt45gul_name_ding" + ) + assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF + + new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) + native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) + with patch( + "homeassistant.components.august.binary_sensor._native_datetime", + return_value=native_time, + ): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() + + binary_sensor_k98gidt45gul_name_motion = hass.states.get( + "binary_sensor.k98gidt45gul_name_motion" + ) + assert binary_sensor_k98gidt45gul_name_motion.state == STATE_OFF + + pubnub.message( + pubnub, + Mock( + channel=doorbell_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={ + "status": "buttonpush", + }, + ), + ) + await hass.async_block_till_done() + + binary_sensor_k98gidt45gul_name_ding = hass.states.get( + "binary_sensor.k98gidt45gul_name_ding" + ) + assert binary_sensor_k98gidt45gul_name_ding.state == STATE_ON + new_time = dt_util.utcnow() + datetime.timedelta(seconds=40) + native_time = datetime.datetime.now() + datetime.timedelta(seconds=40) + with patch( + "homeassistant.components.august.binary_sensor._native_datetime", + return_value=native_time, + ): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() + + binary_sensor_k98gidt45gul_name_ding = hass.states.get( + "binary_sensor.k98gidt45gul_name_ding" + ) + assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF async def test_doorbell_device_registry(hass): diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 205c5c70689..c87291e0f79 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -1,7 +1,7 @@ """Test the August config flow.""" from unittest.mock import patch -from august.authenticator import ValidationResult +from yalexs.authenticator import ValidationResult from homeassistant import config_entries, setup from homeassistant.components.august.const import ( diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index ced07360008..54a5e9321f2 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,7 +1,7 @@ """The gateway tests for the august platform.""" from unittest.mock import MagicMock, patch -from august.authenticator_common import AuthenticationState +from yalexs.authenticator_common import AuthenticationState from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index d0116d2c586..8b0885f7341 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -3,13 +3,14 @@ import asyncio from unittest.mock import patch from aiohttp import ClientResponseError -from august.authenticator_common import AuthenticationState -from august.exceptions import AugustApiAIOHTTPError +from yalexs.authenticator_common import AuthenticationState +from yalexs.exceptions import AugustApiAIOHTTPError from homeassistant import setup from homeassistant.components.august.const import DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) @@ -46,7 +47,7 @@ async def test_august_is_offline(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "august.authenticator_async.AuthenticatorAsync.async_authenticate", + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", side_effect=asyncio.TimeoutError, ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -152,7 +153,7 @@ async def test_auth_fails(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "august.authenticator_async.AuthenticatorAsync.async_authenticate", + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", side_effect=ClientResponseError(None, None, status=401), ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -178,7 +179,7 @@ async def test_bad_password(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "august.authenticator_async.AuthenticatorAsync.async_authenticate", + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", return_value=_mock_august_authentication( "original_token", 1234, AuthenticationState.BAD_PASSWORD ), @@ -206,7 +207,7 @@ async def test_http_failure(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "august.authenticator_async.AuthenticatorAsync.async_authenticate", + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", side_effect=ClientResponseError(None, None, status=500), ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -230,7 +231,7 @@ async def test_unknown_auth_state(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "august.authenticator_async.AuthenticatorAsync.async_authenticate", + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", return_value=_mock_august_authentication("original_token", 1234, None), ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -256,7 +257,7 @@ async def test_requires_validation_state(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "august.authenticator_async.AuthenticatorAsync.async_authenticate", + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", return_value=_mock_august_authentication( "original_token", 1234, AuthenticationState.REQUIRES_VALIDATION ), @@ -268,3 +269,18 @@ async def test_requires_validation_state(hass): assert len(hass.config_entries.flow.async_progress()) == 1 assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth" + + +async def test_load_unload(hass): + """Config entry can be unloaded.""" + + august_operative_lock = await _mock_operative_august_lock_detail(hass) + august_inoperative_lock = await _mock_inoperative_august_lock_detail(hass) + config_entry = await _create_august_with_devices( + hass, [august_operative_lock, august_inoperative_lock] + ) + + assert config_entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index dadd6de2d4f..5b3c163780f 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -1,15 +1,23 @@ """The lock tests for the august platform.""" +import datetime +from unittest.mock import Mock + +from yalexs.pubnub_async import AugustPubNub + from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK, STATE_LOCKED, + STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNLOCKED, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +import homeassistant.util.dt as dt_util +from tests.common import async_fire_time_changed from tests.components.august.mocks import ( _create_august_with_devices, _mock_activities_from_fixture, @@ -112,3 +120,116 @@ async def test_one_lock_unknown_state(hass): lock_brokenid_name = hass.states.get("lock.brokenid_name") assert lock_brokenid_name.state == STATE_UNKNOWN + + +async def test_lock_bridge_offline(hass): + """Test creation of a lock with doorsense and bridge that goes offline.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture( + hass, "get_activity.bridge_offline.json" + ) + await _create_august_with_devices(hass, [lock_one], activities=activities) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_UNAVAILABLE + + +async def test_lock_bridge_online(hass): + """Test creation of a lock with doorsense and bridge that goes offline.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + + activities = await _mock_activities_from_fixture( + hass, "get_activity.bridge_online.json" + ) + await _create_august_with_devices(hass, [lock_one], activities=activities) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + +async def test_lock_update_via_pubnub(hass): + """Test creation of a lock with doorsense and bridge.""" + lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) + assert lock_one.pubsub_channel == "pubsub" + pubnub = AugustPubNub() + + activities = await _mock_activities_from_fixture(hass, "get_activity.lock.json") + config_entry = await _create_august_with_devices( + hass, [lock_one], activities=activities, pubnub=pubnub + ) + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={ + "status": "kAugLockState_Unlocking", + }, + ), + ) + + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={ + "status": "kAugLockState_Locking", + }, + ), + ) + + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + pubnub.connected = True + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(seconds=30)) + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + # Ensure pubnub status is always preserved + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_LOCKED + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=dt_util.utcnow().timestamp() * 10000000, + message={ + "status": "kAugLockState_Unlocking", + }, + ), + ) + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + + async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) + await hass.async_block_till_done() + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/fixtures/august/get_activity.bridge_offline.json b/tests/fixtures/august/get_activity.bridge_offline.json new file mode 100644 index 00000000000..ed4aaadaf73 --- /dev/null +++ b/tests/fixtures/august/get_activity.bridge_offline.json @@ -0,0 +1,34 @@ +[{ + "entities" : { + "activity" : "mockActivity2", + "house" : "123", + "device" : "online_with_doorsense", + "callingUser" : "mockUserId2", + "otherUser" : "deleted" + }, + "callingUser" : { + "LastName" : "elven princess", + "UserID" : "mockUserId2", + "FirstName" : "Your favorite" + }, + "otherUser" : { + "LastName" : "User", + "UserName" : "deleteduser", + "FirstName" : "Unknown", + "UserID" : "deleted", + "PhoneNo" : "deleted" + }, + "deviceType" : "lock", + "deviceName" : "MockHouseTDoor", + "action" : "associated_bridge_offline", + "dateTime" : 1582007218000, + "info" : { + "remote" : true, + "DateLogActionID" : "ABC+Time" + }, + "deviceID" : "online_with_doorsense", + "house" : { + "houseName" : "MockHouse", + "houseID" : "123" + } +}] diff --git a/tests/fixtures/august/get_activity.bridge_online.json b/tests/fixtures/august/get_activity.bridge_online.json new file mode 100644 index 00000000000..db14f06cfe9 --- /dev/null +++ b/tests/fixtures/august/get_activity.bridge_online.json @@ -0,0 +1,34 @@ +[{ + "entities" : { + "activity" : "mockActivity2", + "house" : "123", + "device" : "online_with_doorsense", + "callingUser" : "mockUserId2", + "otherUser" : "deleted" + }, + "callingUser" : { + "LastName" : "elven princess", + "UserID" : "mockUserId2", + "FirstName" : "Your favorite" + }, + "otherUser" : { + "LastName" : "User", + "UserName" : "deleteduser", + "FirstName" : "Unknown", + "UserID" : "deleted", + "PhoneNo" : "deleted" + }, + "deviceType" : "lock", + "deviceName" : "MockHouseTDoor", + "action" : "associated_bridge_online", + "dateTime" : 1582007218000, + "info" : { + "remote" : true, + "DateLogActionID" : "ABC+Time" + }, + "deviceID" : "online_with_doorsense", + "house" : { + "houseName" : "MockHouse", + "houseID" : "123" + } +}] diff --git a/tests/fixtures/august/get_doorbell.json b/tests/fixtures/august/get_doorbell.json index abe6e37b1e3..fb2cd5780c9 100644 --- a/tests/fixtures/august/get_doorbell.json +++ b/tests/fixtures/august/get_doorbell.json @@ -55,7 +55,7 @@ "reconnect" ], "doorbellID" : "K98GiDT45GUL", - "HouseID" : "3dd2accaea08", + "HouseID" : "mockhouseid1", "telemetry" : { "signal_level" : -56, "date" : "2017-12-10 08:05:12", diff --git a/tests/fixtures/august/get_lock.online_with_doorsense.json b/tests/fixtures/august/get_lock.online_with_doorsense.json index f7376570482..e29614c9e48 100644 --- a/tests/fixtures/august/get_lock.online_with_doorsense.json +++ b/tests/fixtures/august/get_lock.online_with_doorsense.json @@ -13,9 +13,10 @@ "updated" : "2000-00-00T00:00:00.447Z" } }, + "pubsubChannel":"pubsub", "Calibrated" : false, "Created" : "2000-00-00T00:00:00.447Z", - "HouseID" : "123", + "HouseID" : "mockhouseid1", "HouseName" : "Test", "LockID" : "online_with_doorsense", "LockName" : "Online door with doorsense",