Convert august to be push instead of poll (#47544)
This commit is contained in:
parent
8e4c0e3ff7
commit
a2c4b438ea
21 changed files with 683 additions and 183 deletions
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()}"
|
|
||||||
)
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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*"},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
34
tests/fixtures/august/get_activity.bridge_offline.json
vendored
Normal file
34
tests/fixtures/august/get_activity.bridge_offline.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}]
|
34
tests/fixtures/august/get_activity.bridge_online.json
vendored
Normal file
34
tests/fixtures/august/get_activity.bridge_online.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}]
|
2
tests/fixtures/august/get_doorbell.json
vendored
2
tests/fixtures/august/get_doorbell.json
vendored
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue