Convert august to be push instead of poll (#47544)

This commit is contained in:
J. Nick Koston 2021-03-21 19:35:12 -10:00 committed by GitHub
parent 8e4c0e3ff7
commit a2c4b438ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 683 additions and 183 deletions

View file

@ -1,14 +1,16 @@
"""Support for August devices.""" """Support for August devices."""
import asyncio import asyncio
import itertools from itertools import chain
import logging import logging
from aiohttp import ClientError, ClientResponseError 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.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, HTTP_UNAUTHORIZED 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 homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from .activity import ActivityStream from .activity import ActivityStream
@ -19,6 +21,13 @@ from .subscriber import AugustSubscriberMixin
_LOGGER = logging.getLogger(__name__) _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): async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the August component from YAML.""" """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): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry.""" """Unload a config entry."""
hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop()
unload_ok = all( unload_ok = all(
await asyncio.gather( await asyncio.gather(
*[ *[
@ -114,25 +126,27 @@ class AugustData(AugustSubscriberMixin):
self._doorbells_by_id = {} self._doorbells_by_id = {}
self._locks_by_id = {} self._locks_by_id = {}
self._house_ids = set() self._house_ids = set()
self._pubnub_unsub = None
async def async_setup(self): async def async_setup(self):
"""Async setup of august device data and activities.""" """Async setup of august device data and activities."""
locks = ( token = self._august_gateway.access_token
await self._api.async_get_operable_locks(self._august_gateway.access_token) user_data, locks, doorbells = await asyncio.gather(
or [] self._api.async_get_user(token),
) self._api.async_get_operable_locks(token),
doorbells = ( self._api.async_get_doorbells(token),
await self._api.async_get_doorbells(self._august_gateway.access_token) or []
) )
if not doorbells:
doorbells = []
if not locks:
locks = []
self._doorbells_by_id = {device.device_id: device for device in doorbells} 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._locks_by_id = {device.device_id: device for device in locks}
self._house_ids = { self._house_ids = {device.house_id for device in chain(locks, doorbells)}
device.house_id for device in itertools.chain(locks, doorbells)
}
await self._async_refresh_device_detail_by_ids( 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 # We remove all devices that we are missing
@ -142,10 +156,32 @@ class AugustData(AugustSubscriberMixin):
self._remove_inoperative_locks() self._remove_inoperative_locks()
self._remove_inoperative_doorbells() self._remove_inoperative_doorbells()
pubnub = AugustPubNub()
for device in self._device_detail_by_id.values():
pubnub.register_device(device)
self.activity_stream = ActivityStream( 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() 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 @property
def doorbells(self): def doorbells(self):
@ -165,11 +201,22 @@ class AugustData(AugustSubscriberMixin):
await self._async_refresh_device_detail_by_ids(self._subscriptions.keys()) await self._async_refresh_device_detail_by_ids(self._subscriptions.keys())
async def _async_refresh_device_detail_by_ids(self, device_ids_list): async def _async_refresh_device_detail_by_ids(self, device_ids_list):
for device_id in device_ids_list: 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 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( await self._async_update_device_detail(
self._locks_by_id[device_id], self._api.async_get_lock_detail self._locks_by_id[device_id], self._api.async_get_lock_detail
) )
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 # keypads are always attached to locks
if ( if (
device_id in self._device_detail_by_id device_id in self._device_detail_by_id
@ -213,9 +260,9 @@ class AugustData(AugustSubscriberMixin):
def _get_device_name(self, device_id): def _get_device_name(self, device_id):
"""Return doorbell or lock name as August has it stored.""" """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 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 return self._doorbells_by_id[device_id].device_name
async def async_lock(self, device_id): async def async_lock(self, device_id):
@ -252,8 +299,7 @@ class AugustData(AugustSubscriberMixin):
return ret return ret
def _remove_inoperative_doorbells(self): def _remove_inoperative_doorbells(self):
doorbells = list(self.doorbells) for doorbell in list(self.doorbells):
for doorbell in doorbells:
device_id = doorbell.device_id device_id = doorbell.device_id
doorbell_is_operative = False doorbell_is_operative = False
doorbell_detail = self._device_detail_by_id.get(device_id) doorbell_detail = self._device_detail_by_id.get(device_id)
@ -273,9 +319,7 @@ class AugustData(AugustSubscriberMixin):
# Remove non-operative locks as there must # Remove non-operative locks as there must
# be a bridge (August Connect) for them to # be a bridge (August Connect) for them to
# be usable # be usable
locks = list(self.locks) for lock in list(self.locks):
for lock in locks:
device_id = lock.device_id device_id = lock.device_id
lock_is_operative = False lock_is_operative = False
lock_detail = self._device_detail_by_id.get(device_id) 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)", "The lock %s could not be setup because it does not have a bridge (Connect)",
lock.device_name, lock.device_name,
) )
elif not lock_detail.bridge.operative: # Bridge may come back online later so we still add the device since we will
_LOGGER.info( # have a pubnub subscription to tell use when it recovers
"The lock %s could not be setup because the bridge (Connect) is not operative",
lock.device_name,
)
else: else:
lock_is_operative = True lock_is_operative = True
if not lock_is_operative: if not lock_is_operative:
del self._locks_by_id[device_id] del self._locks_by_id[device_id]
del self._device_detail_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)

View file

@ -1,8 +1,12 @@
"""Consume the august activity stream.""" """Consume the august activity stream."""
import asyncio
import logging import logging
from aiohttp import ClientError 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 homeassistant.util.dt import utcnow
from .const import ACTIVITY_UPDATE_INTERVAL from .const import ACTIVITY_UPDATE_INTERVAL
@ -17,27 +21,58 @@ ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500
class ActivityStream(AugustSubscriberMixin): class ActivityStream(AugustSubscriberMixin):
"""August activity stream handler.""" """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.""" """Init August activity stream object."""
super().__init__(hass, ACTIVITY_UPDATE_INTERVAL) super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
self._hass = hass self._hass = hass
self._schedule_updates = {}
self._august_gateway = august_gateway self._august_gateway = august_gateway
self._api = api self._api = api
self._house_ids = house_ids self._house_ids = house_ids
self._latest_activities_by_id_type = {} self._latest_activities = {}
self._last_update_time = None self._last_update_time = None
self._abort_async_track_time_interval = None self._abort_async_track_time_interval = None
self.pubnub = pubnub
self._update_debounce = {}
async def async_setup(self): async def async_setup(self):
"""Token refresh check and catch up the activity stream.""" """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): def get_latest_device_activity(self, device_id, activity_types):
"""Return latest activity that is one of the acitivty_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 return None
latest_device_activities = self._latest_activities_by_id_type[device_id] latest_device_activities = self._latest_activities[device_id]
latest_activity = None latest_activity = None
for activity_type in activity_types: for activity_type in activity_types:
@ -54,21 +89,48 @@ class ActivityStream(AugustSubscriberMixin):
async def _async_refresh(self, time): async def _async_refresh(self, time):
"""Update the activity stream from August.""" """Update the activity stream from August."""
# This is the only place we refresh the api token # This is the only place we refresh the api token
await self._august_gateway.async_refresh_access_token_if_needed() 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) await self._async_update_device_activities(time)
async def _async_update_device_activities(self, time): async def _async_update_device_activities(self, time):
_LOGGER.debug("Start retrieving device activities") _LOGGER.debug("Start retrieving device activities")
await asyncio.gather(
*[
self._update_debounce[house_id].async_call()
for house_id in self._house_ids
]
)
self._last_update_time = time
limit = ( @callback
ACTIVITY_STREAM_FETCH_LIMIT def async_schedule_house_id_refresh(self, house_id):
if self._last_update_time """Update for a house activities now and once in the future."""
else ACTIVITY_CATCH_UP_FETCH_LIMIT 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
) )
for house_id in self._house_ids: 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) _LOGGER.debug("Updating device activity for house id %s", house_id)
try: try:
activities = await self._api.async_get_house_activities( activities = await self._api.async_get_house_activities(
@ -81,15 +143,17 @@ class ActivityStream(AugustSubscriberMixin):
ex, ex,
) )
# Make sure we process the next house if one of them fails # Make sure we process the next house if one of them fails
continue return
_LOGGER.debug( _LOGGER.debug(
"Completed retrieving device activities for house id %s", house_id "Completed retrieving device activities for house id %s", house_id
) )
updated_device_ids = self._process_newer_device_activities(activities) updated_device_ids = self.async_process_newer_device_activities(activities)
if not updated_device_ids:
return
if updated_device_ids:
for device_id in updated_device_ids: for device_id in updated_device_ids:
_LOGGER.debug( _LOGGER.debug(
"async_signal_device_id_update (from activity stream): %s", "async_signal_device_id_update (from activity stream): %s",
@ -97,19 +161,14 @@ class ActivityStream(AugustSubscriberMixin):
) )
self.async_signal_device_id_update(device_id) self.async_signal_device_id_update(device_id)
self._last_update_time = time def async_process_newer_device_activities(self, activities):
"""Process activities if they are newer than the last one."""
def _process_newer_device_activities(self, activities):
updated_device_ids = set() updated_device_ids = set()
for activity in activities: for activity in activities:
device_id = activity.device_id device_id = activity.device_id
activity_type = activity.activity_type activity_type = activity.activity_type
device_activities = self._latest_activities.setdefault(device_id, {})
self._latest_activities_by_id_type.setdefault(device_id, {}) lastest_activity = device_activities.get(activity_type)
lastest_activity = self._latest_activities_by_id_type[device_id].get(
activity_type
)
# Ignore activities that are older than the latest one # Ignore activities that are older than the latest one
if ( if (
@ -118,7 +177,7 @@ class ActivityStream(AugustSubscriberMixin):
): ):
continue continue
self._latest_activities_by_id_type[device_id][activity_type] = activity device_activities[activity_type] = activity
updated_device_ids.add(device_id) updated_device_ids.add(device_id)

View file

@ -2,9 +2,9 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from august.activity import ActivityType from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, ActivityType
from august.lock import LockDoorStatus from yalexs.lock import LockDoorStatus
from august.util import update_lock_detail_from_activity from yalexs.util import update_lock_detail_from_activity
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_CONNECTIVITY,
@ -14,15 +14,15 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_call_later
from homeassistant.util.dt import utcnow
from .const import DATA_AUGUST, DOMAIN from .const import ACTIVITY_UPDATE_INTERVAL, DATA_AUGUST, DOMAIN
from .entity import AugustEntityMixin from .entity import AugustEntityMixin
_LOGGER = logging.getLogger(__name__) _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): def _retrieve_online_state(data, detail):
@ -35,30 +35,43 @@ def _retrieve_online_state(data, detail):
def _retrieve_motion_state(data, detail): def _retrieve_motion_state(data, detail):
latest = data.activity_stream.get_latest_device_activity(
return _activity_time_based_state( detail.device_id, {ActivityType.DOORBELL_MOTION}
data,
detail.device_id,
[ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING],
) )
if latest is None:
return False
return _activity_time_based_state(latest)
def _retrieve_ding_state(data, detail): def _retrieve_ding_state(data, detail):
latest = data.activity_stream.get_latest_device_activity(
return _activity_time_based_state( detail.device_id, {ActivityType.DOORBELL_DING}
data, 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.""" """Get the latest state of the sensor."""
latest = data.activity_stream.get_latest_device_activity(device_id, activity_types)
if latest is not None:
start = latest.activity_start_time start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
return start <= datetime.now() <= end return start <= _native_datetime() <= end
return None
def _native_datetime():
"""Return time in the format august uses without timezone."""
return datetime.now()
SENSOR_NAME = 0 SENSOR_NAME = 0
@ -143,12 +156,19 @@ class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorEntity):
def _update_from_data(self): def _update_from_data(self):
"""Get the latest state of the sensor and update activity.""" """Get the latest state of the sensor and update activity."""
door_activity = self._data.activity_stream.get_latest_device_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: if door_activity is not None:
update_lock_detail_from_activity(self._detail, door_activity) 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 @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique of the door open binary sensor.""" """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 true if the binary sensor is on."""
return self._state return self._state
@property
def _sensor_config(self):
"""Return the config for the sensor."""
return SENSOR_TYPES_DOORBELL[self._sensor_type]
@property @property
def device_class(self): def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES.""" """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 @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """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 @property
def _state_provider(self): def _state_provider(self):
"""Return the state provider for the binary sensor.""" """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 @property
def _is_time_based(self): def _is_time_based(self):
"""Return true of false if the sensor is time based.""" """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 @callback
def _update_from_data(self): def _update_from_data(self):
@ -228,14 +253,17 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
"""Timer callback for sensor update.""" """Timer callback for sensor update."""
self._check_for_off_update_listener = None self._check_for_off_update_listener = None
self._update_from_data() 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._check_for_off_update_listener = async_call_later(
self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION self.hass, TIME_TO_RECHECK_DETECTION.seconds, _scheduled_update
) )
def _cancel_any_pending_updates(self): def _cancel_any_pending_updates(self):
"""Cancel any updates to recheck a sensor to see if it is ready to turn off.""" """Cancel any updates to recheck a sensor to see if it is ready to turn off."""
if self._check_for_off_update_listener: if not self._check_for_off_update_listener:
return
_LOGGER.debug("%s: canceled pending update", self.entity_id) _LOGGER.debug("%s: canceled pending update", self.entity_id)
self._check_for_off_update_listener() self._check_for_off_update_listener()
self._check_for_off_update_listener = None self._check_for_off_update_listener = None
@ -248,7 +276,4 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity):
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique id of the doorbell sensor.""" """Get the unique id of the doorbell sensor."""
return ( return f"{self._device_id}_{self._sensor_config[SENSOR_NAME].lower()}"
f"{self._device_id}_"
f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}"
)

View file

@ -1,7 +1,7 @@
"""Support for August doorbell camera.""" """Support for August doorbell camera."""
from august.activity import ActivityType from yalexs.activity import ActivityType
from august.util import update_doorbell_image_from_activity from yalexs.util import update_doorbell_image_from_activity
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.core import callback from homeassistant.core import callback
@ -63,7 +63,7 @@ class AugustCamera(AugustEntityMixin, Camera):
def _update_from_data(self): def _update_from_data(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
doorbell_activity = self._data.activity_stream.get_latest_device_activity( 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: if doorbell_activity is not None:

View file

@ -1,8 +1,8 @@
"""Config flow for August integration.""" """Config flow for August integration."""
import logging import logging
from august.authenticator import ValidationResult
import voluptuous as vol import voluptuous as vol
from yalexs.authenticator import ValidationResult
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME

View file

@ -5,8 +5,8 @@ import logging
import os import os
from aiohttp import ClientError, ClientResponseError from aiohttp import ClientError, ClientResponseError
from august.api_async import ApiAsync from yalexs.api_async import ApiAsync
from august.authenticator_async import AuthenticationState, AuthenticatorAsync from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_PASSWORD,

View file

@ -1,9 +1,9 @@
"""Support for August lock.""" """Support for August lock."""
import logging import logging
from august.activity import ActivityType from yalexs.activity import ActivityType
from august.lock import LockStatus from yalexs.lock import LockStatus
from august.util import update_lock_detail_from_activity from yalexs.util import update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
@ -73,13 +73,21 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
def _update_from_data(self): def _update_from_data(self):
"""Get the latest state of the sensor and update activity.""" """Get the latest state of the sensor and update activity."""
lock_activity = self._data.activity_stream.get_latest_device_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: if lock_activity is not None:
self._changed_by = lock_activity.operated_by self._changed_by = lock_activity.operated_by
update_lock_detail_from_activity(self._detail, lock_activity) 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() self._update_lock_status_from_detail()
@property @property

View file

@ -2,7 +2,7 @@
"domain": "august", "domain": "august",
"name": "August", "name": "August",
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["py-august==0.25.2"], "requirements": ["yalexs==1.1.4"],
"codeowners": ["@bdraco"], "codeowners": ["@bdraco"],
"dhcp": [ "dhcp": [
{"hostname":"connect","macaddress":"D86162*"}, {"hostname":"connect","macaddress":"D86162*"},

View file

@ -1,7 +1,7 @@
"""Support for August sensors.""" """Support for August sensors."""
import logging import logging
from august.activity import ActivityType from yalexs.activity import ActivityType
from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE
@ -154,7 +154,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity):
def _update_from_data(self): def _update_from_data(self):
"""Get the latest state of the sensor and update activity.""" """Get the latest state of the sensor and update activity."""
lock_activity = self._data.activity_stream.get_latest_device_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 self._available = True

View file

@ -1191,9 +1191,6 @@ pushover_complete==1.1.1
# homeassistant.components.rpi_gpio_pwm # homeassistant.components.rpi_gpio_pwm
pwmled==1.6.7 pwmled==1.6.7
# homeassistant.components.august
py-august==0.25.2
# homeassistant.components.canary # homeassistant.components.canary
py-canary==0.5.1 py-canary==0.5.1
@ -2347,6 +2344,9 @@ xs1-api-client==3.0.0
# homeassistant.components.yale_smart_alarm # homeassistant.components.yale_smart_alarm
yalesmartalarmclient==0.1.6 yalesmartalarmclient==0.1.6
# homeassistant.components.august
yalexs==1.1.4
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.5.4 yeelight==0.5.4

View file

@ -616,9 +616,6 @@ pure-python-adb[async]==0.3.0.dev0
# homeassistant.components.pushbullet # homeassistant.components.pushbullet
pushbullet.py==0.11.0 pushbullet.py==0.11.0
# homeassistant.components.august
py-august==0.25.2
# homeassistant.components.canary # homeassistant.components.canary
py-canary==0.5.1 py-canary==0.5.1
@ -1208,6 +1205,9 @@ xbox-webapi==2.0.8
# homeassistant.components.zestimate # homeassistant.components.zestimate
xmltodict==0.12.0 xmltodict==0.12.0
# homeassistant.components.august
yalexs==1.1.4
# homeassistant.components.yeelight # homeassistant.components.yeelight
yeelight==0.5.4 yeelight==0.5.4

View file

@ -6,21 +6,26 @@ import time
# from unittest.mock import AsyncMock # from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch 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_DOOR_OPERATION,
ACTIVITY_ACTIONS_DOORBELL_DING, ACTIVITY_ACTIONS_DOORBELL_DING,
ACTIVITY_ACTIONS_DOORBELL_MOTION, ACTIVITY_ACTIONS_DOORBELL_MOTION,
ACTIVITY_ACTIONS_DOORBELL_VIEW, ACTIVITY_ACTIONS_DOORBELL_VIEW,
ACTIVITY_ACTIONS_LOCK_OPERATION, ACTIVITY_ACTIONS_LOCK_OPERATION,
SOURCE_LOCK_OPERATE,
SOURCE_LOG,
BridgeOperationActivity,
DoorbellDingActivity, DoorbellDingActivity,
DoorbellMotionActivity, DoorbellMotionActivity,
DoorbellViewActivity, DoorbellViewActivity,
DoorOperationActivity, DoorOperationActivity,
LockOperationActivity, LockOperationActivity,
) )
from august.authenticator import AuthenticationState from yalexs.authenticator import AuthenticationState
from august.doorbell import Doorbell, DoorbellDetail from yalexs.doorbell import Doorbell, DoorbellDetail
from august.lock import Lock, LockDetail from yalexs.lock import Lock, LockDetail
from yalexs.pubnub_async import AugustPubNub
from homeassistant.components.august.const import CONF_LOGIN_METHOD, DOMAIN from homeassistant.components.august.const import CONF_LOGIN_METHOD, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 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.ApiAsync")
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") @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.""" """Set up august integration."""
authenticate_mock.side_effect = MagicMock( authenticate_mock.side_effect = MagicMock(
return_value=_mock_august_authentication( return_value=_mock_august_authentication(
@ -62,16 +69,21 @@ async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
options={}, options={},
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
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) assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
return True return entry
async def _create_august_with_devices( 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: if api_call_side_effects is None:
api_call_side_effects = {} api_call_side_effects = {}
if pubnub is None:
pubnub = AugustPubNub()
device_data = {"doorbells": [], "locks": []} device_data = {"doorbells": [], "locks": []}
for device in devices: for device in devices:
@ -152,10 +164,12 @@ async def _create_august_with_devices(
"unlock_return_activities" "unlock_return_activities"
] = unlock_return_activities_side_effect ] = 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") api_instance = MagicMock(name="Api")
if api_call_side_effects["get_lock_detail"]: 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"] 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): 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).state = PropertyMock(return_value=state)
type(authentication).access_token = PropertyMock(return_value=token_text) type(authentication).access_token = PropertyMock(return_value=token_text)
type(authentication).access_token_expires = PropertyMock( 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): def _mock_lock_operation_activity(lock, action, offset):
return LockOperationActivity( return LockOperationActivity(
SOURCE_LOCK_OPERATE,
{ {
"dateTime": (time.time() + offset) * 1000, "dateTime": (time.time() + offset) * 1000,
"deviceID": lock.device_id, "deviceID": lock.device_id,
"deviceType": "lock", "deviceType": "lock",
"action": action, "action": action,
} },
) )
def _mock_door_operation_activity(lock, action, offset): def _mock_door_operation_activity(lock, action, offset):
return DoorOperationActivity( return DoorOperationActivity(
SOURCE_LOCK_OPERATE,
{ {
"dateTime": (time.time() + offset) * 1000, "dateTime": (time.time() + offset) * 1000,
"deviceID": lock.device_id, "deviceID": lock.device_id,
"deviceType": "lock", "deviceType": "lock",
"action": action, "action": action,
} },
) )
@ -327,13 +345,15 @@ def _activity_from_dict(activity_dict):
activity_dict["dateTime"] = time.time() * 1000 activity_dict["dateTime"] = time.time() * 1000
if action in ACTIVITY_ACTIONS_DOORBELL_DING: if action in ACTIVITY_ACTIONS_DOORBELL_DING:
return DoorbellDingActivity(activity_dict) return DoorbellDingActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_MOTION: if action in ACTIVITY_ACTIONS_DOORBELL_MOTION:
return DoorbellMotionActivity(activity_dict) return DoorbellMotionActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_VIEW: if action in ACTIVITY_ACTIONS_DOORBELL_VIEW:
return DoorbellViewActivity(activity_dict) return DoorbellViewActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_LOCK_OPERATION: if action in ACTIVITY_ACTIONS_LOCK_OPERATION:
return LockOperationActivity(activity_dict) return LockOperationActivity(SOURCE_LOG, activity_dict)
if action in ACTIVITY_ACTIONS_DOOR_OPERATION: 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 return None

View file

@ -1,4 +1,9 @@
"""The binary_sensor tests for the august platform.""" """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.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -9,7 +14,9 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
from homeassistant.helpers import device_registry as dr 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 ( from tests.components.august.mocks import (
_create_august_with_devices, _create_august_with_devices,
_mock_activities_from_fixture, _mock_activities_from_fixture,
@ -52,6 +59,22 @@ async def test_doorsense(hass):
assert binary_sensor_online_with_doorsense_name.state == STATE_OFF 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): async def test_create_doorbell(hass):
"""Test creation of a doorbell.""" """Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") 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" "binary_sensor.k98gidt45gul_name_ding"
) )
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF 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): async def test_doorbell_device_registry(hass):

View file

@ -1,7 +1,7 @@
"""Test the August config flow.""" """Test the August config flow."""
from unittest.mock import patch from unittest.mock import patch
from august.authenticator import ValidationResult from yalexs.authenticator import ValidationResult
from homeassistant import config_entries, setup from homeassistant import config_entries, setup
from homeassistant.components.august.const import ( from homeassistant.components.august.const import (

View file

@ -1,7 +1,7 @@
"""The gateway tests for the august platform.""" """The gateway tests for the august platform."""
from unittest.mock import MagicMock, patch 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.const import DOMAIN
from homeassistant.components.august.gateway import AugustGateway from homeassistant.components.august.gateway import AugustGateway

View file

@ -3,13 +3,14 @@ import asyncio
from unittest.mock import patch from unittest.mock import patch
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from august.authenticator_common import AuthenticationState from yalexs.authenticator_common import AuthenticationState
from august.exceptions import AugustApiAIOHTTPError from yalexs.exceptions import AugustApiAIOHTTPError
from homeassistant import setup from homeassistant import setup
from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.const import DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY, ENTRY_STATE_SETUP_RETRY,
) )
@ -46,7 +47,7 @@ async def test_august_is_offline(hass):
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"august.authenticator_async.AuthenticatorAsync.async_authenticate", "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
side_effect=asyncio.TimeoutError, side_effect=asyncio.TimeoutError,
): ):
await hass.config_entries.async_setup(config_entry.entry_id) 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", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"august.authenticator_async.AuthenticatorAsync.async_authenticate", "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
side_effect=ClientResponseError(None, None, status=401), side_effect=ClientResponseError(None, None, status=401),
): ):
await hass.config_entries.async_setup(config_entry.entry_id) 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", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"august.authenticator_async.AuthenticatorAsync.async_authenticate", "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
return_value=_mock_august_authentication( return_value=_mock_august_authentication(
"original_token", 1234, AuthenticationState.BAD_PASSWORD "original_token", 1234, AuthenticationState.BAD_PASSWORD
), ),
@ -206,7 +207,7 @@ async def test_http_failure(hass):
await setup.async_setup_component(hass, "persistent_notification", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"august.authenticator_async.AuthenticatorAsync.async_authenticate", "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
side_effect=ClientResponseError(None, None, status=500), side_effect=ClientResponseError(None, None, status=500),
): ):
await hass.config_entries.async_setup(config_entry.entry_id) 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", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"august.authenticator_async.AuthenticatorAsync.async_authenticate", "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
return_value=_mock_august_authentication("original_token", 1234, None), return_value=_mock_august_authentication("original_token", 1234, None),
): ):
await hass.config_entries.async_setup(config_entry.entry_id) 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", {}) await setup.async_setup_component(hass, "persistent_notification", {})
with patch( with patch(
"august.authenticator_async.AuthenticatorAsync.async_authenticate", "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate",
return_value=_mock_august_authentication( return_value=_mock_august_authentication(
"original_token", 1234, AuthenticationState.REQUIRES_VALIDATION "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 len(hass.config_entries.flow.async_progress()) == 1
assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth" 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()

View file

@ -1,15 +1,23 @@
"""The lock tests for the august platform.""" """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.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
SERVICE_LOCK, SERVICE_LOCK,
SERVICE_UNLOCK, SERVICE_UNLOCK,
STATE_LOCKED, STATE_LOCKED,
STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
STATE_UNLOCKED, STATE_UNLOCKED,
) )
from homeassistant.helpers import device_registry as dr, entity_registry as er 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 ( from tests.components.august.mocks import (
_create_august_with_devices, _create_august_with_devices,
_mock_activities_from_fixture, _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") lock_brokenid_name = hass.states.get("lock.brokenid_name")
assert lock_brokenid_name.state == STATE_UNKNOWN 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()

View file

@ -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"
}
}]

View file

@ -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"
}
}]

View file

@ -55,7 +55,7 @@
"reconnect" "reconnect"
], ],
"doorbellID" : "K98GiDT45GUL", "doorbellID" : "K98GiDT45GUL",
"HouseID" : "3dd2accaea08", "HouseID" : "mockhouseid1",
"telemetry" : { "telemetry" : {
"signal_level" : -56, "signal_level" : -56,
"date" : "2017-12-10 08:05:12", "date" : "2017-12-10 08:05:12",

View file

@ -13,9 +13,10 @@
"updated" : "2000-00-00T00:00:00.447Z" "updated" : "2000-00-00T00:00:00.447Z"
} }
}, },
"pubsubChannel":"pubsub",
"Calibrated" : false, "Calibrated" : false,
"Created" : "2000-00-00T00:00:00.447Z", "Created" : "2000-00-00T00:00:00.447Z",
"HouseID" : "123", "HouseID" : "mockhouseid1",
"HouseName" : "Test", "HouseName" : "Test",
"LockID" : "online_with_doorsense", "LockID" : "online_with_doorsense",
"LockName" : "Online door with doorsense", "LockName" : "Online door with doorsense",