Deduplicate code in the august integration (#32101)
* Deduplicate code in the august integration * Add additional tests for august (more coming) * Door state is now updated when a lock or unlock call returns as the state is contained in the response which avoids the confusing out of sync state * revert * document known issue with doorsense and lock getting out of sync (pre-existing) * Address review comments * Additional review comments
This commit is contained in:
parent
d2d788631e
commit
693441e56f
7 changed files with 379 additions and 146 deletions
|
@ -40,18 +40,20 @@ DATA_AUGUST = "august"
|
||||||
DOMAIN = "august"
|
DOMAIN = "august"
|
||||||
DEFAULT_ENTITY_NAMESPACE = "august"
|
DEFAULT_ENTITY_NAMESPACE = "august"
|
||||||
|
|
||||||
# Limit battery and hardware updates to 1800 seconds
|
# Limit battery, online, and hardware updates to 1800 seconds
|
||||||
# in order to reduce the number of api requests and
|
# in order to reduce the number of api requests and
|
||||||
# avoid hitting rate limits
|
# avoid hitting rate limits
|
||||||
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
|
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
|
||||||
|
|
||||||
# Doorbells need to update more frequently than locks
|
# Doorbells need to update more frequently than locks
|
||||||
# since we get an image from the doorbell api
|
# since we get an image from the doorbell api. Once
|
||||||
MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20)
|
# py-august 0.18.0 is released doorbell status updates
|
||||||
|
# can be reduced in the same was as locks have been
|
||||||
|
MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES = timedelta(seconds=20)
|
||||||
|
|
||||||
# Activity needs to be checked more frequently as the
|
# Activity needs to be checked more frequently as the
|
||||||
# doorbell motion and rings are included here
|
# doorbell motion and rings are included here
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_ACTIVITY_UPDATES = timedelta(seconds=10)
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
|
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
|
|
||||||
|
@ -265,7 +267,7 @@ class AugustData:
|
||||||
activities = await self.async_get_device_activities(device_id, *activity_types)
|
activities = await self.async_get_device_activities(device_id, *activity_types)
|
||||||
return next(iter(activities or []), None)
|
return next(iter(activities or []), None)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_ACTIVITY_UPDATES)
|
||||||
async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||||
"""Update data object with latest from August API."""
|
"""Update data object with latest from August API."""
|
||||||
|
|
||||||
|
@ -292,77 +294,32 @@ class AugustData:
|
||||||
|
|
||||||
_LOGGER.debug("Completed retrieving device activities")
|
_LOGGER.debug("Completed retrieving device activities")
|
||||||
|
|
||||||
async def async_get_doorbell_detail(self, doorbell_id):
|
async def async_get_doorbell_detail(self, device_id):
|
||||||
"""Return doorbell detail."""
|
"""Return doorbell detail."""
|
||||||
await self._async_update_doorbells()
|
await self._async_update_doorbells_detail()
|
||||||
return self._doorbell_detail_by_id.get(doorbell_id)
|
return self._doorbell_detail_by_id.get(device_id)
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES)
|
||||||
async def _async_update_doorbells(self):
|
async def _async_update_doorbells_detail(self):
|
||||||
await self._hass.async_add_executor_job(self._update_doorbells)
|
await self._hass.async_add_executor_job(self._update_doorbells_detail)
|
||||||
|
|
||||||
def _update_doorbells(self):
|
def _update_doorbells_detail(self):
|
||||||
detail_by_id = {}
|
self._doorbell_detail_by_id = self._update_device_detail(
|
||||||
|
"doorbell", self._doorbells, self._api.get_doorbell_detail
|
||||||
_LOGGER.debug("Start retrieving doorbell details")
|
|
||||||
for doorbell in self._doorbells:
|
|
||||||
_LOGGER.debug("Updating doorbell status for %s", doorbell.device_name)
|
|
||||||
try:
|
|
||||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
|
||||||
self._access_token, doorbell.device_id
|
|
||||||
)
|
)
|
||||||
except RequestException as ex:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Request error trying to retrieve doorbell status for %s. %s",
|
|
||||||
doorbell.device_name,
|
|
||||||
ex,
|
|
||||||
)
|
|
||||||
detail_by_id[doorbell.device_id] = None
|
|
||||||
except Exception:
|
|
||||||
detail_by_id[doorbell.device_id] = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
_LOGGER.debug("Completed retrieving doorbell details")
|
def lock_has_doorsense(self, device_id):
|
||||||
self._doorbell_detail_by_id = detail_by_id
|
|
||||||
|
|
||||||
def update_door_state(self, lock_id, door_state, update_start_time_utc):
|
|
||||||
"""Set the door status and last status update time.
|
|
||||||
|
|
||||||
This is called when newer activity is detected on the activity feed
|
|
||||||
in order to keep the internal data in sync
|
|
||||||
"""
|
|
||||||
# When syncing the door state became available via py-august, this
|
|
||||||
# function caused to be actively used. It will be again as we will
|
|
||||||
# update the door state from lock/unlock operations as the august api
|
|
||||||
# does report the door state on lock/unlock, however py-august does not
|
|
||||||
# expose this to us yet.
|
|
||||||
self._lock_detail_by_id[lock_id].door_state = door_state
|
|
||||||
self._lock_detail_by_id[lock_id].door_state_datetime = update_start_time_utc
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update_lock_status(self, lock_id, lock_status, update_start_time_utc):
|
|
||||||
"""Set the lock status and last status update time.
|
|
||||||
|
|
||||||
This is used when the lock, unlock apis are called
|
|
||||||
or newer activity is detected on the activity feed
|
|
||||||
in order to keep the internal data in sync
|
|
||||||
"""
|
|
||||||
self._lock_detail_by_id[lock_id].lock_status = lock_status
|
|
||||||
self._lock_detail_by_id[lock_id].lock_status_datetime = update_start_time_utc
|
|
||||||
return True
|
|
||||||
|
|
||||||
def lock_has_doorsense(self, lock_id):
|
|
||||||
"""Determine if a lock has doorsense installed and can tell when the door is open or closed."""
|
"""Determine if a lock has doorsense installed and can tell when the door is open or closed."""
|
||||||
# We do not update here since this is not expected
|
# We do not update here since this is not expected
|
||||||
# to change until restart
|
# to change until restart
|
||||||
if self._lock_detail_by_id[lock_id] is None:
|
if self._lock_detail_by_id[device_id] is None:
|
||||||
return False
|
return False
|
||||||
return self._lock_detail_by_id[lock_id].doorsense
|
return self._lock_detail_by_id[device_id].doorsense
|
||||||
|
|
||||||
async def async_get_lock_detail(self, lock_id):
|
async def async_get_lock_detail(self, device_id):
|
||||||
"""Return lock detail."""
|
"""Return lock detail."""
|
||||||
await self._async_update_locks_detail()
|
await self._async_update_locks_detail()
|
||||||
return self._lock_detail_by_id[lock_id]
|
return self._lock_detail_by_id[device_id]
|
||||||
|
|
||||||
def get_lock_name(self, device_id):
|
def get_lock_name(self, device_id):
|
||||||
"""Return lock name as August has it stored."""
|
"""Return lock name as August has it stored."""
|
||||||
|
@ -375,34 +332,39 @@ class AugustData:
|
||||||
await self._hass.async_add_executor_job(self._update_locks_detail)
|
await self._hass.async_add_executor_job(self._update_locks_detail)
|
||||||
|
|
||||||
def _update_locks_detail(self):
|
def _update_locks_detail(self):
|
||||||
|
self._lock_detail_by_id = self._update_device_detail(
|
||||||
|
"lock", self._locks, self._api.get_lock_detail
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_device_detail(self, device_type, devices, api_call):
|
||||||
detail_by_id = {}
|
detail_by_id = {}
|
||||||
|
|
||||||
_LOGGER.debug("Start retrieving locks detail")
|
_LOGGER.debug("Start retrieving %s detail", device_type)
|
||||||
for lock in self._locks:
|
for device in devices:
|
||||||
|
device_id = device.device_id
|
||||||
try:
|
try:
|
||||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
detail_by_id[device_id] = api_call(self._access_token, device_id)
|
||||||
self._access_token, lock.device_id
|
|
||||||
)
|
|
||||||
except RequestException as ex:
|
except RequestException as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Request error trying to retrieve door details for %s. %s",
|
"Request error trying to retrieve %s details for %s. %s",
|
||||||
lock.device_name,
|
device_type,
|
||||||
|
device.device_name,
|
||||||
ex,
|
ex,
|
||||||
)
|
)
|
||||||
detail_by_id[lock.device_id] = None
|
detail_by_id[device_id] = None
|
||||||
except Exception:
|
except Exception:
|
||||||
detail_by_id[lock.device_id] = None
|
detail_by_id[device_id] = None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
_LOGGER.debug("Completed retrieving locks detail")
|
_LOGGER.debug("Completed retrieving %s detail", device_type)
|
||||||
self._lock_detail_by_id = detail_by_id
|
return detail_by_id
|
||||||
|
|
||||||
def lock(self, device_id):
|
def lock(self, device_id):
|
||||||
"""Lock the device."""
|
"""Lock the device."""
|
||||||
return _call_api_operation_that_requires_bridge(
|
return _call_api_operation_that_requires_bridge(
|
||||||
self.get_lock_name(device_id),
|
self.get_lock_name(device_id),
|
||||||
"lock",
|
"lock",
|
||||||
self._api.lock,
|
self._api.lock_return_activities,
|
||||||
self._access_token,
|
self._access_token,
|
||||||
device_id,
|
device_id,
|
||||||
)
|
)
|
||||||
|
@ -412,7 +374,7 @@ class AugustData:
|
||||||
return _call_api_operation_that_requires_bridge(
|
return _call_api_operation_that_requires_bridge(
|
||||||
self.get_lock_name(device_id),
|
self.get_lock_name(device_id),
|
||||||
"unlock",
|
"unlock",
|
||||||
self._api.unlock,
|
self._api.unlock_return_activities,
|
||||||
self._access_token,
|
self._access_token,
|
||||||
device_id,
|
device_id,
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,6 @@ from august.util import update_lock_detail_from_activity
|
||||||
|
|
||||||
from homeassistant.components.lock import LockDevice
|
from homeassistant.components.lock import LockDevice
|
||||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||||
from homeassistant.util import dt
|
|
||||||
|
|
||||||
from . import DATA_AUGUST
|
from . import DATA_AUGUST
|
||||||
|
|
||||||
|
@ -43,27 +42,31 @@ class AugustLock(LockDevice):
|
||||||
|
|
||||||
async def async_lock(self, **kwargs):
|
async def async_lock(self, **kwargs):
|
||||||
"""Lock the device."""
|
"""Lock the device."""
|
||||||
update_start_time_utc = dt.utcnow()
|
await self._call_lock_operation(self._data.lock)
|
||||||
lock_status = await self.hass.async_add_executor_job(
|
|
||||||
self._data.lock, self._lock.device_id
|
|
||||||
)
|
|
||||||
self._update_lock_status(lock_status, update_start_time_utc)
|
|
||||||
|
|
||||||
async def async_unlock(self, **kwargs):
|
async def async_unlock(self, **kwargs):
|
||||||
"""Unlock the device."""
|
"""Unlock the device."""
|
||||||
update_start_time_utc = dt.utcnow()
|
await self._call_lock_operation(self._data.unlock)
|
||||||
lock_status = await self.hass.async_add_executor_job(
|
|
||||||
self._data.unlock, self._lock.device_id
|
|
||||||
)
|
|
||||||
self._update_lock_status(lock_status, update_start_time_utc)
|
|
||||||
|
|
||||||
def _update_lock_status(self, lock_status, update_start_time_utc):
|
async def _call_lock_operation(self, lock_operation):
|
||||||
|
activities = await self.hass.async_add_executor_job(
|
||||||
|
lock_operation, self._lock.device_id
|
||||||
|
)
|
||||||
|
for lock_activity in activities:
|
||||||
|
update_lock_detail_from_activity(self._lock_detail, lock_activity)
|
||||||
|
|
||||||
|
if self._update_lock_status_from_detail():
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def _update_lock_status_from_detail(self):
|
||||||
|
lock_status = self._lock_detail.lock_status
|
||||||
if self._lock_status != lock_status:
|
if self._lock_status != lock_status:
|
||||||
self._lock_status = lock_status
|
self._lock_status = lock_status
|
||||||
self._data.update_lock_status(
|
self._available = (
|
||||||
self._lock.device_id, lock_status, update_start_time_utc
|
lock_status is not None and lock_status != LockStatus.UNKNOWN
|
||||||
)
|
)
|
||||||
self.schedule_update_ha_state()
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Get the latest state of the sensor and update activity."""
|
"""Get the latest state of the sensor and update activity."""
|
||||||
|
@ -76,10 +79,7 @@ class AugustLock(LockDevice):
|
||||||
self._changed_by = lock_activity.operated_by
|
self._changed_by = lock_activity.operated_by
|
||||||
update_lock_detail_from_activity(self._lock_detail, lock_activity)
|
update_lock_detail_from_activity(self._lock_detail, lock_activity)
|
||||||
|
|
||||||
self._lock_status = self._lock_detail.lock_status
|
self._update_lock_status_from_detail()
|
||||||
self._available = (
|
|
||||||
self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
from asynctest import mock
|
from asynctest import mock
|
||||||
from august.activity import Activity
|
from august.activity import Activity, DoorOperationActivity, LockOperationActivity
|
||||||
from august.api import Api
|
from august.api import Api
|
||||||
from august.authenticator import AuthenticationState
|
from august.authenticator import AuthenticationState
|
||||||
from august.doorbell import Doorbell, DoorbellDetail
|
from august.doorbell import Doorbell, DoorbellDetail
|
||||||
from august.exceptions import AugustApiHTTPError
|
from august.exceptions import AugustApiHTTPError
|
||||||
from august.lock import Lock, LockDetail, LockStatus
|
from august.lock import Lock, LockDetail
|
||||||
|
|
||||||
from homeassistant.components.august import (
|
from homeassistant.components.august import (
|
||||||
CONF_LOGIN_METHOD,
|
CONF_LOGIN_METHOD,
|
||||||
|
@ -19,7 +20,6 @@ from homeassistant.components.august import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
AugustData,
|
AugustData,
|
||||||
)
|
)
|
||||||
from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor
|
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt
|
from homeassistant.util import dt
|
||||||
|
|
||||||
|
@ -39,44 +39,125 @@ def _mock_get_config():
|
||||||
|
|
||||||
@mock.patch("homeassistant.components.august.Api")
|
@mock.patch("homeassistant.components.august.Api")
|
||||||
@mock.patch("homeassistant.components.august.Authenticator.authenticate")
|
@mock.patch("homeassistant.components.august.Authenticator.authenticate")
|
||||||
async def _mock_setup_august(hass, api_mocks_callback, authenticate_mock, api_mock):
|
async def _mock_setup_august(hass, api_instance, 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("original_token", 1234)
|
return_value=_mock_august_authentication("original_token", 1234)
|
||||||
)
|
)
|
||||||
api_mocks_callback(api_mock)
|
api_mock.return_value = api_instance
|
||||||
assert await async_setup_component(hass, DOMAIN, _mock_get_config())
|
assert await async_setup_component(hass, DOMAIN, _mock_get_config())
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def _create_august_with_devices(hass, lock_details=[], doorbell_details=[]):
|
async def _create_august_with_devices(hass, devices, api_call_side_effects=None):
|
||||||
locks = []
|
if api_call_side_effects is None:
|
||||||
doorbells = []
|
api_call_side_effects = {}
|
||||||
for lock in lock_details:
|
device_data = {
|
||||||
if isinstance(lock, LockDetail):
|
"doorbells": [],
|
||||||
locks.append(_mock_august_lock(lock.device_id))
|
"locks": [],
|
||||||
for doorbell in doorbell_details:
|
}
|
||||||
if isinstance(lock, DoorbellDetail):
|
for device in devices:
|
||||||
doorbells.append(_mock_august_doorbell(doorbell.device_id))
|
if isinstance(device, LockDetail):
|
||||||
|
device_data["locks"].append(
|
||||||
|
{"base": _mock_august_lock(device.device_id), "detail": device}
|
||||||
|
)
|
||||||
|
elif isinstance(device, DoorbellDetail):
|
||||||
|
device_data["doorbells"].append(
|
||||||
|
{"base": _mock_august_doorbell(device.device_id), "detail": device}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def _get_device_detail(device_type, device_id):
|
||||||
|
for device in device_data[device_type]:
|
||||||
|
if device["detail"].device_id == device_id:
|
||||||
|
return device["detail"]
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def _get_base_devices(device_type):
|
||||||
|
base_devices = []
|
||||||
|
for device in device_data[device_type]:
|
||||||
|
base_devices.append(device["base"])
|
||||||
|
return base_devices
|
||||||
|
|
||||||
def api_mocks_callback(api):
|
|
||||||
def get_lock_detail_side_effect(access_token, device_id):
|
def get_lock_detail_side_effect(access_token, device_id):
|
||||||
for lock in lock_details:
|
return _get_device_detail("locks", device_id)
|
||||||
if isinstance(lock, LockDetail) and lock.device_id == device_id:
|
|
||||||
return lock
|
|
||||||
|
|
||||||
api_instance = MagicMock()
|
def get_operable_locks_side_effect(access_token):
|
||||||
api_instance.get_lock_detail.side_effect = get_lock_detail_side_effect
|
return _get_base_devices("locks")
|
||||||
api_instance.get_operable_locks.return_value = locks
|
|
||||||
api_instance.get_doorbells.return_value = doorbells
|
|
||||||
api_instance.lock.return_value = LockStatus.LOCKED
|
|
||||||
api_instance.unlock.return_value = LockStatus.UNLOCKED
|
|
||||||
api.return_value = api_instance
|
|
||||||
|
|
||||||
await _mock_setup_august(hass, api_mocks_callback)
|
def get_doorbells_side_effect(access_token):
|
||||||
|
return _get_base_devices("doorbells")
|
||||||
|
|
||||||
return True
|
def get_house_activities_side_effect(access_token, house_id, limit=10):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def lock_return_activities_side_effect(access_token, device_id):
|
||||||
|
lock = _get_device_detail("locks", device_id)
|
||||||
|
return [
|
||||||
|
_mock_lock_operation_activity(lock, "lock"),
|
||||||
|
_mock_door_operation_activity(lock, "doorclosed"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def unlock_return_activities_side_effect(access_token, device_id):
|
||||||
|
lock = _get_device_detail("locks", device_id)
|
||||||
|
return [
|
||||||
|
_mock_lock_operation_activity(lock, "unlock"),
|
||||||
|
_mock_door_operation_activity(lock, "dooropen"),
|
||||||
|
]
|
||||||
|
|
||||||
|
if "get_lock_detail" not in api_call_side_effects:
|
||||||
|
api_call_side_effects["get_lock_detail"] = get_lock_detail_side_effect
|
||||||
|
if "get_operable_locks" not in api_call_side_effects:
|
||||||
|
api_call_side_effects["get_operable_locks"] = get_operable_locks_side_effect
|
||||||
|
if "get_doorbells" not in api_call_side_effects:
|
||||||
|
api_call_side_effects["get_doorbells"] = get_doorbells_side_effect
|
||||||
|
if "get_house_activities" not in api_call_side_effects:
|
||||||
|
api_call_side_effects["get_house_activities"] = get_house_activities_side_effect
|
||||||
|
if "lock_return_activities" not in api_call_side_effects:
|
||||||
|
api_call_side_effects[
|
||||||
|
"lock_return_activities"
|
||||||
|
] = lock_return_activities_side_effect
|
||||||
|
if "unlock_return_activities" not in api_call_side_effects:
|
||||||
|
api_call_side_effects[
|
||||||
|
"unlock_return_activities"
|
||||||
|
] = unlock_return_activities_side_effect
|
||||||
|
|
||||||
|
return await _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):
|
||||||
|
api_instance = MagicMock(name="Api")
|
||||||
|
|
||||||
|
if api_call_side_effects["get_lock_detail"]:
|
||||||
|
api_instance.get_lock_detail.side_effect = api_call_side_effects[
|
||||||
|
"get_lock_detail"
|
||||||
|
]
|
||||||
|
|
||||||
|
if api_call_side_effects["get_operable_locks"]:
|
||||||
|
api_instance.get_operable_locks.side_effect = api_call_side_effects[
|
||||||
|
"get_operable_locks"
|
||||||
|
]
|
||||||
|
|
||||||
|
if api_call_side_effects["get_doorbells"]:
|
||||||
|
api_instance.get_doorbells.side_effect = api_call_side_effects["get_doorbells"]
|
||||||
|
|
||||||
|
if api_call_side_effects["get_house_activities"]:
|
||||||
|
api_instance.get_house_activities.side_effect = api_call_side_effects[
|
||||||
|
"get_house_activities"
|
||||||
|
]
|
||||||
|
|
||||||
|
if api_call_side_effects["lock_return_activities"]:
|
||||||
|
api_instance.lock_return_activities.side_effect = api_call_side_effects[
|
||||||
|
"lock_return_activities"
|
||||||
|
]
|
||||||
|
|
||||||
|
if api_call_side_effects["unlock_return_activities"]:
|
||||||
|
api_instance.unlock_return_activities.side_effect = api_call_side_effects[
|
||||||
|
"unlock_return_activities"
|
||||||
|
]
|
||||||
|
return await _mock_setup_august(hass, api_instance)
|
||||||
|
|
||||||
|
|
||||||
class MockAugustApiFailing(Api):
|
class MockAugustApiFailing(Api):
|
||||||
|
@ -114,19 +195,6 @@ class MockActivity(Activity):
|
||||||
return self._action
|
return self._action
|
||||||
|
|
||||||
|
|
||||||
class MockAugustComponentDoorBinarySensor(AugustDoorBinarySensor):
|
|
||||||
"""A mock for august component AugustDoorBinarySensor class."""
|
|
||||||
|
|
||||||
def _update_door_state(self, door_state, activity_start_time_utc):
|
|
||||||
"""Mock updating the lock status."""
|
|
||||||
self._data.set_last_door_state_update_time_utc(
|
|
||||||
self._door.device_id, activity_start_time_utc
|
|
||||||
)
|
|
||||||
self.last_update_door_state = {}
|
|
||||||
self.last_update_door_state["door_state"] = door_state
|
|
||||||
self.last_update_door_state["activity_start_time_utc"] = activity_start_time_utc
|
|
||||||
|
|
||||||
|
|
||||||
class MockAugustComponentData(AugustData):
|
class MockAugustComponentData(AugustData):
|
||||||
"""A wrapper to mock AugustData."""
|
"""A wrapper to mock AugustData."""
|
||||||
|
|
||||||
|
@ -210,7 +278,7 @@ def _mock_august_lock(lockid="mocklockid1", houseid="mockhouseid1"):
|
||||||
|
|
||||||
def _mock_august_doorbell(deviceid="mockdeviceid1", houseid="mockhouseid1"):
|
def _mock_august_doorbell(deviceid="mockdeviceid1", houseid="mockhouseid1"):
|
||||||
return Doorbell(
|
return Doorbell(
|
||||||
deviceid, _mock_august_doorbell_data(device=deviceid, houseid=houseid)
|
deviceid, _mock_august_doorbell_data(deviceid=deviceid, houseid=houseid)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -218,11 +286,12 @@ def _mock_august_doorbell_data(deviceid="mockdeviceid1", houseid="mockhouseid1")
|
||||||
return {
|
return {
|
||||||
"_id": deviceid,
|
"_id": deviceid,
|
||||||
"DeviceID": deviceid,
|
"DeviceID": deviceid,
|
||||||
"DeviceName": deviceid + " Name",
|
"name": deviceid + " Name",
|
||||||
"HouseID": houseid,
|
"HouseID": houseid,
|
||||||
"UserType": "owner",
|
"UserType": "owner",
|
||||||
"SerialNumber": "mockserial",
|
"serialNumber": "mockserial",
|
||||||
"battery": 90,
|
"battery": 90,
|
||||||
|
"status": "standby",
|
||||||
"currentFirmwareVersion": "mockfirmware",
|
"currentFirmwareVersion": "mockfirmware",
|
||||||
"Bridge": {
|
"Bridge": {
|
||||||
"_id": "bridgeid1",
|
"_id": "bridgeid1",
|
||||||
|
@ -273,6 +342,11 @@ async def _mock_lock_from_fixture(hass, path):
|
||||||
return LockDetail(json_dict)
|
return LockDetail(json_dict)
|
||||||
|
|
||||||
|
|
||||||
|
async def _mock_doorbell_from_fixture(hass, path):
|
||||||
|
json_dict = await _load_json_fixture(hass, path)
|
||||||
|
return DoorbellDetail(json_dict)
|
||||||
|
|
||||||
|
|
||||||
async def _load_json_fixture(hass, path):
|
async def _load_json_fixture(hass, path):
|
||||||
fixture = await hass.async_add_executor_job(
|
fixture = await hass.async_add_executor_job(
|
||||||
load_fixture, os.path.join("august", path)
|
load_fixture, os.path.join("august", path)
|
||||||
|
@ -284,3 +358,25 @@ def _mock_doorsense_missing_august_lock_detail(lockid):
|
||||||
doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid)
|
doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid)
|
||||||
del doorsense_lock_detail_data["LockStatus"]["doorState"]
|
del doorsense_lock_detail_data["LockStatus"]["doorState"]
|
||||||
return LockDetail(doorsense_lock_detail_data)
|
return LockDetail(doorsense_lock_detail_data)
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_lock_operation_activity(lock, action):
|
||||||
|
return LockOperationActivity(
|
||||||
|
{
|
||||||
|
"dateTime": time.time() * 1000,
|
||||||
|
"deviceID": lock.device_id,
|
||||||
|
"deviceType": "lock",
|
||||||
|
"action": action,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_door_operation_activity(lock, action):
|
||||||
|
return DoorOperationActivity(
|
||||||
|
{
|
||||||
|
"dateTime": time.time() * 1000,
|
||||||
|
"deviceID": lock.device_id,
|
||||||
|
"deviceType": "lock",
|
||||||
|
"action": action,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -1 +1,71 @@
|
||||||
"""The binary_sensor tests for the august platform."""
|
"""The binary_sensor tests for the august platform."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
SERVICE_LOCK,
|
||||||
|
SERVICE_UNLOCK,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.components.august.mocks import (
|
||||||
|
_create_august_with_devices,
|
||||||
|
_mock_doorbell_from_fixture,
|
||||||
|
_mock_lock_from_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(
|
||||||
|
reason="The lock and doorsense can get out of sync due to update intervals, "
|
||||||
|
+ "this is an existing bug which will be fixed with dispatcher events to tell "
|
||||||
|
+ "all linked devices to update."
|
||||||
|
)
|
||||||
|
async def test_doorsense(hass):
|
||||||
|
"""Test creation of a lock with doorsense and bridge."""
|
||||||
|
lock_one = await _mock_lock_from_fixture(
|
||||||
|
hass, "get_lock.online_with_doorsense.json"
|
||||||
|
)
|
||||||
|
lock_details = [lock_one]
|
||||||
|
await _create_august_with_devices(hass, lock_details)
|
||||||
|
|
||||||
|
binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open")
|
||||||
|
assert binary_sensor_abc_name.state == STATE_ON
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
data[ATTR_ENTITY_ID] = "lock.abc_name"
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open")
|
||||||
|
assert binary_sensor_abc_name.state == STATE_ON
|
||||||
|
|
||||||
|
assert await hass.services.async_call(
|
||||||
|
LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open")
|
||||||
|
assert binary_sensor_abc_name.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_doorbell(hass):
|
||||||
|
"""Test creation of a doorbell."""
|
||||||
|
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
|
||||||
|
doorbell_details = [doorbell_one]
|
||||||
|
await _create_august_with_devices(hass, doorbell_details)
|
||||||
|
|
||||||
|
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_online = hass.states.get(
|
||||||
|
"binary_sensor.k98gidt45gul_name_online"
|
||||||
|
)
|
||||||
|
assert binary_sensor_k98gidt45gul_name_online.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
|
||||||
|
|
18
tests/components/august/test_camera.py
Normal file
18
tests/components/august/test_camera.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
"""The camera tests for the august platform."""
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_IDLE
|
||||||
|
|
||||||
|
from tests.components.august.mocks import (
|
||||||
|
_create_august_with_devices,
|
||||||
|
_mock_doorbell_from_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_doorbell(hass):
|
||||||
|
"""Test creation of a doorbell."""
|
||||||
|
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
|
||||||
|
doorbell_details = [doorbell_one]
|
||||||
|
await _create_august_with_devices(hass, doorbell_details)
|
||||||
|
|
||||||
|
camera_k98gidt45gul_name = hass.states.get("camera.k98gidt45gul_name")
|
||||||
|
assert camera_k98gidt45gul_name.state == STATE_IDLE
|
|
@ -3,9 +3,9 @@
|
||||||
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_UNLOCK,
|
SERVICE_UNLOCK,
|
||||||
STATE_LOCKED,
|
STATE_LOCKED,
|
||||||
STATE_ON,
|
|
||||||
STATE_UNLOCKED,
|
STATE_UNLOCKED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,13 +15,13 @@ from tests.components.august.mocks import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_one_lock_unlock_happy_path(hass):
|
async def test_one_lock_operation(hass):
|
||||||
"""Test creation of a lock with doorsense and bridge."""
|
"""Test creation of a lock with doorsense and bridge."""
|
||||||
lock_one = await _mock_lock_from_fixture(
|
lock_one = await _mock_lock_from_fixture(
|
||||||
hass, "get_lock.online_with_doorsense.json"
|
hass, "get_lock.online_with_doorsense.json"
|
||||||
)
|
)
|
||||||
lock_details = [lock_one]
|
lock_details = [lock_one]
|
||||||
await _create_august_with_devices(hass, lock_details=lock_details)
|
await _create_august_with_devices(hass, lock_details)
|
||||||
|
|
||||||
lock_abc_name = hass.states.get("lock.abc_name")
|
lock_abc_name = hass.states.get("lock.abc_name")
|
||||||
|
|
||||||
|
@ -42,5 +42,9 @@ async def test_one_lock_unlock_happy_path(hass):
|
||||||
assert lock_abc_name.attributes.get("battery_level") == 92
|
assert lock_abc_name.attributes.get("battery_level") == 92
|
||||||
assert lock_abc_name.attributes.get("friendly_name") == "ABC Name"
|
assert lock_abc_name.attributes.get("friendly_name") == "ABC Name"
|
||||||
|
|
||||||
binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open")
|
assert await hass.services.async_call(
|
||||||
assert binary_sensor_abc_name.state == STATE_ON
|
LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
lock_abc_name = hass.states.get("lock.abc_name")
|
||||||
|
assert lock_abc_name.state == STATE_LOCKED
|
||||||
|
|
83
tests/fixtures/august/get_doorbell.json
vendored
Normal file
83
tests/fixtures/august/get_doorbell.json
vendored
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{
|
||||||
|
"status_timestamp" : 1512811834532,
|
||||||
|
"appID" : "august-iphone",
|
||||||
|
"LockID" : "BBBB1F5F11114C24CCCC97571DD6AAAA",
|
||||||
|
"recentImage" : {
|
||||||
|
"original_filename" : "file",
|
||||||
|
"placeholder" : false,
|
||||||
|
"bytes" : 24476,
|
||||||
|
"height" : 640,
|
||||||
|
"format" : "jpg",
|
||||||
|
"width" : 480,
|
||||||
|
"version" : 1512892814,
|
||||||
|
"resource_type" : "image",
|
||||||
|
"etag" : "54966926be2e93f77d498a55f247661f",
|
||||||
|
"tags" : [],
|
||||||
|
"public_id" : "qqqqt4ctmxwsysylaaaa",
|
||||||
|
"url" : "http://image.com/vmk16naaaa7ibuey7sar.jpg",
|
||||||
|
"created_at" : "2017-12-10T08:01:35Z",
|
||||||
|
"signature" : "75z47ca21b5e8ffda21d2134e478a2307c4625da",
|
||||||
|
"secure_url" : "https://image.com/vmk16naaaa7ibuey7sar.jpg",
|
||||||
|
"type" : "upload"
|
||||||
|
},
|
||||||
|
"settings" : {
|
||||||
|
"keepEncoderRunning" : true,
|
||||||
|
"videoResolution" : "640x480",
|
||||||
|
"minACNoScaling" : 40,
|
||||||
|
"irConfiguration" : 8448272,
|
||||||
|
"directLink" : true,
|
||||||
|
"overlayEnabled" : true,
|
||||||
|
"notify_when_offline" : true,
|
||||||
|
"micVolume" : 100,
|
||||||
|
"bitrateCeiling" : 512000,
|
||||||
|
"initialBitrate" : 384000,
|
||||||
|
"IVAEnabled" : false,
|
||||||
|
"turnOffCamera" : false,
|
||||||
|
"ringSoundEnabled" : true,
|
||||||
|
"JPGQuality" : 70,
|
||||||
|
"motion_notifications" : true,
|
||||||
|
"speakerVolume" : 92,
|
||||||
|
"buttonpush_notifications" : true,
|
||||||
|
"ABREnabled" : true,
|
||||||
|
"debug" : false,
|
||||||
|
"batteryLowThreshold" : 3.1,
|
||||||
|
"batteryRun" : false,
|
||||||
|
"IREnabled" : true,
|
||||||
|
"batteryUseThreshold" : 3.4
|
||||||
|
},
|
||||||
|
"doorbellServerURL" : "https://doorbells.august.com",
|
||||||
|
"name" : "Front Door",
|
||||||
|
"createdAt" : "2016-11-26T22:27:11.176Z",
|
||||||
|
"installDate" : "2016-11-26T22:27:11.176Z",
|
||||||
|
"serialNumber" : "tBXZR0Z35E",
|
||||||
|
"dvrSubscriptionSetupDone" : true,
|
||||||
|
"caps" : [
|
||||||
|
"reconnect"
|
||||||
|
],
|
||||||
|
"doorbellID" : "K98GiDT45GUL",
|
||||||
|
"HouseID" : "3dd2accaea08",
|
||||||
|
"telemetry" : {
|
||||||
|
"signal_level" : -56,
|
||||||
|
"date" : "2017-12-10 08:05:12",
|
||||||
|
"battery_soc" : 96,
|
||||||
|
"battery" : 4.061763,
|
||||||
|
"steady_ac_in" : 22.196405,
|
||||||
|
"BSSID" : "88:ee:00:dd:aa:11",
|
||||||
|
"SSID" : "foo_ssid",
|
||||||
|
"updated_at" : "2017-12-10T08:05:13.650Z",
|
||||||
|
"temperature" : 28.25,
|
||||||
|
"wifi_freq" : 5745,
|
||||||
|
"load_average" : "0.50 0.47 0.35 1/154 9345",
|
||||||
|
"link_quality" : 54,
|
||||||
|
"battery_soh" : 95,
|
||||||
|
"uptime" : "16168.75 13830.49",
|
||||||
|
"ip_addr" : "10.0.1.11",
|
||||||
|
"doorbell_low_battery" : false,
|
||||||
|
"ac_in" : 23.856874
|
||||||
|
},
|
||||||
|
"installUserID" : "c3b2a94e-373e-aaaa-bbbb-36e996827777",
|
||||||
|
"status" : "doorbell_call_status_online",
|
||||||
|
"firmwareVersion" : "2.3.0-RC153+201711151527",
|
||||||
|
"pubsubChannel" : "7c7a6672-59c8-3333-ffff-dcd98705cccc",
|
||||||
|
"updatedAt" : "2017-12-10T08:05:13.650Z"
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue