Coordinate all august detail and activity updates (#32249)

* Removing polling from august

* Now using subscribers to the detail and activity

* Fix hash to list keys

* continue to the next house if one fails

* Add async_signal_device_id_update

* Fix double initial update

* Handle self.hass not being available until after async_added_to_hass

* Remove not needed await

* Fix regression with device name
This commit is contained in:
J. Nick Koston 2020-02-27 17:44:23 -10:00 committed by GitHub
parent fefbe02d44
commit 223c01d842
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 385 additions and 460 deletions

View file

@ -5,8 +5,6 @@ import logging
from august.api import AugustApiHTTPError from august.api import AugustApiHTTPError
from august.authenticator import ValidationResult from august.authenticator import ValidationResult
from august.doorbell import Doorbell
from august.lock import Lock
from requests import RequestException from requests import RequestException
import voluptuous as vol import voluptuous as vol
@ -15,13 +13,10 @@ 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 .activity import ActivityStream 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,
@ -36,13 +31,12 @@ from .const import (
) )
from .exceptions import InvalidAuth, RequireValidation from .exceptions import InvalidAuth, RequireValidation
from .gateway import AugustGateway from .gateway import AugustGateway
from .subscriber import AugustSubscriberMixin
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TWO_FA_REVALIDATE = "verify_configurator" TWO_FA_REVALIDATE = "verify_configurator"
DEFAULT_SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
@ -134,7 +128,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() await hass.data[DOMAIN][entry_id][DATA_AUGUST].activity_stream.async_setup()
for component in AUGUST_COMPONENTS: for component in AUGUST_COMPONENTS:
hass.async_create_task( hass.async_create_task(
@ -180,8 +174,6 @@ 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(
*[ *[
@ -197,131 +189,103 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return unload_ok return unload_ok
class AugustData: class AugustData(AugustSubscriberMixin):
"""August data object.""" """August data object."""
def __init__(self, hass, august_gateway): def __init__(self, hass, august_gateway):
"""Init August data object.""" """Init August data object."""
super().__init__(hass, MIN_TIME_BETWEEN_DETAIL_UPDATES)
self._hass = hass self._hass = hass
self._august_gateway = august_gateway self._august_gateway = august_gateway
self._api = august_gateway.api self._api = august_gateway.api
self._device_detail_by_id = {}
self._doorbells = ( locks = self._api.get_operable_locks(self._august_gateway.access_token) or []
self._api.get_doorbells(self._august_gateway.access_token) or [] doorbells = self._api.get_doorbells(self._august_gateway.access_token) or []
self._doorbells_by_id = dict((device.device_id, device) for device in doorbells)
self._locks_by_id = dict((device.device_id, device) for device in locks)
self._house_ids = set(
device.house_id for device in itertools.chain(locks, doorbells)
) )
self._locks = (
self._api.get_operable_locks(self._august_gateway.access_token) or [] self._refresh_device_detail_by_ids(
[device.device_id for device in itertools.chain(locks, doorbells)]
) )
self._house_ids = set()
for device in itertools.chain(self._doorbells, self._locks):
self._house_ids.add(device.house_id)
self._doorbell_detail_by_id = {} # We remove all devices that we are missing
self._lock_detail_by_id = {} # detail as we cannot determine if they are usable.
# This also allows us to avoid checking for
# We check the locks right away so we can # detail being None all over the place
# remove inoperative ones self._remove_inoperative_locks()
self._update_locks_detail() self._remove_inoperative_doorbells()
self._update_doorbells_detail()
self._filter_inoperative_locks()
self.activity_stream = ActivityStream( self.activity_stream = ActivityStream(
hass, self._api, self._august_gateway, self._house_ids hass, self._api, self._august_gateway, self._house_ids
) )
@property
def house_ids(self):
"""Return a list of house_ids."""
return self._house_ids
@property @property
def doorbells(self): def doorbells(self):
"""Return a list of doorbells.""" """Return a list of py-august Doorbell objects."""
return self._doorbells return self._doorbells_by_id.values()
@property @property
def locks(self): def locks(self):
"""Return a list of locks.""" """Return a list of py-august Lock objects."""
return self._locks return self._locks_by_id.values()
async def async_get_device_detail(self, device): def get_device_detail(self, device_id):
"""Return the detail for a device.""" """Return the py-august LockDetail or DoorbellDetail object for a device."""
if isinstance(device, Lock): return self._device_detail_by_id[device_id]
return await self.async_get_lock_detail(device.device_id)
if isinstance(device, Doorbell):
return await self.async_get_doorbell_detail(device.device_id)
raise ValueError
async def async_get_doorbell_detail(self, device_id): def _refresh(self, time):
"""Return doorbell detail.""" self._refresh_device_detail_by_ids(self._subscriptions.keys())
await self._async_update_doorbells_detail()
return self._doorbell_detail_by_id.get(device_id)
@Throttle(MIN_TIME_BETWEEN_DETAIL_UPDATES) def _refresh_device_detail_by_ids(self, device_ids_list):
async def _async_update_doorbells_detail(self): for device_id in device_ids_list:
await self._hass.async_add_executor_job(self._update_doorbells_detail) if device_id in self._locks_by_id:
self._update_device_detail(
def _update_doorbells_detail(self): self._locks_by_id[device_id], self._api.get_lock_detail
self._doorbell_detail_by_id = self._update_device_detail(
"doorbell", self._doorbells, self._api.get_doorbell_detail
)
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[device_id] is None:
return False
return self._lock_detail_by_id[device_id].doorsense
async def async_get_lock_detail(self, device_id):
"""Return lock detail."""
await self._async_update_locks_detail()
return self._lock_detail_by_id[device_id]
def get_device_name(self, device_id):
"""Return doorbell or lock name as August has it stored."""
for device in itertools.chain(self._locks, self._doorbells):
if device.device_id == device_id:
return device.device_name
@Throttle(MIN_TIME_BETWEEN_DETAIL_UPDATES)
async def _async_update_locks_detail(self):
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 %s detail", device_type)
for device in devices:
device_id = device.device_id
detail_by_id[device_id] = None
try:
detail_by_id[device_id] = api_call(
self._august_gateway.access_token, device_id
) )
except RequestException as ex: elif device_id in self._doorbells_by_id:
_LOGGER.error( self._update_device_detail(
"Request error trying to retrieve %s details for %s. %s", self._doorbells_by_id[device_id], self._api.get_doorbell_detail
device_type,
device.device_name,
ex,
) )
_LOGGER.debug(
"signal_device_id_update (from detail updates): %s", device_id,
)
self.signal_device_id_update(device_id)
_LOGGER.debug("Completed retrieving %s detail", device_type) def _update_device_detail(self, device, api_call):
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( _LOGGER.debug(
"async_dispatcher_send (from operation): AUGUST_DEVICE_UPDATE-%s", device_id "Started retrieving detail for %s (%s)",
device.device_name,
device.device_id,
) )
async_dispatcher_send(self._hass, f"{AUGUST_DEVICE_UPDATE}-{device_id}")
try:
self._device_detail_by_id[device.device_id] = api_call(
self._august_gateway.access_token, device.device_id
)
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve %s details for %s. %s",
device.device_id,
device.device_name,
ex,
)
_LOGGER.debug(
"Completed retrieving detail for %s (%s)",
device.device_name,
device.device_id,
)
def _get_device_name(self, device_id):
"""Return doorbell or lock name as August has it stored."""
if self._locks_by_id.get(device_id):
return self._locks_by_id[device_id].device_name
if self._doorbells_by_id.get(device_id):
return self._doorbells_by_id[device_id].device_name
def lock(self, device_id): def lock(self, device_id):
"""Lock the device.""" """Lock the device."""
@ -347,20 +311,41 @@ class AugustData:
try: try:
ret = func(*args, **kwargs) ret = func(*args, **kwargs)
except AugustApiHTTPError as err: except AugustApiHTTPError as err:
device_name = self.get_device_name(device_id) device_name = self._get_device_name(device_id)
if device_name is None: if device_name is None:
device_name = f"DeviceID: {device_id}" device_name = f"DeviceID: {device_id}"
raise HomeAssistantError(f"{device_name}: {err}") raise HomeAssistantError(f"{device_name}: {err}")
return ret return ret
def _filter_inoperative_locks(self): def _remove_inoperative_doorbells(self):
doorbells = list(self.doorbells)
for doorbell in doorbells:
device_id = doorbell.device_id
doorbell_is_operative = False
doorbell_detail = self._device_detail_by_id.get(device_id)
if doorbell_detail is None:
_LOGGER.info(
"The doorbell %s could not be setup because the system could not fetch details about the doorbell.",
doorbell.device_name,
)
else:
doorbell_is_operative = True
if not doorbell_is_operative:
del self._doorbells_by_id[device_id]
del self._device_detail_by_id[device_id]
def _remove_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
# be usable # be usable
operative_locks = [] locks = list(self.locks)
for lock in self._locks:
lock_detail = self._lock_detail_by_id.get(lock.device_id) for lock in locks:
device_id = lock.device_id
lock_is_operative = False
lock_detail = self._device_detail_by_id.get(device_id)
if lock_detail is None: if lock_detail is None:
_LOGGER.info( _LOGGER.info(
"The lock %s could not be setup because the system could not fetch details about the lock.", "The lock %s could not be setup because the system could not fetch details about the lock.",
@ -377,6 +362,8 @@ class AugustData:
lock.device_name, lock.device_name,
) )
else: else:
operative_locks.append(lock) lock_is_operative = True
self._locks = operative_locks if not lock_is_operative:
del self._locks_by_id[device_id]
del self._device_detail_by_id[device_id]

View file

@ -4,12 +4,10 @@ import logging
from requests import RequestException 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 homeassistant.util.dt import utcnow
from .const import ACTIVITY_UPDATE_INTERVAL, AUGUST_DEVICE_UPDATE from .const import ACTIVITY_UPDATE_INTERVAL
from .subscriber import AugustSubscriberMixin
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -17,11 +15,12 @@ ACTIVITY_STREAM_FETCH_LIMIT = 10
ACTIVITY_CATCH_UP_FETCH_LIMIT = 200 ACTIVITY_CATCH_UP_FETCH_LIMIT = 200
class ActivityStream: class ActivityStream(AugustSubscriberMixin):
"""August activity stream handler.""" """August activity stream handler."""
def __init__(self, hass, api, august_gateway, house_ids): def __init__(self, hass, api, august_gateway, house_ids):
"""Init August activity stream object.""" """Init August activity stream object."""
super().__init__(hass, ACTIVITY_UPDATE_INTERVAL)
self._hass = hass self._hass = hass
self._august_gateway = august_gateway self._august_gateway = august_gateway
self._api = api self._api = api
@ -30,22 +29,11 @@ class ActivityStream:
self._last_update_time = None self._last_update_time = None
self._abort_async_track_time_interval = None self._abort_async_track_time_interval = None
async def async_start(self): async def async_setup(self):
"""Start fetching updates from the activity stream.""" """Token refresh check and catch up the activity stream."""
await self._async_update(utcnow) await self._refresh(utcnow)
self._abort_async_track_time_interval = async_track_time_interval(
self._hass, self._async_update, ACTIVITY_UPDATE_INTERVAL
)
@callback def get_latest_device_activity(self, device_id, activity_types):
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.""" """Return latest activity that is one of the acitivty_types."""
if device_id not in self._latest_activities_by_id_type: if device_id not in self._latest_activities_by_id_type:
return None return None
@ -65,14 +53,14 @@ class ActivityStream:
return latest_activity return latest_activity
async def _async_update(self, time): async def _refresh(self, time):
"""Update the activity stream from August.""" """Update the activity stream from August."""
# This is the only place we refresh the api token # This is the only place we refresh the api token
await self._august_gateway.async_refresh_access_token_if_needed() await self._august_gateway.async_refresh_access_token_if_needed()
await self._update_device_activities(time) await self._async_update_device_activities(time)
async def _update_device_activities(self, time): async def _async_update_device_activities(self, time):
_LOGGER.debug("Start retrieving device activities") _LOGGER.debug("Start retrieving device activities")
limit = ( limit = (
@ -98,6 +86,9 @@ class ActivityStream:
house_id, house_id,
ex, ex,
) )
# Make sure we process the next house if one of them fails
continue
_LOGGER.debug( _LOGGER.debug(
"Completed retrieving device activities for house id %s", house_id "Completed retrieving device activities for house id %s", house_id
) )
@ -107,12 +98,10 @@ class ActivityStream:
if updated_device_ids: if updated_device_ids:
for device_id in updated_device_ids: for device_id in updated_device_ids:
_LOGGER.debug( _LOGGER.debug(
"async_dispatcher_send (from activity stream): AUGUST_DEVICE_UPDATE-%s", "async_signal_device_id_update (from activity stream): %s",
device_id, device_id,
) )
async_dispatcher_send( self.async_signal_device_id_update(device_id)
self._hass, f"{AUGUST_DEVICE_UPDATE}-{device_id}"
)
self._last_update_time = time self._last_update_time = time

View file

@ -13,51 +13,45 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, BinarySensorDevice,
) )
from homeassistant.core import callback 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.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from .const import ( from .const import DATA_AUGUST, DOMAIN
AUGUST_DEVICE_UPDATE, from .entity import AugustEntityMixin
DATA_AUGUST,
DEFAULT_NAME,
DOMAIN,
MIN_TIME_BETWEEN_DETAIL_UPDATES,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TIME_TO_DECLARE_DETECTION = timedelta(seconds=60) TIME_TO_DECLARE_DETECTION = timedelta(seconds=60)
SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
def _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."""
# The doorbell will go into standby mode when there is no motion
# for a short while. It will wake by itself when needed so we need
# to consider is available or we will not report motion or dings
return detail.is_online or detail.is_standby return detail.is_online or detail.is_standby
async def _async_retrieve_motion_state(data, detail): def _retrieve_motion_state(data, detail):
return await _async_activity_time_based_state( return _activity_time_based_state(
data, data,
detail.device_id, detail.device_id,
[ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING], [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING],
) )
async def _async_retrieve_ding_state(data, detail): def _retrieve_ding_state(data, detail):
return await _async_activity_time_based_state( return _activity_time_based_state(
data, detail.device_id, [ActivityType.DOORBELL_DING] data, detail.device_id, [ActivityType.DOORBELL_DING]
) )
async def _async_activity_time_based_state(data, device_id, activity_types): def _activity_time_based_state(data, device_id, activity_types):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
latest = data.activity_stream.async_get_latest_device_activity( latest = data.activity_stream.get_latest_device_activity(device_id, activity_types)
device_id, activity_types
)
if latest is not None: if latest is not None:
start = latest.activity_start_time start = latest.activity_start_time
@ -69,15 +63,17 @@ async def _async_activity_time_based_state(data, device_id, activity_types):
SENSOR_NAME = 0 SENSOR_NAME = 0
SENSOR_DEVICE_CLASS = 1 SENSOR_DEVICE_CLASS = 1
SENSOR_STATE_PROVIDER = 2 SENSOR_STATE_PROVIDER = 2
SENSOR_STATE_IS_TIME_BASED = 3
# sensor_type: [name, device_class, async_state_provider] # sensor_type: [name, device_class, state_provider, is_time_based]
SENSOR_TYPES_DOORBELL = { SENSOR_TYPES_DOORBELL = {
"doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _async_retrieve_ding_state], "doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _retrieve_ding_state, True],
"doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _async_retrieve_motion_state], "doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _retrieve_motion_state, True],
"doorbell_online": [ "doorbell_online": [
"Online", "Online",
DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_CONNECTIVITY,
_async_retrieve_online_state, _retrieve_online_state,
False,
], ],
} }
@ -88,8 +84,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
devices = [] devices = []
for door in data.locks: for door in data.locks:
if not data.lock_has_doorsense(door.device_id): detail = data.get_device_detail(door.device_id)
_LOGGER.debug("Not adding sensor class door for lock %s ", door.device_name) if not detail.doorsense:
_LOGGER.debug(
"Not adding sensor class door for lock %s because it does not have doorsense.",
door.device_name,
)
continue continue
_LOGGER.debug("Adding sensor class door for %s", door.device_name) _LOGGER.debug("Adding sensor class door for %s", door.device_name)
@ -107,19 +107,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(devices, True) async_add_entities(devices, True)
class AugustDoorBinarySensor(BinarySensorDevice): class AugustDoorBinarySensor(AugustEntityMixin, BinarySensorDevice):
"""Representation of an August Door binary sensor.""" """Representation of an August Door binary sensor."""
def __init__(self, data, sensor_type, door): def __init__(self, data, sensor_type, device):
"""Initialize the sensor.""" """Initialize the sensor."""
self._undo_dispatch_subscription = None super().__init__(data, device)
self._data = data self._data = data
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._door = door self._device = device
self._state = None self._state = None
self._available = False self._available = False
self._firmware_version = None self._update_from_data()
self._model = None
@property @property
def available(self): def available(self):
@ -139,76 +138,43 @@ class AugustDoorBinarySensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return f"{self._door.device_name} Open" return f"{self._device.device_name} Open"
async def async_update(self): @callback
def _update_from_data(self):
"""Get the latest state of the sensor and update activity.""" """Get the latest state of the sensor and update activity."""
door_activity = self._data.activity_stream.async_get_latest_device_activity( door_activity = self._data.activity_stream.get_latest_device_activity(
self._door.device_id, [ActivityType.DOOR_OPERATION] self._device_id, [ActivityType.DOOR_OPERATION]
) )
detail = await self._data.async_get_lock_detail(self._door.device_id) detail = self._detail
if door_activity is not None: if door_activity is not None:
update_lock_detail_from_activity(detail, door_activity) update_lock_detail_from_activity(detail, door_activity)
lock_door_state = None lock_door_state = detail.door_state
self._available = False self._available = detail.bridge_is_online
if detail is not None:
lock_door_state = detail.door_state
self._available = detail.bridge_is_online
self._firmware_version = detail.firmware_version
self._model = detail.model
self._state = lock_door_state == LockDoorStatus.OPEN self._state = lock_door_state == LockDoorStatus.OPEN
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique of the door open binary sensor.""" """Get the unique of the door open binary sensor."""
return f"{self._door.device_id}_open" return f"{self._device_id}_open"
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._door.device_id)},
"name": self._door.device_name,
"manufacturer": DEFAULT_NAME,
"sw_version": self._firmware_version,
"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(AugustEntityMixin, 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, device):
"""Initialize the sensor.""" """Initialize the sensor."""
self._undo_dispatch_subscription = None super().__init__(data, device)
self._check_for_off_update_listener = 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._device = device
self._state = None self._state = None
self._available = False self._available = False
self._firmware_version = None self._update_from_data()
self._model = None
@property @property
def available(self): def available(self):
@ -228,42 +194,47 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return f"{self._doorbell.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}" return f"{self._device.device_name} {SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME]}"
async def async_update(self): @property
def _state_provider(self):
"""Return the state provider for the binary sensor."""
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_PROVIDER]
@property
def _is_time_based(self):
"""Return true of false if the sensor is time based."""
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_STATE_IS_TIME_BASED]
@callback
def _update_from_data(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
self._cancel_any_pending_updates() self._cancel_any_pending_updates()
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][ self._state = self._state_provider(self._data, self._detail)
SENSOR_STATE_PROVIDER
]
detail = await self._data.async_get_doorbell_detail(self._doorbell.device_id)
# The doorbell will go into standby mode when there is no motion
# for a short while. It will wake by itself when needed so we need
# to consider is available or we will not report motion or dings
if self.device_class == DEVICE_CLASS_CONNECTIVITY:
self._available = True
else:
self._available = detail is not None and (
detail.is_online or detail.is_standby
)
self._state = None if self._is_time_based:
if detail is not None: self._available = _retrieve_online_state(self._data, self._detail)
self._firmware_version = detail.firmware_version self._schedule_update_to_recheck_turn_off_sensor()
self._model = detail.model else:
self._state = await async_state_provider(self._data, detail) self._available = True
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): 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.""" """Schedule an update to recheck the sensor to see if it is ready to turn off."""
# If the sensor is already off there is nothing to do
if not self._state:
return
# self.hass is only available after setup is completed
# and we will recheck in async_added_to_hass
if not self.hass:
return
@callback @callback
def _scheduled_update(now): def _scheduled_update(now):
"""Timer callback for sensor update.""" """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 = None
self._update_from_data()
self._check_for_off_update_listener = async_track_point_in_utc_time( self._check_for_off_update_listener = async_track_point_in_utc_time(
self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION self.hass, _scheduled_update, utcnow() + TIME_TO_DECLARE_DETECTION
@ -272,41 +243,19 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
def _cancel_any_pending_updates(self): def _cancel_any_pending_updates(self):
"""Cancel any updates to recheck a sensor to see if it is ready to turn off.""" """Cancel any updates to recheck a sensor to see if it is ready to turn off."""
if self._check_for_off_update_listener: if self._check_for_off_update_listener:
_LOGGER.debug("%s: canceled pending update", self.entity_id)
self._check_for_off_update_listener() self._check_for_off_update_listener()
self._check_for_off_update_listener = None self._check_for_off_update_listener = None
async def async_added_to_hass(self):
"""Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed."""
self._schedule_update_to_recheck_turn_off_sensor()
await super().async_added_to_hass()
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique id of the doorbell sensor.""" """Get the unique id of the doorbell sensor."""
return ( return (
f"{self._doorbell.device_id}_" f"{self._device_id}_"
f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}" f"{SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower()}"
) )
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._doorbell.device_id)},
"name": self._doorbell.device_name,
"manufacturer": "August",
"sw_version": self._firmware_version,
"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

@ -5,18 +5,9 @@ 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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ( from .const import DATA_AUGUST, DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN
AUGUST_DEVICE_UPDATE, from .entity import AugustEntityMixin
DATA_AUGUST,
DEFAULT_NAME,
DEFAULT_TIMEOUT,
DOMAIN,
MIN_TIME_BETWEEN_DETAIL_UPDATES,
)
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):
@ -30,31 +21,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(devices, True) async_add_entities(devices, True)
class AugustCamera(Camera): class AugustCamera(AugustEntityMixin, Camera):
"""An implementation of a August security camera.""" """An implementation of a August security camera."""
def __init__(self, data, doorbell, timeout): def __init__(self, data, device, timeout):
"""Initialize a August security camera.""" """Initialize a August security camera."""
super().__init__() super().__init__(data, device)
self._undo_dispatch_subscription = None
self._data = data self._data = data
self._doorbell = doorbell self._device = device
self._doorbell_detail = None
self._timeout = timeout self._timeout = timeout
self._image_url = None self._image_url = None
self._image_content = None self._image_content = None
self._firmware_version = None
self._model = None
@property @property
def name(self): def name(self):
"""Return the name of this device.""" """Return the name of this device."""
return self._doorbell.device_name return f"{self._device.device_name} Camera"
@property @property
def is_recording(self): def is_recording(self):
"""Return true if the device is recording.""" """Return true if the device is recording."""
return self._doorbell.has_subscription return self._device.has_subscription
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
@ -69,77 +56,34 @@ class AugustCamera(Camera):
@property @property
def model(self): def model(self):
"""Return the camera model.""" """Return the camera model."""
return self._model return self._detail.model
async def async_camera_image(self): @callback
"""Return bytes of camera image.""" def _update_from_data(self):
self._doorbell_detail = await self._data.async_get_doorbell_detail( """Get the latest state of the sensor."""
self._doorbell.device_id doorbell_activity = self._data.activity_stream.get_latest_device_activity(
) self._device_id, [ActivityType.DOORBELL_MOTION]
doorbell_activity = self._data.activity_stream.async_get_latest_device_activity(
self._doorbell.device_id, [ActivityType.DOORBELL_MOTION]
) )
if doorbell_activity is not None: if doorbell_activity is not None:
update_doorbell_image_from_activity( update_doorbell_image_from_activity(self._detail, doorbell_activity)
self._doorbell_detail, doorbell_activity
)
if self._doorbell_detail is None: async def async_camera_image(self):
return None """Return bytes of camera image."""
self._update_from_data()
if self._image_url is not self._doorbell_detail.image_url: if self._image_url is not self._detail.image_url:
self._image_url = self._doorbell_detail.image_url self._image_url = self._detail.image_url
self._image_content = await self.hass.async_add_executor_job( self._image_content = await self.hass.async_add_executor_job(
self._camera_image self._camera_image
) )
return self._image_content return self._image_content
async def async_update(self):
"""Update camera data."""
self._doorbell_detail = await self._data.async_get_doorbell_detail(
self._doorbell.device_id
)
if self._doorbell_detail is None:
return None
self._firmware_version = self._doorbell_detail.firmware_version
self._model = self._doorbell_detail.model
def _camera_image(self): def _camera_image(self):
"""Return bytes of camera image.""" """Return bytes of camera image."""
return self._doorbell_detail.get_doorbell_image(timeout=self._timeout) return self._detail.get_doorbell_image(timeout=self._timeout)
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique id of the camera.""" """Get the unique id of the camera."""
return f"{self._doorbell.device_id:s}_camera" return f"{self._device_id:s}_camera"
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._doorbell.device_id)},
"name": self._doorbell.device_name + " Camera",
"manufacturer": DEFAULT_NAME,
"sw_version": self._firmware_version,
"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,8 +8,6 @@ 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"

View file

@ -0,0 +1,67 @@
"""Base class for August entity."""
import logging
from homeassistant.core import callback
from . import DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
class AugustEntityMixin:
"""Base implementation for August device."""
def __init__(self, data, device):
"""Initialize an August device."""
super().__init__()
self._data = data
self._device = device
self._undo_dispatch_subscription = None
@property
def should_poll(self):
"""Return False, updates are controlled via the hub."""
return False
@property
def _device_id(self):
return self._device.device_id
@property
def _detail(self):
return self._data.get_device_detail(self._device.device_id)
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._device_id)},
"name": self._device.device_name,
"manufacturer": DEFAULT_NAME,
"sw_version": self._detail.firmware_version,
"model": self._detail.model,
}
@callback
def _update_from_data_and_write_state(self):
self._update_from_data()
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Subscribe to updates."""
self._data.async_subscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
self._data.activity_stream.async_subscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
async def async_will_remove_from_hass(self):
"""Undo subscription."""
self._data.async_unsubscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)
self._data.activity_stream.async_unsubscribe_device_id(
self._device_id, self._update_from_data_and_write_state
)

View file

@ -8,20 +8,12 @@ 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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ( from .const import DATA_AUGUST, DOMAIN
AUGUST_DEVICE_UPDATE, from .entity import AugustEntityMixin
DATA_AUGUST,
DEFAULT_NAME,
DOMAIN,
MIN_TIME_BETWEEN_DETAIL_UPDATES,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
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):
"""Set up August locks.""" """Set up August locks."""
@ -35,20 +27,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(devices, True) async_add_entities(devices, True)
class AugustLock(LockDevice): class AugustLock(AugustEntityMixin, LockDevice):
"""Representation of an August lock.""" """Representation of an August lock."""
def __init__(self, data, lock): def __init__(self, data, device):
"""Initialize the lock.""" """Initialize the lock."""
self._undo_dispatch_subscription = None super().__init__(data, device)
self._data = data self._data = data
self._lock = lock self._device = device
self._lock_status = None self._lock_status = None
self._lock_detail = None
self._changed_by = None self._changed_by = None
self._available = False self._available = False
self._firmware_version = None self._update_from_data()
self._model = None
async def async_lock(self, **kwargs): async def async_lock(self, **kwargs):
"""Lock the device.""" """Lock the device."""
@ -60,52 +50,47 @@ class AugustLock(LockDevice):
async def _call_lock_operation(self, lock_operation): async def _call_lock_operation(self, lock_operation):
activities = await self.hass.async_add_executor_job( activities = await self.hass.async_add_executor_job(
lock_operation, self._lock.device_id lock_operation, self._device_id
) )
detail = self._detail
for lock_activity in activities: for lock_activity in activities:
update_lock_detail_from_activity(self._lock_detail, lock_activity) update_lock_detail_from_activity(detail, lock_activity)
if self._update_lock_status_from_detail(): if self._update_lock_status_from_detail():
await self._data.async_signal_operation_changed_device_state( _LOGGER.debug(
self._lock.device_id "async_signal_device_id_update (from lock operation): %s",
self._device_id,
) )
self._data.async_signal_device_id_update(self._device_id)
def _update_lock_status_from_detail(self): def _update_lock_status_from_detail(self):
detail = self._lock_detail detail = self._detail
lock_status = None lock_status = detail.lock_status
self._available = False self._available = detail.bridge_is_online
if detail is not None:
lock_status = detail.lock_status
self._available = detail.bridge_is_online
if self._lock_status != lock_status: if self._lock_status != lock_status:
self._lock_status = lock_status self._lock_status = lock_status
return True return True
return False return False
async def async_update(self): @callback
def _update_from_data(self):
"""Get the latest state of the sensor and update activity.""" """Get the latest state of the sensor and update activity."""
self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id) lock_detail = self._detail
lock_activity = self._data.activity_stream.async_get_latest_device_activity( lock_activity = self._data.activity_stream.get_latest_device_activity(
self._lock.device_id, [ActivityType.LOCK_OPERATION] self._device_id, [ActivityType.LOCK_OPERATION]
) )
if lock_activity is not None: if lock_activity is not None:
self._changed_by = lock_activity.operated_by self._changed_by = lock_activity.operated_by
if self._lock_detail is not None: update_lock_detail_from_activity(lock_detail, lock_activity)
update_lock_detail_from_activity(self._lock_detail, lock_activity)
if self._lock_detail is not None:
self._firmware_version = self._lock_detail.firmware_version
self._model = self._lock_detail.model
self._update_lock_status_from_detail() self._update_lock_status_from_detail()
@property @property
def name(self): def name(self):
"""Return the name of this device.""" """Return the name of this device."""
return self._lock.device_name return self._device.device_name
@property @property
def available(self): def available(self):
@ -127,45 +112,14 @@ class AugustLock(LockDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
if self._lock_detail is None: attributes = {ATTR_BATTERY_LEVEL: self._detail.battery_level}
return None
attributes = {ATTR_BATTERY_LEVEL: self._lock_detail.battery_level} if self._detail.keypad is not None:
attributes["keypad_battery_level"] = self._detail.keypad.battery_level
if self._lock_detail.keypad is not None:
attributes["keypad_battery_level"] = self._lock_detail.keypad.battery_level
return attributes return attributes
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._lock.device_id)},
"name": self._lock.device_name,
"manufacturer": DEFAULT_NAME,
"sw_version": self._firmware_version,
"model": self._model,
}
@property @property
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._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

@ -2,9 +2,11 @@
import logging import logging
from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES from .const import DATA_AUGUST, DOMAIN
from .entity import AugustEntityMixin
BATTERY_LEVEL_FULL = "Full" BATTERY_LEVEL_FULL = "Full"
BATTERY_LEVEL_MEDIUM = "Medium" BATTERY_LEVEL_MEDIUM = "Medium"
@ -12,22 +14,14 @@ BATTERY_LEVEL_LOW = "Low"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = MIN_TIME_BETWEEN_DETAIL_UPDATES
def _retrieve_device_battery_state(detail): def _retrieve_device_battery_state(detail):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
if detail is None:
return None
return detail.battery_level return detail.battery_level
def _retrieve_linked_keypad_battery_state(detail): def _retrieve_linked_keypad_battery_state(detail):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
if detail is None:
return None
if detail.keypad is None: if detail.keypad is None:
return None return None
@ -73,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor_type in SENSOR_TYPES_BATTERY: for sensor_type in SENSOR_TYPES_BATTERY:
for device in batteries[sensor_type]: for device in batteries[sensor_type]:
state_provider = SENSOR_TYPES_BATTERY[sensor_type]["state_provider"] state_provider = SENSOR_TYPES_BATTERY[sensor_type]["state_provider"]
detail = await data.async_get_device_detail(device) detail = data.get_device_detail(device.device_id)
state = state_provider(detail) state = state_provider(detail)
sensor_name = SENSOR_TYPES_BATTERY[sensor_type]["name"] sensor_name = SENSOR_TYPES_BATTERY[sensor_type]["name"]
if state is None: if state is None:
@ -91,18 +85,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(devices, True) async_add_entities(devices, True)
class AugustBatterySensor(Entity): class AugustBatterySensor(AugustEntityMixin, Entity):
"""Representation of an August sensor.""" """Representation of an August sensor."""
def __init__(self, data, sensor_type, device): def __init__(self, data, sensor_type, device):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(data, device)
self._data = data self._data = data
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._device = device self._device = device
self._state = None self._state = None
self._available = False self._available = False
self._firmware_version = None self._update_from_data()
self._model = None
@property @property
def available(self): def available(self):
@ -131,28 +125,14 @@ class AugustBatterySensor(Entity):
sensor_name = SENSOR_TYPES_BATTERY[self._sensor_type]["name"] sensor_name = SENSOR_TYPES_BATTERY[self._sensor_type]["name"]
return f"{device_name} {sensor_name}" return f"{device_name} {sensor_name}"
async def async_update(self): @callback
def _update_from_data(self):
"""Get the latest state of the sensor.""" """Get the latest state of the sensor."""
state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"] state_provider = SENSOR_TYPES_BATTERY[self._sensor_type]["state_provider"]
detail = await self._data.async_get_device_detail(self._device) self._state = state_provider(self._detail)
self._state = state_provider(detail)
self._available = self._state is not None self._available = self._state is not None
if detail is not None:
self._firmware_version = detail.firmware_version
self._model = detail.model
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Get the unique id of the device sensor.""" """Get the unique id of the device sensor."""
return f"{self._device.device_id}_{self._sensor_type}" return f"{self._device_id}_{self._sensor_type}"
@property
def device_info(self):
"""Return the device_info of the device."""
return {
"identifiers": {(DOMAIN, self._device.device_id)},
"name": self._device.device_name,
"manufacturer": DEFAULT_NAME,
"sw_version": self._firmware_version,
"model": self._model,
}

View file

@ -0,0 +1,53 @@
"""Base class for August entity."""
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_interval
class AugustSubscriberMixin:
"""Base implementation for a subscriber."""
def __init__(self, hass, update_interval):
"""Initialize an subscriber."""
super().__init__()
self._hass = hass
self._update_interval = update_interval
self._subscriptions = {}
self._unsub_interval = None
@callback
def async_subscribe_device_id(self, device_id, update_callback):
"""Add an callback subscriber."""
if not self._subscriptions:
self._unsub_interval = async_track_time_interval(
self._hass, self._refresh, self._update_interval
)
self._subscriptions.setdefault(device_id, []).append(update_callback)
@callback
def async_unsubscribe_device_id(self, device_id, update_callback):
"""Remove a callback subscriber."""
self._subscriptions[device_id].remove(update_callback)
if not self._subscriptions[device_id]:
del self._subscriptions[device_id]
if not self._subscriptions:
self._unsub_interval()
self._unsub_interval = None
@callback
def async_signal_device_id_update(self, device_id):
"""Call the callbacks for a device_id."""
if not self._subscriptions.get(device_id):
return
for update_callback in self._subscriptions[device_id]:
update_callback()
def signal_device_id_update(self, device_id):
"""Call the callbacks for a device_id."""
if not self._subscriptions.get(device_id):
return
for update_callback in self._subscriptions[device_id]:
self._hass.loop.call_soon_threadsafe(update_callback)

View file

@ -130,5 +130,7 @@ async def test_doorbell_device_registry(hass):
reg_device = device_registry.async_get_device( reg_device = device_registry.async_get_device(
identifiers={("august", "tmt100")}, connections=set() identifiers={("august", "tmt100")}, connections=set()
) )
assert "hydra1" == reg_device.model assert reg_device.model == "hydra1"
assert "3.1.0-HYDRC75+201909251139" == reg_device.sw_version assert reg_device.name == "tmt100 Name"
assert reg_device.manufacturer == "August"
assert reg_device.sw_version == "3.1.0-HYDRC75+201909251139"

View file

@ -14,5 +14,5 @@ async def test_create_doorbell(hass):
doorbell_details = [doorbell_one] doorbell_details = [doorbell_one]
await _create_august_with_devices(hass, doorbell_details) await _create_august_with_devices(hass, doorbell_details)
camera_k98gidt45gul_name = hass.states.get("camera.k98gidt45gul_name") camera_k98gidt45gul_name_camera = hass.states.get("camera.k98gidt45gul_name_camera")
assert camera_k98gidt45gul_name.state == STATE_IDLE assert camera_k98gidt45gul_name_camera.state == STATE_IDLE

View file

@ -30,6 +30,8 @@ async def test_lock_device_registry(hass):
) )
assert reg_device.model == "AUG-MD01" assert reg_device.model == "AUG-MD01"
assert reg_device.sw_version == "undefined-4.3.0-1.8.14" assert reg_device.sw_version == "undefined-4.3.0-1.8.14"
assert reg_device.name == "online_with_doorsense Name"
assert reg_device.manufacturer == "August"
async def test_one_lock_operation(hass): async def test_one_lock_operation(hass):