Reduce August doorbell detail updates (#32193)

* 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
pull/32205/head
J. Nick Koston 2020-02-25 21:43:41 -10:00 committed by GitHub
parent b5c1afcb84
commit 638a3025df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 158 additions and 27 deletions

View File

@ -30,8 +30,7 @@ from .const import (
DOMAIN, DOMAIN,
LOGIN_METHODS, LOGIN_METHODS,
MIN_TIME_BETWEEN_ACTIVITY_UPDATES, MIN_TIME_BETWEEN_ACTIVITY_UPDATES,
MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES, MIN_TIME_BETWEEN_DETAIL_UPDATES,
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES,
VERIFICATION_CODE_KEY, VERIFICATION_CODE_KEY,
) )
from .exceptions import InvalidAuth, RequireValidation from .exceptions import InvalidAuth, RequireValidation
@ -296,7 +295,7 @@ class AugustData:
await self._async_update_doorbells_detail() await self._async_update_doorbells_detail()
return self._doorbell_detail_by_id.get(device_id) return self._doorbell_detail_by_id.get(device_id)
@Throttle(MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES) @Throttle(MIN_TIME_BETWEEN_DETAIL_UPDATES)
async def _async_update_doorbells_detail(self): async def _async_update_doorbells_detail(self):
await self._hass.async_add_executor_job(self._update_doorbells_detail) await self._hass.async_add_executor_job(self._update_doorbells_detail)
@ -324,7 +323,7 @@ class AugustData:
if lock.device_id == device_id: if lock.device_id == device_id:
return lock.device_name return lock.device_name
@Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES) @Throttle(MIN_TIME_BETWEEN_DETAIL_UPDATES)
async def _async_update_locks_detail(self): async def _async_update_locks_detail(self):
await self._hass.async_add_executor_job(self._update_locks_detail) await self._hass.async_add_executor_job(self._update_locks_detail)

View File

@ -22,7 +22,7 @@ SCAN_INTERVAL = timedelta(seconds=5)
async def _async_retrieve_online_state(data, detail): async def _async_retrieve_online_state(data, detail):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
return detail.is_online or detail.status == "standby" return detail.is_online or detail.is_standby
async def _async_retrieve_motion_state(data, detail): async def _async_retrieve_motion_state(data, detail):
@ -137,12 +137,13 @@ class AugustDoorBinarySensor(BinarySensorDevice):
update_lock_detail_from_activity(detail, door_activity) update_lock_detail_from_activity(detail, door_activity)
lock_door_state = None lock_door_state = None
self._available = False
if detail is not None: if detail is not None:
lock_door_state = detail.door_state lock_door_state = detail.door_state
self._available = detail.bridge_is_online
self._firmware_version = detail.firmware_version self._firmware_version = detail.firmware_version
self._model = detail.model self._model = detail.model
self._available = lock_door_state != LockDoorStatus.UNKNOWN
self._state = lock_door_state == LockDoorStatus.OPEN self._state = lock_door_state == LockDoorStatus.OPEN
@property @property
@ -208,7 +209,7 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
self._available = True self._available = True
else: else:
self._available = detail is not None and ( self._available = detail is not None and (
detail.is_online or detail.status == "standby" detail.is_online or detail.is_standby
) )
self._state = None self._state = None

View File

@ -1,7 +1,8 @@
"""Support for August camera.""" """Support for August camera."""
from datetime import timedelta from datetime import timedelta
import requests from august.activity import ActivityType
from august.util import update_doorbell_image_from_activity
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
@ -66,6 +67,15 @@ 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(
self._doorbell.device_id, ActivityType.DOORBELL_MOTION
)
if doorbell_activity is not None:
update_doorbell_image_from_activity(
self._doorbell_detail, doorbell_activity
)
if self._doorbell_detail is None: if self._doorbell_detail is None:
return None return None
@ -89,9 +99,8 @@ class AugustCamera(Camera):
self._model = self._doorbell_detail.model self._model = self._doorbell_detail.model
def _camera_image(self): def _camera_image(self):
"""Return bytes of camera image via http get.""" """Return bytes of camera image."""
# Move this to py-august: see issue#32048 return self._doorbell_detail.get_doorbell_image(timeout=self._timeout)
return requests.get(self._image_url, timeout=self._timeout).content
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:

View File

@ -23,13 +23,7 @@ DOMAIN = "august"
# Limit battery, online, 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_DETAIL_UPDATES = timedelta(seconds=1800)
# Doorbells need to update more frequently than locks
# since we get an image from the doorbell api. Once
# 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

View File

@ -67,9 +67,7 @@ class AugustLock(LockDevice):
if detail is not None: if detail is not None:
lock_status = detail.lock_status lock_status = detail.lock_status
self._available = ( self._available = detail.bridge_is_online
lock_status is not None and lock_status != LockStatus.UNKNOWN
)
if self._lock_status != lock_status: if self._lock_status != lock_status:
self._lock_status = lock_status self._lock_status = lock_status

View File

@ -5,7 +5,18 @@ 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 DoorOperationActivity, LockOperationActivity from august.activity import (
ACTIVITY_ACTIONS_DOOR_OPERATION,
ACTIVITY_ACTIONS_DOORBELL_DING,
ACTIVITY_ACTIONS_DOORBELL_MOTION,
ACTIVITY_ACTIONS_DOORBELL_VIEW,
ACTIVITY_ACTIONS_LOCK_OPERATION,
DoorbellDingActivity,
DoorbellMotionActivity,
DoorbellViewActivity,
DoorOperationActivity,
LockOperationActivity,
)
from august.authenticator import AuthenticationState from august.authenticator import AuthenticationState
from august.doorbell import Doorbell, DoorbellDetail from august.doorbell import Doorbell, DoorbellDetail
from august.lock import Lock, LockDetail from august.lock import Lock, LockDetail
@ -45,9 +56,12 @@ async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
return True return True
async def _create_august_with_devices(hass, devices, api_call_side_effects=None): async def _create_august_with_devices(
hass, devices, api_call_side_effects=None, activities=None
):
if api_call_side_effects is None: if api_call_side_effects is None:
api_call_side_effects = {} api_call_side_effects = {}
device_data = { device_data = {
"doorbells": [], "doorbells": [],
"locks": [], "locks": [],
@ -89,6 +103,8 @@ async def _create_august_with_devices(hass, devices, api_call_side_effects=None)
return _get_base_devices("doorbells") return _get_base_devices("doorbells")
def get_house_activities_side_effect(access_token, house_id, limit=10): def get_house_activities_side_effect(access_token, house_id, limit=10):
if activities is not None:
return activities
return [] return []
def lock_return_activities_side_effect(access_token, device_id): def lock_return_activities_side_effect(access_token, device_id):
@ -234,6 +250,17 @@ async def _mock_inoperative_august_lock_detail(hass):
return await _mock_lock_from_fixture(hass, "get_lock.offline.json") return await _mock_lock_from_fixture(hass, "get_lock.offline.json")
async def _mock_activities_from_fixture(hass, path):
json_dict = await _load_json_fixture(hass, path)
activities = []
for activity_json in json_dict:
activity = _activity_from_dict(activity_json)
if activity:
activities.append(activity)
return activities
async def _mock_lock_from_fixture(hass, path): async def _mock_lock_from_fixture(hass, path):
json_dict = await _load_json_fixture(hass, path) json_dict = await _load_json_fixture(hass, path)
return LockDetail(json_dict) return LockDetail(json_dict)
@ -279,3 +306,21 @@ def _mock_door_operation_activity(lock, action):
"action": action, "action": action,
} }
) )
def _activity_from_dict(activity_dict):
action = activity_dict.get("action")
activity_dict["dateTime"] = time.time() * 1000
if action in ACTIVITY_ACTIONS_DOORBELL_DING:
return DoorbellDingActivity(activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_MOTION:
return DoorbellMotionActivity(activity_dict)
if action in ACTIVITY_ACTIONS_DOORBELL_VIEW:
return DoorbellViewActivity(activity_dict)
if action in ACTIVITY_ACTIONS_LOCK_OPERATION:
return LockOperationActivity(activity_dict)
if action in ACTIVITY_ACTIONS_DOOR_OPERATION:
return DoorOperationActivity(activity_dict)
return None

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
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_doorbell_from_fixture, _mock_doorbell_from_fixture,
_mock_lock_from_fixture, _mock_lock_from_fixture,
) )
@ -70,6 +71,10 @@ async def test_create_doorbell(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
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_create_doorbell_offline(hass): async def test_create_doorbell_offline(hass):
@ -90,6 +95,29 @@ async def test_create_doorbell_offline(hass):
assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE
async def test_create_doorbell_with_motion(hass):
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
doorbell_details = [doorbell_one]
activities = await _mock_activities_from_fixture(
hass, "get_activity.doorbell_motion.json"
)
await _create_august_with_devices(hass, doorbell_details, activities=activities)
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_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
async def test_doorbell_device_registry(hass): async def test_doorbell_device_registry(hass):
"""Test creation of a lock with doorsense and bridge ands up in the registry.""" """Test creation of a lock with doorsense and bridge ands up in the registry."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json") doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")

View File

@ -6,7 +6,7 @@ from homeassistant.const import (
SERVICE_LOCK, SERVICE_LOCK,
SERVICE_UNLOCK, SERVICE_UNLOCK,
STATE_LOCKED, STATE_LOCKED,
STATE_UNAVAILABLE, STATE_UNKNOWN,
STATE_UNLOCKED, STATE_UNLOCKED,
) )
@ -80,6 +80,5 @@ async def test_one_lock_unknown_state(hass):
await _create_august_with_devices(hass, lock_details) await _create_august_with_devices(hass, lock_details)
lock_brokenid_name = hass.states.get("lock.brokenid_name") lock_brokenid_name = hass.states.get("lock.brokenid_name")
# Once we have bridge_is_online support in py-august
# this can change to STATE_UNKNOWN assert lock_brokenid_name.state == STATE_UNKNOWN
assert lock_brokenid_name.state == STATE_UNAVAILABLE

View File

@ -0,0 +1,58 @@
[
{
"otherUser" : {
"FirstName" : "Unknown",
"UserName" : "deleteduser",
"LastName" : "User",
"UserID" : "deleted",
"PhoneNo" : "deleted"
},
"dateTime" : 1582663119959,
"deviceID" : "K98GiDT45GUL",
"info" : {
"videoUploadProgress" : "in_progress",
"image" : {
"resource_type" : "image",
"etag" : "fdsf",
"created_at" : "2020-02-25T20:38:39Z",
"type" : "upload",
"format" : "jpg",
"version" : 1582663119,
"secure_url" : "https://res.cloudinary.com/updated_image.jpg",
"signature" : "fdfdfd",
"url" : "http://res.cloudinary.com/updated_image.jpg",
"bytes" : 48545,
"placeholder" : false,
"original_filename" : "file",
"width" : 720,
"tags" : [],
"public_id" : "xnsj5gphpzij9brifpf4",
"height" : 576
},
"dvrID" : "dvr",
"videoAvailable" : false,
"hasSubscription" : false
},
"callingUser" : {
"LastName" : "User",
"UserName" : "deleteduser",
"FirstName" : "Unknown",
"UserID" : "deleted",
"PhoneNo" : "deleted"
},
"house" : {
"houseName" : "K98GiDT45GUL",
"houseID" : "na"
},
"action" : "doorbell_motion_detected",
"deviceType" : "doorbell",
"entities" : {
"otherUser" : "deleted",
"house" : "na",
"device" : "K98GiDT45GUL",
"activity" : "de5585cfd4eae900bb5ba3dc",
"callingUser" : "deleted"
},
"deviceName" : "Front Door"
}
]