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:
parent
fefbe02d44
commit
223c01d842
12 changed files with 385 additions and 460 deletions
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
67
homeassistant/components/august/entity.py
Normal file
67
homeassistant/components/august/entity.py
Normal 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
|
||||||
|
)
|
|
@ -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()
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
53
homeassistant/components/august/subscriber.py
Normal file
53
homeassistant/components/august/subscriber.py
Normal 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)
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue