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
pull/32094/head
J. Nick Koston 2020-02-23 11:54:35 -10:00 committed by GitHub
parent d2d788631e
commit 693441e56f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 379 additions and 146 deletions

View File

@ -40,18 +40,20 @@ DATA_AUGUST = "august"
DOMAIN = "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
# avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
# Doorbells need to update more frequently than locks
# since we get an image from the doorbell api
MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20)
# 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
# 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)
@ -265,7 +267,7 @@ class AugustData:
activities = await self.async_get_device_activities(device_id, *activity_types)
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):
"""Update data object with latest from August API."""
@ -292,77 +294,32 @@ class AugustData:
_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."""
await self._async_update_doorbells()
return self._doorbell_detail_by_id.get(doorbell_id)
await self._async_update_doorbells_detail()
return self._doorbell_detail_by_id.get(device_id)
@Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES)
async def _async_update_doorbells(self):
await self._hass.async_add_executor_job(self._update_doorbells)
@Throttle(MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES)
async def _async_update_doorbells_detail(self):
await self._hass.async_add_executor_job(self._update_doorbells_detail)
def _update_doorbells(self):
detail_by_id = {}
def _update_doorbells_detail(self):
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")
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):
def lock_has_doorsense(self, device_id):
"""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
# 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 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."""
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):
"""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)
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 = {}
_LOGGER.debug("Start retrieving locks detail")
for lock in self._locks:
_LOGGER.debug("Start retrieving %s detail", device_type)
for device in devices:
device_id = device.device_id
try:
detail_by_id[lock.device_id] = self._api.get_lock_detail(
self._access_token, lock.device_id
)
detail_by_id[device_id] = api_call(self._access_token, device_id)
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve door details for %s. %s",
lock.device_name,
"Request error trying to retrieve %s details for %s. %s",
device_type,
device.device_name,
ex,
)
detail_by_id[lock.device_id] = None
detail_by_id[device_id] = None
except Exception:
detail_by_id[lock.device_id] = None
detail_by_id[device_id] = None
raise
_LOGGER.debug("Completed retrieving locks detail")
self._lock_detail_by_id = detail_by_id
_LOGGER.debug("Completed retrieving %s detail", device_type)
return detail_by_id
def lock(self, device_id):
"""Lock the device."""
return _call_api_operation_that_requires_bridge(
self.get_lock_name(device_id),
"lock",
self._api.lock,
self._api.lock_return_activities,
self._access_token,
device_id,
)
@ -412,7 +374,7 @@ class AugustData:
return _call_api_operation_that_requires_bridge(
self.get_lock_name(device_id),
"unlock",
self._api.unlock,
self._api.unlock_return_activities,
self._access_token,
device_id,
)

View File

@ -8,7 +8,6 @@ from august.util import update_lock_detail_from_activity
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.util import dt
from . import DATA_AUGUST
@ -43,27 +42,31 @@ class AugustLock(LockDevice):
async def async_lock(self, **kwargs):
"""Lock the device."""
update_start_time_utc = dt.utcnow()
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)
await self._call_lock_operation(self._data.lock)
async def async_unlock(self, **kwargs):
"""Unlock the device."""
update_start_time_utc = dt.utcnow()
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)
await self._call_lock_operation(self._data.unlock)
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:
self._lock_status = lock_status
self._data.update_lock_status(
self._lock.device_id, lock_status, update_start_time_utc
self._available = (
lock_status is not None and lock_status != LockStatus.UNKNOWN
)
self.schedule_update_ha_state()
return True
return False
async def async_update(self):
"""Get the latest state of the sensor and update activity."""
@ -76,10 +79,7 @@ class AugustLock(LockDevice):
self._changed_by = lock_activity.operated_by
update_lock_detail_from_activity(self._lock_detail, lock_activity)
self._lock_status = self._lock_detail.lock_status
self._available = (
self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN
)
self._update_lock_status_from_detail()
@property
def name(self):

View File

@ -2,15 +2,16 @@
import datetime
import json
import os
import time
from unittest.mock import MagicMock, PropertyMock
from asynctest import mock
from august.activity import Activity
from august.activity import Activity, DoorOperationActivity, LockOperationActivity
from august.api import Api
from august.authenticator import AuthenticationState
from august.doorbell import Doorbell, DoorbellDetail
from august.exceptions import AugustApiHTTPError
from august.lock import Lock, LockDetail, LockStatus
from august.lock import Lock, LockDetail
from homeassistant.components.august import (
CONF_LOGIN_METHOD,
@ -19,7 +20,6 @@ from homeassistant.components.august import (
DOMAIN,
AugustData,
)
from homeassistant.components.august.binary_sensor import AugustDoorBinarySensor
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
@ -39,44 +39,125 @@ def _mock_get_config():
@mock.patch("homeassistant.components.august.Api")
@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."""
authenticate_mock.side_effect = MagicMock(
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())
await hass.async_block_till_done()
return True
async def _create_august_with_devices(hass, lock_details=[], doorbell_details=[]):
locks = []
doorbells = []
for lock in lock_details:
if isinstance(lock, LockDetail):
locks.append(_mock_august_lock(lock.device_id))
for doorbell in doorbell_details:
if isinstance(lock, DoorbellDetail):
doorbells.append(_mock_august_doorbell(doorbell.device_id))
async def _create_august_with_devices(hass, devices, api_call_side_effects=None):
if api_call_side_effects is None:
api_call_side_effects = {}
device_data = {
"doorbells": [],
"locks": [],
}
for device in devices:
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 api_mocks_callback(api):
def get_lock_detail_side_effect(access_token, device_id):
for lock in lock_details:
if isinstance(lock, LockDetail) and lock.device_id == device_id:
return lock
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
api_instance = MagicMock()
api_instance.get_lock_detail.side_effect = get_lock_detail_side_effect
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
def _get_base_devices(device_type):
base_devices = []
for device in device_data[device_type]:
base_devices.append(device["base"])
return base_devices
await _mock_setup_august(hass, api_mocks_callback)
def get_lock_detail_side_effect(access_token, device_id):
return _get_device_detail("locks", device_id)
return True
def get_operable_locks_side_effect(access_token):
return _get_base_devices("locks")
def get_doorbells_side_effect(access_token):
return _get_base_devices("doorbells")
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):
@ -114,19 +195,6 @@ class MockActivity(Activity):
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):
"""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"):
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 {
"_id": deviceid,
"DeviceID": deviceid,
"DeviceName": deviceid + " Name",
"name": deviceid + " Name",
"HouseID": houseid,
"UserType": "owner",
"SerialNumber": "mockserial",
"serialNumber": "mockserial",
"battery": 90,
"status": "standby",
"currentFirmwareVersion": "mockfirmware",
"Bridge": {
"_id": "bridgeid1",
@ -273,6 +342,11 @@ async def _mock_lock_from_fixture(hass, path):
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):
fixture = await hass.async_add_executor_job(
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)
del doorsense_lock_detail_data["LockStatus"]["doorState"]
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,
}
)

View File

@ -1 +1,71 @@
"""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

View 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

View File

@ -3,9 +3,9 @@
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_LOCK,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_ON,
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."""
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=lock_details)
await _create_august_with_devices(hass, lock_details)
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("friendly_name") == "ABC Name"
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
)
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
View 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"
}