Centralize august activity updates (#32197)

* Reduce August doorbell detail updates

* Doorbell images now get updates from the activity feed

* Tests for activity updates

* py-august now provides bridge_is_online for available state

* py-august now provides is_standby for available state

* py-august now provides get_doorbell_image (eliminate requests)

* remove debug

* black after merge conflict

* Centralize august activity updates

* Updates appear significantly more responsive

* Should address the community complaints about "lag"

* Reduce detail updates (device end points) to one hour interval

* Signal entities to update via dispatcher when new activity arrives

* Resolves out of sync state (skipped test is now unskipped)

* pylint

* fix merge conflict

* review comments

* Remove stray

* Address review items that can be done without refactor
This commit is contained in:
J. Nick Koston 2020-02-26 16:48:44 -10:00 committed by GitHub
parent d207c37c33
commit 4c5e364d90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 352 additions and 117 deletions

View file

@ -1,7 +1,6 @@
"""Support for August devices.""" """Support for August devices."""
import asyncio import asyncio
from datetime import timedelta import itertools
from functools import partial
import logging import logging
from august.api import AugustApiHTTPError from august.api import AugustApiHTTPError
@ -16,10 +15,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .activity import ActivityStream
from .const import ( from .const import (
AUGUST_COMPONENTS, AUGUST_COMPONENTS,
AUGUST_DEVICE_UPDATE,
CONF_ACCESS_TOKEN_CACHE_FILE, CONF_ACCESS_TOKEN_CACHE_FILE,
CONF_INSTALL_ID, CONF_INSTALL_ID,
CONF_LOGIN_METHOD, CONF_LOGIN_METHOD,
@ -29,7 +31,6 @@ from .const import (
DEFAULT_TIMEOUT, DEFAULT_TIMEOUT,
DOMAIN, DOMAIN,
LOGIN_METHODS, LOGIN_METHODS,
MIN_TIME_BETWEEN_ACTIVITY_UPDATES,
MIN_TIME_BETWEEN_DETAIL_UPDATES, MIN_TIME_BETWEEN_DETAIL_UPDATES,
VERIFICATION_CODE_KEY, VERIFICATION_CODE_KEY,
) )
@ -40,7 +41,7 @@ _LOGGER = logging.getLogger(__name__)
TWO_FA_REVALIDATE = "verify_configurator" TWO_FA_REVALIDATE = "verify_configurator"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) DEFAULT_SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
@ -133,6 +134,7 @@ async def async_setup_august(hass, config_entry, august_gateway):
hass.data[DOMAIN][entry_id][DATA_AUGUST] = await hass.async_add_executor_job( hass.data[DOMAIN][entry_id][DATA_AUGUST] = await hass.async_add_executor_job(
AugustData, hass, august_gateway AugustData, hass, august_gateway
) )
await hass.data[DOMAIN][entry_id][DATA_AUGUST].activity_stream.async_start()
for component in AUGUST_COMPONENTS: for component in AUGUST_COMPONENTS:
hass.async_create_task( hass.async_create_task(
@ -178,6 +180,8 @@ async def async_setup_entry(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].activity_stream.async_stop()
unload_ok = all( unload_ok = all(
await asyncio.gather( await asyncio.gather(
*[ *[
@ -196,8 +200,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
class AugustData: class AugustData:
"""August data object.""" """August data object."""
DEFAULT_ACTIVITY_FETCH_LIMIT = 10
def __init__(self, hass, august_gateway): def __init__(self, hass, august_gateway):
"""Init August data object.""" """Init August data object."""
self._hass = hass self._hass = hass
@ -211,12 +213,11 @@ class AugustData:
self._api.get_operable_locks(self._august_gateway.access_token) or [] self._api.get_operable_locks(self._august_gateway.access_token) or []
) )
self._house_ids = set() self._house_ids = set()
for device in self._doorbells + self._locks: for device in itertools.chain(self._doorbells, self._locks):
self._house_ids.add(device.house_id) self._house_ids.add(device.house_id)
self._doorbell_detail_by_id = {} self._doorbell_detail_by_id = {}
self._lock_detail_by_id = {} self._lock_detail_by_id = {}
self._activities_by_id = {}
# We check the locks right away so we can # We check the locks right away so we can
# remove inoperative ones # remove inoperative ones
@ -224,6 +225,10 @@ class AugustData:
self._update_doorbells_detail() self._update_doorbells_detail()
self._filter_inoperative_locks() self._filter_inoperative_locks()
self.activity_stream = ActivityStream(
hass, self._api, self._august_gateway, self._house_ids
)
@property @property
def house_ids(self): def house_ids(self):
"""Return a list of house_ids.""" """Return a list of house_ids."""
@ -239,49 +244,6 @@ class AugustData:
"""Return a list of locks.""" """Return a list of locks."""
return self._locks return self._locks
async def async_get_device_activities(self, device_id, *activity_types):
"""Return a list of activities."""
_LOGGER.debug("Getting device activities for %s", device_id)
await self._async_update_device_activities()
activities = self._activities_by_id.get(device_id, [])
if activity_types:
return [a for a in activities if a.activity_type in activity_types]
return activities
async def async_get_latest_device_activity(self, device_id, *activity_types):
"""Return latest activity."""
activities = await self.async_get_device_activities(device_id, *activity_types)
return next(iter(activities or []), None)
@Throttle(MIN_TIME_BETWEEN_ACTIVITY_UPDATES)
async def _async_update_device_activities(self, limit=DEFAULT_ACTIVITY_FETCH_LIMIT):
"""Update data object with latest from August API."""
# This is the only place we refresh the api token
await self._august_gateway.async_refresh_access_token_if_needed()
return await self._hass.async_add_executor_job(
partial(self._update_device_activities, limit=limit)
)
def _update_device_activities(self, limit=DEFAULT_ACTIVITY_FETCH_LIMIT):
_LOGGER.debug("Start retrieving device activities")
for house_id in self.house_ids:
_LOGGER.debug("Updating device activity for house id %s", house_id)
activities = self._api.get_house_activities(
self._august_gateway.access_token, house_id, limit=limit
)
device_ids = {a.device_id for a in activities}
for device_id in device_ids:
self._activities_by_id[device_id] = [
a for a in activities if a.device_id == device_id
]
_LOGGER.debug("Completed retrieving device activities")
async def async_get_device_detail(self, device): async def async_get_device_detail(self, device):
"""Return the detail for a device.""" """Return the detail for a device."""
if isinstance(device, Lock): if isinstance(device, Lock):
@ -317,11 +279,11 @@ class AugustData:
await self._async_update_locks_detail() await self._async_update_locks_detail()
return self._lock_detail_by_id[device_id] return self._lock_detail_by_id[device_id]
def get_lock_name(self, device_id): def get_device_name(self, device_id):
"""Return lock name as August has it stored.""" """Return doorbell or lock name as August has it stored."""
for lock in self._locks: for device in itertools.chain(self._locks, self._doorbells):
if lock.device_id == device_id: if device.device_id == device_id:
return lock.device_name return device.device_name
@Throttle(MIN_TIME_BETWEEN_DETAIL_UPDATES) @Throttle(MIN_TIME_BETWEEN_DETAIL_UPDATES)
async def _async_update_locks_detail(self): async def _async_update_locks_detail(self):
@ -354,11 +316,17 @@ class AugustData:
_LOGGER.debug("Completed retrieving %s detail", device_type) _LOGGER.debug("Completed retrieving %s detail", device_type)
return detail_by_id return detail_by_id
async def async_signal_operation_changed_device_state(self, device_id):
"""Signal a device update when an operation changes state."""
_LOGGER.debug(
"async_dispatcher_send (from operation): AUGUST_DEVICE_UPDATE-%s", device_id
)
async_dispatcher_send(self._hass, f"{AUGUST_DEVICE_UPDATE}-{device_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 self._call_api_op_requires_bridge(
self.get_lock_name(device_id), device_id,
"lock",
self._api.lock_return_activities, self._api.lock_return_activities,
self._august_gateway.access_token, self._august_gateway.access_token,
device_id, device_id,
@ -366,14 +334,26 @@ class AugustData:
def unlock(self, device_id): def unlock(self, device_id):
"""Unlock the device.""" """Unlock the device."""
return _call_api_operation_that_requires_bridge( return self._call_api_op_requires_bridge(
self.get_lock_name(device_id), device_id,
"unlock",
self._api.unlock_return_activities, self._api.unlock_return_activities,
self._august_gateway.access_token, self._august_gateway.access_token,
device_id, device_id,
) )
def _call_api_op_requires_bridge(self, device_id, func, *args, **kwargs):
"""Call an API that requires the bridge to be online and will change the device state."""
ret = None
try:
ret = func(*args, **kwargs)
except AugustApiHTTPError as err:
device_name = self.get_device_name(device_id)
if device_name is None:
device_name = f"DeviceID: {device_id}"
raise HomeAssistantError(f"{device_name}: {err}")
return ret
def _filter_inoperative_locks(self): def _filter_inoperative_locks(self):
# 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
@ -400,16 +380,3 @@ class AugustData:
operative_locks.append(lock) operative_locks.append(lock)
self._locks = operative_locks self._locks = operative_locks
def _call_api_operation_that_requires_bridge(
device_name, operation_name, func, *args, **kwargs
):
"""Call an API that requires the bridge to be online."""
ret = None
try:
ret = func(*args, **kwargs)
except AugustApiHTTPError as err:
raise HomeAssistantError(device_name + ": " + str(err))
return ret

View file

@ -0,0 +1,141 @@
"""Consume the august activity stream."""
from functools import partial
import logging
from requests import RequestException
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.dt import utcnow
from .const import ACTIVITY_UPDATE_INTERVAL, AUGUST_DEVICE_UPDATE
_LOGGER = logging.getLogger(__name__)
ACTIVITY_STREAM_FETCH_LIMIT = 10
ACTIVITY_CATCH_UP_FETCH_LIMIT = 200
class ActivityStream:
"""August activity stream handler."""
def __init__(self, hass, api, august_gateway, house_ids):
"""Init August activity stream object."""
self._hass = hass
self._august_gateway = august_gateway
self._api = api
self._house_ids = house_ids
self._latest_activities_by_id_type = {}
self._last_update_time = None
self._abort_async_track_time_interval = None
async def async_start(self):
"""Start fetching updates from the activity stream."""
await self._async_update(utcnow)
self._abort_async_track_time_interval = async_track_time_interval(
self._hass, self._async_update, ACTIVITY_UPDATE_INTERVAL
)
@callback
def async_stop(self):
"""Stop fetching updates from the activity stream."""
if self._abort_async_track_time_interval is None:
return
self._abort_async_track_time_interval()
@callback
def async_get_latest_device_activity(self, device_id, activity_types):
"""Return latest activity that is one of the acitivty_types."""
if device_id not in self._latest_activities_by_id_type:
return None
latest_device_activities = self._latest_activities_by_id_type[device_id]
latest_activity = None
for activity_type in activity_types:
if activity_type in latest_device_activities:
if (
latest_activity is not None
and latest_device_activities[activity_type].activity_start_time
<= latest_activity.activity_start_time
):
continue
latest_activity = latest_device_activities[activity_type]
return latest_activity
async def _async_update(self, time):
"""Update the activity stream from August."""
# This is the only place we refresh the api token
await self._august_gateway.async_refresh_access_token_if_needed()
await self._update_device_activities(time)
async def _update_device_activities(self, time):
_LOGGER.debug("Start retrieving device activities")
limit = (
ACTIVITY_STREAM_FETCH_LIMIT
if self._last_update_time
else ACTIVITY_CATCH_UP_FETCH_LIMIT
)
for house_id in self._house_ids:
_LOGGER.debug("Updating device activity for house id %s", house_id)
try:
activities = await self._hass.async_add_executor_job(
partial(
self._api.get_house_activities,
self._august_gateway.access_token,
house_id,
limit=limit,
)
)
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve activity for house id %s: %s",
house_id,
ex,
)
_LOGGER.debug(
"Completed retrieving device activities for house id %s", house_id
)
updated_device_ids = self._process_newer_device_activities(activities)
if updated_device_ids:
for device_id in updated_device_ids:
_LOGGER.debug(
"async_dispatcher_send (from activity stream): AUGUST_DEVICE_UPDATE-%s",
device_id,
)
async_dispatcher_send(
self._hass, f"{AUGUST_DEVICE_UPDATE}-{device_id}"
)
self._last_update_time = time
def _process_newer_device_activities(self, activities):
updated_device_ids = set()
for activity in activities:
self._latest_activities_by_id_type.setdefault(activity.device_id, {})
lastest_activity = self._latest_activities_by_id_type[
activity.device_id
].get(activity.activity_type)
# Ignore activities that are older than the latest one
if (
lastest_activity
and lastest_activity.activity_start_time >= activity.activity_start_time
):
continue
self._latest_activities_by_id_type[activity.device_id][
activity.activity_type
] = activity
updated_device_ids.add(activity.device_id)
return updated_device_ids

View file

@ -12,12 +12,24 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OCCUPANCY,
BinarySensorDevice, BinarySensorDevice,
) )
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN from .const import (
AUGUST_DEVICE_UPDATE,
DATA_AUGUST,
DEFAULT_NAME,
DOMAIN,
MIN_TIME_BETWEEN_DETAIL_UPDATES,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5) TIME_TO_DECLARE_DETECTION = timedelta(seconds=60)
SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
async def _async_retrieve_online_state(data, detail): async def _async_retrieve_online_state(data, detail):
@ -43,11 +55,13 @@ async def _async_retrieve_ding_state(data, detail):
async def _async_activity_time_based_state(data, device_id, activity_types): async def _async_activity_time_based_state(data, device_id, activity_types):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
latest = await data.async_get_latest_device_activity(device_id, *activity_types) latest = data.activity_stream.async_get_latest_device_activity(
device_id, activity_types
)
if latest is not None: if latest is not None:
start = latest.activity_start_time start = latest.activity_start_time
end = latest.activity_end_time + timedelta(seconds=45) end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
return start <= datetime.now() <= end return start <= datetime.now() <= end
return None return None
@ -98,6 +112,7 @@ class AugustDoorBinarySensor(BinarySensorDevice):
def __init__(self, data, sensor_type, door): def __init__(self, data, sensor_type, door):
"""Initialize the sensor.""" """Initialize the sensor."""
self._undo_dispatch_subscription = None
self._data = data self._data = data
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._door = door self._door = door
@ -128,8 +143,8 @@ class AugustDoorBinarySensor(BinarySensorDevice):
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."""
door_activity = await self._data.async_get_latest_device_activity( door_activity = self._data.activity_stream.async_get_latest_device_activity(
self._door.device_id, ActivityType.DOOR_OPERATION self._door.device_id, [ActivityType.DOOR_OPERATION]
) )
detail = await self._data.async_get_lock_detail(self._door.device_id) detail = await self._data.async_get_lock_detail(self._door.device_id)
@ -162,12 +177,31 @@ class AugustDoorBinarySensor(BinarySensorDevice):
"model": self._model, "model": self._model,
} }
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass, f"{AUGUST_DEVICE_UPDATE}-{self._door.device_id}", update
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
if self._undo_dispatch_subscription:
self._undo_dispatch_subscription()
class AugustDoorbellBinarySensor(BinarySensorDevice): class AugustDoorbellBinarySensor(BinarySensorDevice):
"""Representation of an August binary sensor.""" """Representation of an August binary sensor."""
def __init__(self, data, sensor_type, doorbell): def __init__(self, data, sensor_type, doorbell):
"""Initialize the sensor.""" """Initialize the sensor."""
self._undo_dispatch_subscription = None
self._check_for_off_update_listener = None
self._data = data self._data = data
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._doorbell = doorbell self._doorbell = doorbell
@ -198,6 +232,7 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
async def async_update(self): async def async_update(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
self._cancel_any_pending_updates()
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][ async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
SENSOR_STATE_PROVIDER SENSOR_STATE_PROVIDER
] ]
@ -217,6 +252,28 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
self._firmware_version = detail.firmware_version self._firmware_version = detail.firmware_version
self._model = detail.model self._model = detail.model
self._state = await async_state_provider(self._data, detail) self._state = await async_state_provider(self._data, detail)
if self._state and self.device_class != DEVICE_CLASS_CONNECTIVITY:
self._schedule_update_to_recheck_turn_off_sensor()
def _schedule_update_to_recheck_turn_off_sensor(self):
"""Schedule an update to recheck the sensor to see if it is ready to turn off."""
@callback
def _scheduled_update(now):
"""Timer callback for sensor update."""
_LOGGER.debug("%s: executing scheduled update", self.entity_id)
self.async_schedule_update_ha_state(True)
self._check_for_off_update_listener = None
self._check_for_off_update_listener = async_track_point_in_utc_time(
self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION
)
def _cancel_any_pending_updates(self):
"""Cancel any updates to recheck a sensor to see if it is ready to turn off."""
if self._check_for_off_update_listener:
self._check_for_off_update_listener()
self._check_for_off_update_listener = None
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
@ -236,3 +293,20 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
"sw_version": self._firmware_version, "sw_version": self._firmware_version,
"model": self._model, "model": self._model,
} }
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass, f"{AUGUST_DEVICE_UPDATE}-{self._doorbell.device_id}", update
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
if self._undo_dispatch_subscription:
self._undo_dispatch_subscription()

View file

@ -1,14 +1,22 @@
"""Support for August camera.""" """Support for August doorbell camera."""
from datetime import timedelta
from august.activity import ActivityType from august.activity import ActivityType
from august.util import update_doorbell_image_from_activity from august.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.helpers.dispatcher import async_dispatcher_connect
from .const import DATA_AUGUST, DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN from .const import (
AUGUST_DEVICE_UPDATE,
DATA_AUGUST,
DEFAULT_NAME,
DEFAULT_TIMEOUT,
DOMAIN,
MIN_TIME_BETWEEN_DETAIL_UPDATES,
)
SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
@ -28,6 +36,7 @@ class AugustCamera(Camera):
def __init__(self, data, doorbell, timeout): def __init__(self, data, doorbell, timeout):
"""Initialize a August security camera.""" """Initialize a August security camera."""
super().__init__() super().__init__()
self._undo_dispatch_subscription = None
self._data = data self._data = data
self._doorbell = doorbell self._doorbell = doorbell
self._doorbell_detail = None self._doorbell_detail = None
@ -67,8 +76,8 @@ class AugustCamera(Camera):
self._doorbell_detail = await self._data.async_get_doorbell_detail( self._doorbell_detail = await self._data.async_get_doorbell_detail(
self._doorbell.device_id self._doorbell.device_id
) )
doorbell_activity = await self._data.async_get_latest_device_activity( doorbell_activity = self._data.activity_stream.async_get_latest_device_activity(
self._doorbell.device_id, ActivityType.DOORBELL_MOTION self._doorbell.device_id, [ActivityType.DOORBELL_MOTION]
) )
if doorbell_activity is not None: if doorbell_activity is not None:
@ -117,3 +126,20 @@ class AugustCamera(Camera):
"sw_version": self._firmware_version, "sw_version": self._firmware_version,
"model": self._model, "model": self._model,
} }
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass, f"{AUGUST_DEVICE_UPDATE}-{self._doorbell.device_id}", update
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
if self._undo_dispatch_subscription:
self._undo_dispatch_subscription()

View file

@ -8,6 +8,8 @@ CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
CONF_LOGIN_METHOD = "login_method" CONF_LOGIN_METHOD = "login_method"
CONF_INSTALL_ID = "install_id" CONF_INSTALL_ID = "install_id"
AUGUST_DEVICE_UPDATE = "august_devices_update"
VERIFICATION_CODE_KEY = "verification_code" VERIFICATION_CODE_KEY = "verification_code"
NOTIFICATION_ID = "august_notification" NOTIFICATION_ID = "august_notification"
@ -20,16 +22,14 @@ DATA_AUGUST = "data_august"
DEFAULT_NAME = "August" DEFAULT_NAME = "August"
DOMAIN = "august" DOMAIN = "august"
# Limit battery, online, and hardware updates to 1800 seconds # Limit battery, online, and hardware updates to hourly
# 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_DETAIL_UPDATES = timedelta(seconds=1800) MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1)
# 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_ACTIVITY_UPDATES = timedelta(seconds=10) ACTIVITY_UPDATE_INTERVAL = timedelta(seconds=10)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
LOGIN_METHODS = ["phone", "email"] LOGIN_METHODS = ["phone", "email"]

View file

@ -1,5 +1,4 @@
"""Support for August lock.""" """Support for August lock."""
from datetime import timedelta
import logging import logging
from august.activity import ActivityType from august.activity import ActivityType
@ -8,12 +7,20 @@ 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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN from .const import (
AUGUST_DEVICE_UPDATE,
DATA_AUGUST,
DEFAULT_NAME,
DOMAIN,
MIN_TIME_BETWEEN_DETAIL_UPDATES,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
@ -33,6 +40,7 @@ class AugustLock(LockDevice):
def __init__(self, data, lock): def __init__(self, data, lock):
"""Initialize the lock.""" """Initialize the lock."""
self._undo_dispatch_subscription = None
self._data = data self._data = data
self._lock = lock self._lock = lock
self._lock_status = None self._lock_status = None
@ -58,7 +66,9 @@ class AugustLock(LockDevice):
update_lock_detail_from_activity(self._lock_detail, lock_activity) update_lock_detail_from_activity(self._lock_detail, lock_activity)
if self._update_lock_status_from_detail(): if self._update_lock_status_from_detail():
self.schedule_update_ha_state() await self._data.async_signal_operation_changed_device_state(
self._lock.device_id
)
def _update_lock_status_from_detail(self): def _update_lock_status_from_detail(self):
detail = self._lock_detail detail = self._lock_detail
@ -77,8 +87,8 @@ class AugustLock(LockDevice):
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."""
self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id) self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id)
lock_activity = await self._data.async_get_latest_device_activity( lock_activity = self._data.activity_stream.async_get_latest_device_activity(
self._lock.device_id, ActivityType.LOCK_OPERATION self._lock.device_id, [ActivityType.LOCK_OPERATION]
) )
if lock_activity is not None: if lock_activity is not None:
@ -142,3 +152,20 @@ class AugustLock(LockDevice):
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique id of the lock.""" """Get the unique id of the lock."""
return f"{self._lock.device_id:s}_lock" return f"{self._lock.device_id:s}_lock"
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._undo_dispatch_subscription = async_dispatcher_connect(
self.hass, f"{AUGUST_DEVICE_UPDATE}-{self._lock.device_id}", update
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
if self._undo_dispatch_subscription:
self._undo_dispatch_subscription()

View file

@ -1,11 +1,10 @@
"""Support for August sensors.""" """Support for August sensors."""
from datetime import timedelta
import logging import logging
from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES
BATTERY_LEVEL_FULL = "Full" BATTERY_LEVEL_FULL = "Full"
BATTERY_LEVEL_MEDIUM = "Medium" BATTERY_LEVEL_MEDIUM = "Medium"
@ -13,7 +12,7 @@ BATTERY_LEVEL_LOW = "Low"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
def _retrieve_device_battery_state(detail): def _retrieve_device_battery_state(detail):

View file

@ -1,7 +1,5 @@
"""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.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
@ -20,11 +18,6 @@ from tests.components.august.mocks import (
) )
@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): async def test_doorsense(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(
@ -33,24 +26,32 @@ async def test_doorsense(hass):
lock_details = [lock_one] lock_details = [lock_one]
await _create_august_with_devices(hass, lock_details) await _create_august_with_devices(hass, lock_details)
binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open") binary_sensor_online_with_doorsense_name = hass.states.get(
assert binary_sensor_abc_name.state == STATE_ON "binary_sensor.online_with_doorsense_name_open"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
data = {} data = {}
data[ATTR_ENTITY_ID] = "lock.abc_name" data[ATTR_ENTITY_ID] = "lock.online_with_doorsense_name"
assert await hass.services.async_call( assert await hass.services.async_call(
LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True
) )
await hass.async_block_till_done()
binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open") binary_sensor_online_with_doorsense_name = hass.states.get(
assert binary_sensor_abc_name.state == STATE_ON "binary_sensor.online_with_doorsense_name_open"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_ON
assert await hass.services.async_call( assert await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True
) )
await hass.async_block_till_done()
binary_sensor_abc_name = hass.states.get("binary_sensor.abc_name_open") binary_sensor_online_with_doorsense_name = hass.states.get(
assert binary_sensor_abc_name.state == STATE_OFF "binary_sensor.online_with_doorsense_name_open"
)
assert binary_sensor_online_with_doorsense_name.state == STATE_OFF
async def test_create_doorbell(hass): async def test_create_doorbell(hass):

View file

@ -28,8 +28,8 @@ async def test_lock_device_registry(hass):
reg_device = device_registry.async_get_device( reg_device = device_registry.async_get_device(
identifiers={("august", "online_with_doorsense")}, connections=set() identifiers={("august", "online_with_doorsense")}, connections=set()
) )
assert "AUG-MD01" == reg_device.model assert reg_device.model == "AUG-MD01"
assert "undefined-4.3.0-1.8.14" == reg_device.sw_version assert reg_device.sw_version == "undefined-4.3.0-1.8.14"
async def test_one_lock_operation(hass): async def test_one_lock_operation(hass):