Add Config flow to august (#32133)

* Add battery sensors for August devices

* Additional tests and cleanup in prep for config flow
  and device registry

* pylint

* update name for new style guidelines - https://developers.home-assistant.io/docs/development_guidelines/#use-new-style-string-formatting

* Config Flow for august push

* Update homeassistant/components/august/__init__.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* Address review items

* Update tests

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
pull/32184/head
J. Nick Koston 2020-02-25 08:18:15 -10:00 committed by GitHub
parent 900714a3ee
commit 2925e0617c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1686 additions and 444 deletions

View File

@ -0,0 +1,32 @@
{
"config" : {
"error" : {
"unknown" : "Unexpected error",
"cannot_connect" : "Failed to connect, please try again",
"invalid_auth" : "Invalid authentication"
},
"abort" : {
"already_configured" : "Account is already configured"
},
"step" : {
"validation" : {
"title" : "Two factor authentication",
"data" : {
"code" : "Verification code"
},
"description" : "Please check your {login_method} ({username}) and enter the verification code below"
},
"user" : {
"description" : "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
"data" : {
"timeout" : "Timeout (seconds)",
"password" : "Password",
"username" : "Username",
"login_method" : "Login Method"
},
"title" : "Setup an August account"
}
},
"title" : "August"
}
}

View File

@ -4,62 +4,45 @@ from datetime import timedelta
from functools import partial
import logging
from august.api import Api, AugustApiHTTPError
from august.authenticator import AuthenticationState, Authenticator, ValidationResult
from requests import RequestException, Session
from august.api import AugustApiHTTPError
from august.authenticator import ValidationResult
from august.doorbell import Doorbell
from august.lock import Lock
from requests import RequestException
import voluptuous as vol
from homeassistant.const import (
CONF_PASSWORD,
CONF_TIMEOUT,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from .const import (
AUGUST_COMPONENTS,
CONF_ACCESS_TOKEN_CACHE_FILE,
CONF_INSTALL_ID,
CONF_LOGIN_METHOD,
DATA_AUGUST,
DEFAULT_AUGUST_CONFIG_FILE,
DEFAULT_NAME,
DEFAULT_TIMEOUT,
DOMAIN,
LOGIN_METHODS,
MIN_TIME_BETWEEN_ACTIVITY_UPDATES,
MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES,
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES,
VERIFICATION_CODE_KEY,
)
from .exceptions import InvalidAuth, RequireValidation
from .gateway import AugustGateway
_LOGGER = logging.getLogger(__name__)
_CONFIGURING = {}
DEFAULT_TIMEOUT = 10
ACTIVITY_FETCH_LIMIT = 10
ACTIVITY_INITIAL_FETCH_LIMIT = 20
CONF_LOGIN_METHOD = "login_method"
CONF_INSTALL_ID = "install_id"
NOTIFICATION_ID = "august_notification"
NOTIFICATION_TITLE = "August Setup"
AUGUST_CONFIG_FILE = ".august.conf"
DATA_AUGUST = "august"
DOMAIN = "august"
DEFAULT_ENTITY_NAMESPACE = "august"
# Limit battery, online, and hardware updates to 1800 seconds
# in order to reduce the number of api requests and
# avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
# Doorbells need to update more frequently than locks
# since we get an image from the doorbell api. Once
# py-august 0.18.0 is released doorbell status updates
# can be reduced in the same was as locks have been
MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES = timedelta(seconds=20)
# Activity needs to be checked more frequently as the
# doorbell motion and rings are included here
MIN_TIME_BETWEEN_ACTIVITY_UPDATES = timedelta(seconds=10)
TWO_FA_REVALIDATE = "verify_configurator"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
LOGIN_METHODS = ["phone", "email"]
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
@ -75,138 +58,159 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
async def async_request_validation(hass, config_entry, august_gateway):
"""Request a new verification code from the user."""
def request_configuration(hass, config, api, authenticator, token_refresh_lock):
"""Request configuration steps from the user."""
#
# In the future this should start a new config flow
# instead of using the legacy configurator
#
_LOGGER.error("Access token is no longer valid.")
configurator = hass.components.configurator
entry_id = config_entry.entry_id
def august_configuration_callback(data):
"""Run when the configuration callback is called."""
result = authenticator.validate_verification_code(data.get("verification_code"))
async def async_august_configuration_validation_callback(data):
code = data.get(VERIFICATION_CODE_KEY)
result = await hass.async_add_executor_job(
august_gateway.authenticator.validate_verification_code, code
)
if result == ValidationResult.INVALID_VERIFICATION_CODE:
configurator.notify_errors(
_CONFIGURING[DOMAIN], "Invalid verification code"
configurator.async_notify_errors(
hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE],
"Invalid verification code, please make sure you are using the latest code and try again.",
)
elif result == ValidationResult.VALIDATED:
setup_august(hass, config, api, authenticator, token_refresh_lock)
return await async_setup_august(hass, config_entry, august_gateway)
if DOMAIN not in _CONFIGURING:
authenticator.send_verification_code()
return False
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
login_method = conf.get(CONF_LOGIN_METHOD)
if TWO_FA_REVALIDATE not in hass.data[DOMAIN][entry_id]:
await hass.async_add_executor_job(
august_gateway.authenticator.send_verification_code
)
_CONFIGURING[DOMAIN] = configurator.request_config(
NOTIFICATION_TITLE,
august_configuration_callback,
description=f"Please check your {login_method} ({username}) and enter the verification code below",
entry_data = config_entry.data
login_method = entry_data.get(CONF_LOGIN_METHOD)
username = entry_data.get(CONF_USERNAME)
hass.data[DOMAIN][entry_id][TWO_FA_REVALIDATE] = configurator.async_request_config(
f"{DEFAULT_NAME} ({username})",
async_august_configuration_validation_callback,
description="August must be re-verified. Please check your {} ({}) and enter the verification "
"code below".format(login_method, username),
submit_caption="Verify",
fields=[
{"id": "verification_code", "name": "Verification code", "type": "string"}
{"id": VERIFICATION_CODE_KEY, "name": "Verification code", "type": "string"}
],
)
return
def setup_august(hass, config, api, authenticator, token_refresh_lock):
async def async_setup_august(hass, config_entry, august_gateway):
"""Set up the August component."""
authentication = None
entry_id = config_entry.entry_id
hass.data[DOMAIN].setdefault(entry_id, {})
try:
authentication = authenticator.authenticate()
except RequestException as ex:
_LOGGER.error("Unable to connect to August service: %s", str(ex))
hass.components.persistent_notification.create(
"Error: {ex}<br />You will need to restart hass after fixing.",
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID,
)
state = authentication.state
if state == AuthenticationState.AUTHENTICATED:
if DOMAIN in _CONFIGURING:
hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
hass.data[DATA_AUGUST] = AugustData(
hass, api, authentication, authenticator, token_refresh_lock
)
for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
if state == AuthenticationState.BAD_PASSWORD:
_LOGGER.error("Invalid password provided")
august_gateway.authenticate()
except RequireValidation:
await async_request_validation(hass, config_entry, august_gateway)
return False
if state == AuthenticationState.REQUIRES_VALIDATION:
request_configuration(hass, config, api, authenticator, token_refresh_lock)
except InvalidAuth:
_LOGGER.error("Password is no longer valid. Please set up August again")
return False
# We still use the configurator to get a new 2fa code
# when needed since config_flow doesn't have a way
# to re-request if it expires
if TWO_FA_REVALIDATE in hass.data[DOMAIN][entry_id]:
hass.components.configurator.async_request_done(
hass.data[DOMAIN][entry_id].pop(TWO_FA_REVALIDATE)
)
hass.data[DOMAIN][entry_id][DATA_AUGUST] = await hass.async_add_executor_job(
AugustData, hass, august_gateway
)
for component in AUGUST_COMPONENTS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
return True
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the August component from YAML."""
conf = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})
if not conf:
return True
return False
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_LOGIN_METHOD: conf.get(CONF_LOGIN_METHOD),
CONF_USERNAME: conf.get(CONF_USERNAME),
CONF_PASSWORD: conf.get(CONF_PASSWORD),
CONF_INSTALL_ID: conf.get(CONF_INSTALL_ID),
CONF_ACCESS_TOKEN_CACHE_FILE: DEFAULT_AUGUST_CONFIG_FILE,
},
)
)
return True
async def async_setup(hass, config):
"""Set up the August component."""
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up August from a config entry."""
conf = config[DOMAIN]
api_http_session = None
try:
api_http_session = Session()
except RequestException as ex:
_LOGGER.warning("Creating HTTP session failed with: %s", str(ex))
august_gateway = AugustGateway(hass)
august_gateway.async_setup(entry.data)
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
return await async_setup_august(hass, entry, august_gateway)
authenticator = Authenticator(
api,
conf.get(CONF_LOGIN_METHOD),
conf.get(CONF_USERNAME),
conf.get(CONF_PASSWORD),
install_id=conf.get(CONF_INSTALL_ID),
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE),
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in AUGUST_COMPONENTS
]
)
)
def close_http_session(event):
"""Close API sessions used to connect to August."""
_LOGGER.debug("Closing August HTTP sessions")
if api_http_session:
try:
api_http_session.close()
except RequestException:
pass
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
_LOGGER.debug("August HTTP session closed.")
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
_LOGGER.debug("Registered for Home Assistant stop event")
token_refresh_lock = asyncio.Lock()
return await hass.async_add_executor_job(
setup_august, hass, config, api, authenticator, token_refresh_lock
)
return unload_ok
class AugustData:
"""August data object."""
def __init__(self, hass, api, authentication, authenticator, token_refresh_lock):
DEFAULT_ACTIVITY_FETCH_LIMIT = 10
def __init__(self, hass, august_gateway):
"""Init August data object."""
self._hass = hass
self._api = api
self._authenticator = authenticator
self._access_token = authentication.access_token
self._access_token_expires = authentication.access_token_expires
self._august_gateway = august_gateway
self._api = august_gateway.api
self._token_refresh_lock = token_refresh_lock
self._doorbells = self._api.get_doorbells(self._access_token) or []
self._locks = self._api.get_operable_locks(self._access_token) or []
self._doorbells = (
self._api.get_doorbells(self._august_gateway.access_token) or []
)
self._locks = (
self._api.get_operable_locks(self._august_gateway.access_token) or []
)
self._house_ids = set()
for device in self._doorbells + self._locks:
self._house_ids.add(device.house_id)
@ -218,7 +222,7 @@ class AugustData:
# We check the locks right away so we can
# remove inoperative ones
self._update_locks_detail()
self._update_doorbells_detail()
self._filter_inoperative_locks()
@property
@ -236,22 +240,6 @@ class AugustData:
"""Return a list of locks."""
return self._locks
async def _async_refresh_access_token_if_needed(self):
"""Refresh the august access token if needed."""
if self._authenticator.should_refresh():
async with self._token_refresh_lock:
await self._hass.async_add_executor_job(self._refresh_access_token)
def _refresh_access_token(self):
refreshed_authentication = self._authenticator.refresh_access_token(force=False)
_LOGGER.info(
"Refreshed august access token. The old token expired at %s, and the new token expires at %s",
self._access_token_expires,
refreshed_authentication.access_token_expires,
)
self._access_token = refreshed_authentication.access_token
self._access_token_expires = refreshed_authentication.access_token_expires
async def async_get_device_activities(self, device_id, *activity_types):
"""Return a list of activities."""
_LOGGER.debug("Getting device activities for %s", device_id)
@ -268,22 +256,23 @@ class AugustData:
return next(iter(activities or []), None)
@Throttle(MIN_TIME_BETWEEN_ACTIVITY_UPDATES)
async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
async def _async_update_device_activities(self, limit=DEFAULT_ACTIVITY_FETCH_LIMIT):
"""Update data object with latest from August API."""
# This is the only place we refresh the api token
await self._async_refresh_access_token_if_needed()
await self._august_gateway.async_refresh_access_token_if_needed()
return await self._hass.async_add_executor_job(
partial(self._update_device_activities, limit=ACTIVITY_FETCH_LIMIT)
partial(self._update_device_activities, limit=limit)
)
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
def _update_device_activities(self, limit=DEFAULT_ACTIVITY_FETCH_LIMIT):
_LOGGER.debug("Start retrieving device activities")
for house_id in self.house_ids:
_LOGGER.debug("Updating device activity for house id %s", house_id)
activities = self._api.get_house_activities(
self._access_token, house_id, limit=limit
self._august_gateway.access_token, house_id, limit=limit
)
device_ids = {a.device_id for a in activities}
@ -294,6 +283,14 @@ class AugustData:
_LOGGER.debug("Completed retrieving device activities")
async def async_get_device_detail(self, device):
"""Return the detail for a device."""
if isinstance(device, Lock):
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):
"""Return doorbell detail."""
await self._async_update_doorbells_detail()
@ -342,8 +339,11 @@ class AugustData:
_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._access_token, device_id)
detail_by_id[device_id] = api_call(
self._august_gateway.access_token, device_id
)
except RequestException as ex:
_LOGGER.error(
"Request error trying to retrieve %s details for %s. %s",
@ -351,10 +351,6 @@ class AugustData:
device.device_name,
ex,
)
detail_by_id[device_id] = None
except Exception:
detail_by_id[device_id] = None
raise
_LOGGER.debug("Completed retrieving %s detail", device_type)
return detail_by_id
@ -365,7 +361,7 @@ class AugustData:
self.get_lock_name(device_id),
"lock",
self._api.lock_return_activities,
self._access_token,
self._august_gateway.access_token,
device_id,
)
@ -375,7 +371,7 @@ class AugustData:
self.get_lock_name(device_id),
"unlock",
self._api.unlock_return_activities,
self._access_token,
self._august_gateway.access_token,
device_id,
)

View File

@ -6,43 +6,44 @@ from august.activity import ActivityType
from august.lock import LockDoorStatus
from august.util import update_lock_detail_from_activity
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
BinarySensorDevice,
)
from . import DATA_AUGUST
from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
async def _async_retrieve_online_state(data, doorbell):
async def _async_retrieve_online_state(data, detail):
"""Get the latest state of the sensor."""
detail = await data.async_get_doorbell_detail(doorbell.device_id)
if detail is None:
return None
return detail.is_online
return detail.is_online or detail.status == "standby"
async def _async_retrieve_motion_state(data, doorbell):
async def _async_retrieve_motion_state(data, detail):
return await _async_activity_time_based_state(
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
data,
detail.device_id,
[ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING],
)
async def _async_retrieve_ding_state(data, doorbell):
async def _async_retrieve_ding_state(data, detail):
return await _async_activity_time_based_state(
data, doorbell, [ActivityType.DOORBELL_DING]
data, detail.device_id, [ActivityType.DOORBELL_DING]
)
async def _async_activity_time_based_state(data, doorbell, activity_types):
async def _async_activity_time_based_state(data, device_id, activity_types):
"""Get the latest state of the sensor."""
latest = await data.async_get_latest_device_activity(
doorbell.device_id, *activity_types
)
latest = await data.async_get_latest_device_activity(device_id, *activity_types)
if latest is not None:
start = latest.activity_start_time
@ -57,15 +58,19 @@ SENSOR_STATE_PROVIDER = 2
# sensor_type: [name, device_class, async_state_provider]
SENSOR_TYPES_DOORBELL = {
"doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state],
"doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state],
"doorbell_online": ["Online", "connectivity", _async_retrieve_online_state],
"doorbell_ding": ["Ding", DEVICE_CLASS_OCCUPANCY, _async_retrieve_ding_state],
"doorbell_motion": ["Motion", DEVICE_CLASS_MOTION, _async_retrieve_motion_state],
"doorbell_online": [
"Online",
DEVICE_CLASS_CONNECTIVITY,
_async_retrieve_online_state,
],
}
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the August binary sensors."""
data = hass.data[DATA_AUGUST]
data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
devices = []
for door in data.locks:
@ -98,6 +103,7 @@ class AugustDoorBinarySensor(BinarySensorDevice):
self._door = door
self._state = None
self._available = False
self._firmware_version = None
@property
def available(self):
@ -132,6 +138,7 @@ class AugustDoorBinarySensor(BinarySensorDevice):
lock_door_state = None
if detail is not None:
lock_door_state = detail.door_state
self._firmware_version = detail.firmware_version
self._available = lock_door_state != LockDoorStatus.UNKNOWN
self._state = lock_door_state == LockDoorStatus.OPEN
@ -141,6 +148,16 @@ class AugustDoorBinarySensor(BinarySensorDevice):
"""Get the unique of the door open binary sensor."""
return f"{self._door.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,
}
class AugustDoorbellBinarySensor(BinarySensorDevice):
"""Representation of an August binary sensor."""
@ -152,6 +169,7 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
self._doorbell = doorbell
self._state = None
self._available = False
self._firmware_version = None
@property
def available(self):
@ -178,11 +196,21 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
SENSOR_STATE_PROVIDER
]
self._state = await async_state_provider(self._data, self._doorbell)
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
self._available = self._doorbell.is_online or self._doorbell.status == "standby"
if self.device_class == DEVICE_CLASS_CONNECTIVITY:
self._available = True
else:
self._available = detail is not None and (
detail.is_online or detail.status == "standby"
)
self._state = None
if detail is not None:
self._firmware_version = detail.firmware_version
self._state = await async_state_provider(self._data, detail)
@property
def unique_id(self) -> str:
@ -191,3 +219,13 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
f"{self._doorbell.device_id}_"
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,
}

View File

@ -5,14 +5,14 @@ import requests
from homeassistant.components.camera import Camera
from . import DATA_AUGUST, DEFAULT_TIMEOUT
from .const import DATA_AUGUST, DEFAULT_NAME, DEFAULT_TIMEOUT, DOMAIN
SCAN_INTERVAL = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=5)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up August cameras."""
data = hass.data[DATA_AUGUST]
data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
devices = []
for doorbell in data.doorbells:
@ -29,9 +29,11 @@ class AugustCamera(Camera):
super().__init__()
self._data = data
self._doorbell = doorbell
self._doorbell_detail = None
self._timeout = timeout
self._image_url = None
self._image_content = None
self._firmware_version = None
@property
def name(self):
@ -51,7 +53,7 @@ class AugustCamera(Camera):
@property
def brand(self):
"""Return the camera brand."""
return "August"
return DEFAULT_NAME
@property
def model(self):
@ -60,16 +62,30 @@ class AugustCamera(Camera):
async def async_camera_image(self):
"""Return bytes of camera image."""
latest = await self._data.async_get_doorbell_detail(self._doorbell.device_id)
self._doorbell_detail = await self._data.async_get_doorbell_detail(
self._doorbell.device_id
)
if self._doorbell_detail is None:
return None
if self._image_url is not latest.image_url:
self._image_url = latest.image_url
if self._image_url is not self._doorbell_detail.image_url:
self._image_url = self._doorbell_detail.image_url
self._image_content = await self.hass.async_add_executor_job(
self._camera_image
)
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
def _camera_image(self):
"""Return bytes of camera image via http get."""
# Move this to py-august: see issue#32048
@ -79,3 +95,13 @@ class AugustCamera(Camera):
def unique_id(self) -> str:
"""Get the unique id of the camera."""
return f"{self._doorbell.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,
}

View File

@ -0,0 +1,135 @@
"""Config flow for August integration."""
import logging
from august.authenticator import ValidationResult
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from .const import (
CONF_LOGIN_METHOD,
DEFAULT_TIMEOUT,
LOGIN_METHODS,
VERIFICATION_CODE_KEY,
)
from .const import DOMAIN # pylint:disable=unused-import
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
from .gateway import AugustGateway
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
}
)
async def async_validate_input(
hass: core.HomeAssistant, data, august_gateway,
):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
Request configuration steps from the user.
"""
code = data.get(VERIFICATION_CODE_KEY)
if code is not None:
result = await hass.async_add_executor_job(
august_gateway.authenticator.validate_verification_code, code
)
_LOGGER.debug("Verification code validation: %s", result)
if result != ValidationResult.VALIDATED:
raise RequireValidation
try:
august_gateway.authenticate()
except RequireValidation:
_LOGGER.debug(
"Requesting new verification code for %s via %s",
data.get(CONF_USERNAME),
data.get(CONF_LOGIN_METHOD),
)
if code is None:
await hass.async_add_executor_job(
august_gateway.authenticator.send_verification_code
)
raise
return {
"title": data.get(CONF_USERNAME),
"data": august_gateway.config_entry(),
}
class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for August."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Store an AugustGateway()."""
self._august_gateway = None
self.user_auth_details = {}
super().__init__()
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if self._august_gateway is None:
self._august_gateway = AugustGateway(self.hass)
errors = {}
if user_input is not None:
self._august_gateway.async_setup(user_input)
try:
info = await async_validate_input(
self.hass, user_input, self._august_gateway,
)
await self.async_set_unique_id(user_input[CONF_USERNAME])
return self.async_create_entry(title=info["title"], data=info["data"])
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except RequireValidation:
self.user_auth_details = user_input
return await self.async_step_validation()
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_validation(self, user_input=None):
"""Handle validation (2fa) step."""
if user_input:
return await self.async_step_user({**self.user_auth_details, **user_input})
return self.async_show_form(
step_id="validation",
data_schema=vol.Schema(
{vol.Required(VERIFICATION_CODE_KEY): vol.All(str, vol.Strip)}
),
description_placeholders={
CONF_USERNAME: self.user_auth_details.get(CONF_USERNAME),
CONF_LOGIN_METHOD: self.user_auth_details.get(CONF_LOGIN_METHOD),
},
)
async def async_step_import(self, user_input):
"""Handle import."""
await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured()
return await self.async_step_user(user_input)

View File

@ -0,0 +1,42 @@
"""Constants for August devices."""
from datetime import timedelta
DEFAULT_TIMEOUT = 10
CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
CONF_LOGIN_METHOD = "login_method"
CONF_INSTALL_ID = "install_id"
VERIFICATION_CODE_KEY = "verification_code"
NOTIFICATION_ID = "august_notification"
NOTIFICATION_TITLE = "August"
DEFAULT_AUGUST_CONFIG_FILE = ".august.conf"
DATA_AUGUST = "data_august"
DEFAULT_NAME = "August"
DOMAIN = "august"
# Limit battery, online, and hardware updates to 1800 seconds
# in order to reduce the number of api requests and
# avoid hitting rate limits
MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800)
# Doorbells need to update more frequently than locks
# since we get an image from the doorbell api. Once
# py-august 0.18.0 is released doorbell status updates
# can be reduced in the same was as locks have been
MIN_TIME_BETWEEN_DOORBELL_DETAIL_UPDATES = timedelta(seconds=20)
# Activity needs to be checked more frequently as the
# doorbell motion and rings are included here
MIN_TIME_BETWEEN_ACTIVITY_UPDATES = timedelta(seconds=10)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=10)
LOGIN_METHODS = ["phone", "email"]
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock", "sensor"]

View File

@ -0,0 +1,15 @@
"""Shared excecption for the august integration."""
from homeassistant import exceptions
class RequireValidation(exceptions.HomeAssistantError):
"""Error to indicate we require validation (2fa)."""
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,143 @@
"""Handle August connection setup and authentication."""
import asyncio
import logging
from august.api import Api
from august.authenticator import AuthenticationState, Authenticator
from requests import RequestException, Session
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.core import callback
from .const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
CONF_INSTALL_ID,
CONF_LOGIN_METHOD,
DEFAULT_AUGUST_CONFIG_FILE,
VERIFICATION_CODE_KEY,
)
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
_LOGGER = logging.getLogger(__name__)
class AugustGateway:
"""Handle the connection to August."""
def __init__(self, hass):
"""Init the connection."""
self._api_http_session = Session()
self._token_refresh_lock = asyncio.Lock()
self._hass = hass
self._config = None
self._api = None
self._authenticator = None
self._authentication = None
@property
def authenticator(self):
"""August authentication object from py-august."""
return self._authenticator
@property
def authentication(self):
"""August authentication object from py-august."""
return self._authentication
@property
def access_token(self):
"""Access token for the api."""
return self._authentication.access_token
@property
def api(self):
"""August api object from py-august."""
return self._api
def config_entry(self):
"""Config entry."""
return {
CONF_LOGIN_METHOD: self._config[CONF_LOGIN_METHOD],
CONF_USERNAME: self._config[CONF_USERNAME],
CONF_PASSWORD: self._config[CONF_PASSWORD],
CONF_INSTALL_ID: self._config.get(CONF_INSTALL_ID),
CONF_TIMEOUT: self._config.get(CONF_TIMEOUT),
CONF_ACCESS_TOKEN_CACHE_FILE: self._config[CONF_ACCESS_TOKEN_CACHE_FILE],
}
@callback
def async_setup(self, conf):
"""Create the api and authenticator objects."""
if conf.get(VERIFICATION_CODE_KEY):
return
if conf.get(CONF_ACCESS_TOKEN_CACHE_FILE) is None:
conf[
CONF_ACCESS_TOKEN_CACHE_FILE
] = f".{conf[CONF_USERNAME]}{DEFAULT_AUGUST_CONFIG_FILE}"
self._config = conf
self._api = Api(
timeout=self._config.get(CONF_TIMEOUT), http_session=self._api_http_session,
)
self._authenticator = Authenticator(
self._api,
self._config[CONF_LOGIN_METHOD],
self._config[CONF_USERNAME],
self._config[CONF_PASSWORD],
install_id=self._config.get(CONF_INSTALL_ID),
access_token_cache_file=self._hass.config.path(
self._config[CONF_ACCESS_TOKEN_CACHE_FILE]
),
)
def authenticate(self):
"""Authenticate with the details provided to setup."""
self._authentication = None
try:
self._authentication = self.authenticator.authenticate()
except RequestException as ex:
_LOGGER.error("Unable to connect to August service: %s", str(ex))
raise CannotConnect
if self._authentication.state == AuthenticationState.BAD_PASSWORD:
raise InvalidAuth
if self._authentication.state == AuthenticationState.REQUIRES_VALIDATION:
raise RequireValidation
if self._authentication.state != AuthenticationState.AUTHENTICATED:
_LOGGER.error(
"Unknown authentication state: %s", self._authentication.state
)
raise InvalidAuth
return self._authentication
async def async_refresh_access_token_if_needed(self):
"""Refresh the august access token if needed."""
if self.authenticator.should_refresh():
async with self._token_refresh_lock:
await self._hass.async_add_executor_job(self._refresh_access_token)
def _refresh_access_token(self):
refreshed_authentication = self.authenticator.refresh_access_token(force=False)
_LOGGER.info(
"Refreshed august access token. The old token expired at %s, and the new token expires at %s",
self.authentication.access_token_expires,
refreshed_authentication.access_token_expires,
)
self._authentication = refreshed_authentication
def _close_http_session(self):
"""Close API sessions used to connect to August."""
if self._api_http_session:
try:
self._api_http_session.close()
except RequestException:
pass
def __del__(self):
"""Close out the http session on destroy."""
self._close_http_session()

View File

@ -9,16 +9,16 @@ from august.util import update_lock_detail_from_activity
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from . import DATA_AUGUST
from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up August locks."""
data = hass.data[DATA_AUGUST]
data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
devices = []
for lock in data.locks:
@ -39,6 +39,7 @@ class AugustLock(LockDevice):
self._lock_detail = None
self._changed_by = None
self._available = False
self._firmware_version = None
async def async_lock(self, **kwargs):
"""Lock the device."""
@ -59,12 +60,18 @@ class AugustLock(LockDevice):
self.schedule_update_ha_state()
def _update_lock_status_from_detail(self):
lock_status = self._lock_detail.lock_status
if self._lock_status != lock_status:
self._lock_status = lock_status
detail = self._lock_detail
lock_status = None
self._available = False
if detail is not None:
lock_status = detail.lock_status
self._available = (
lock_status is not None and lock_status != LockStatus.UNKNOWN
)
if self._lock_status != lock_status:
self._lock_status = lock_status
return True
return False
@ -77,7 +84,11 @@ class AugustLock(LockDevice):
if lock_activity is not None:
self._changed_by = lock_activity.operated_by
update_lock_detail_from_activity(self._lock_detail, lock_activity)
if self._lock_detail is not None:
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._update_lock_status_from_detail()
@ -94,7 +105,8 @@ class AugustLock(LockDevice):
@property
def is_locked(self):
"""Return true if device is on."""
if self._lock_status is None or self._lock_status is LockStatus.UNKNOWN:
return None
return self._lock_status is LockStatus.LOCKED
@property
@ -115,6 +127,16 @@ class AugustLock(LockDevice):
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,
}
@property
def unique_id(self) -> str:
"""Get the unique id of the lock."""

View File

@ -2,7 +2,14 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["py-august==0.17.0"],
"dependencies": ["configurator"],
"codeowners": ["@bdraco"]
}
"requirements": [
"py-august==0.17.0"
],
"dependencies": [
"configurator"
],
"codeowners": [
"@bdraco"
],
"config_flow": true
}

View File

@ -0,0 +1,158 @@
"""Support for August sensors."""
from datetime import timedelta
import logging
from homeassistant.components.sensor import DEVICE_CLASS_BATTERY
from homeassistant.helpers.entity import Entity
from .const import DATA_AUGUST, DEFAULT_NAME, DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
async def _async_retrieve_device_battery_state(detail):
"""Get the latest state of the sensor."""
if detail is None:
return None
return detail.battery_level
async def _async_retrieve_linked_keypad_battery_state(detail):
"""Get the latest state of the sensor."""
if detail is None:
return None
if detail.keypad is None:
return None
battery_level = detail.keypad.battery_level
_LOGGER.debug("keypad battery level: %s %s", battery_level, battery_level.lower())
if battery_level.lower() == "full":
return 100
if battery_level.lower() == "medium":
return 60
if battery_level.lower() == "low":
return 10
return 0
SENSOR_TYPES_BATTERY = {
"device_battery": {
"name": "Battery",
"async_state_provider": _async_retrieve_device_battery_state,
},
"linked_keypad_battery": {
"name": "Keypad Battery",
"async_state_provider": _async_retrieve_linked_keypad_battery_state,
},
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the August sensors."""
data = hass.data[DOMAIN][config_entry.entry_id][DATA_AUGUST]
devices = []
batteries = {
"device_battery": [],
"linked_keypad_battery": [],
}
for device in data.doorbells:
batteries["device_battery"].append(device)
for device in data.locks:
batteries["device_battery"].append(device)
batteries["linked_keypad_battery"].append(device)
for sensor_type in SENSOR_TYPES_BATTERY:
for device in batteries[sensor_type]:
async_state_provider = SENSOR_TYPES_BATTERY[sensor_type][
"async_state_provider"
]
detail = await data.async_get_device_detail(device)
state = await async_state_provider(detail)
sensor_name = SENSOR_TYPES_BATTERY[sensor_type]["name"]
if state is None:
_LOGGER.debug(
"Not adding battery sensor %s for %s because it is not present",
sensor_name,
device.device_name,
)
else:
_LOGGER.debug(
"Adding battery sensor %s for %s", sensor_name, device.device_name,
)
devices.append(AugustBatterySensor(data, sensor_type, device))
async_add_entities(devices, True)
class AugustBatterySensor(Entity):
"""Representation of an August sensor."""
def __init__(self, data, sensor_type, device):
"""Initialize the sensor."""
self._data = data
self._sensor_type = sensor_type
self._device = device
self._state = None
self._available = False
self._firmware_version = None
@property
def available(self):
"""Return the availability of this sensor."""
return self._available
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return "%" # UNIT_PERCENTAGE will be available after PR#32094
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return DEVICE_CLASS_BATTERY
@property
def name(self):
"""Return the name of the sensor."""
device_name = self._device.device_name
sensor_name = SENSOR_TYPES_BATTERY[self._sensor_type]["name"]
return f"{device_name} {sensor_name}"
async def async_update(self):
"""Get the latest state of the sensor."""
async_state_provider = SENSOR_TYPES_BATTERY[self._sensor_type][
"async_state_provider"
]
detail = await self._data.async_get_device_detail(self._device)
self._state = await async_state_provider(detail)
self._available = self._state is not None
if detail is not None:
self._firmware_version = detail.firmware_version
@property
def unique_id(self) -> str:
"""Get the unique id of the device sensor."""
return f"{self._device.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,
}

View File

@ -0,0 +1,32 @@
{
"config" : {
"error" : {
"unknown" : "Unexpected error",
"cannot_connect" : "Failed to connect, please try again",
"invalid_auth" : "Invalid authentication"
},
"abort" : {
"already_configured" : "Account is already configured"
},
"step" : {
"validation" : {
"title" : "Two factor authentication",
"data" : {
"code" : "Verification code"
},
"description" : "Please check your {login_method} ({username}) and enter the verification code below"
},
"user" : {
"description" : "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
"data" : {
"timeout" : "Timeout (seconds)",
"password" : "Password",
"username" : "Username",
"login_method" : "Login Method"
},
"title" : "Setup an August account"
}
},
"title" : "August"
}
}

View File

@ -12,6 +12,7 @@ FLOWS = [
"almond",
"ambiclimate",
"ambient_station",
"august",
"axis",
"brother",
"cast",

View File

@ -1,16 +1,13 @@
"""Mocks for the august component."""
import datetime
import json
import os
import time
from unittest.mock import MagicMock, PropertyMock
from asynctest import mock
from august.activity import Activity, DoorOperationActivity, LockOperationActivity
from august.api import Api
from august.activity import DoorOperationActivity, LockOperationActivity
from august.authenticator import AuthenticationState
from august.doorbell import Doorbell, DoorbellDetail
from august.exceptions import AugustApiHTTPError
from august.lock import Lock, LockDetail
from homeassistant.components.august import (
@ -18,10 +15,8 @@ from homeassistant.components.august import (
CONF_PASSWORD,
CONF_USERNAME,
DOMAIN,
AugustData,
)
from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from tests.common import load_fixture
@ -37,8 +32,8 @@ def _mock_get_config():
}
@mock.patch("homeassistant.components.august.Api")
@mock.patch("homeassistant.components.august.Authenticator.authenticate")
@mock.patch("homeassistant.components.august.gateway.Api")
@mock.patch("homeassistant.components.august.gateway.Authenticator.authenticate")
async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
"""Set up august integration."""
authenticate_mock.side_effect = MagicMock(
@ -84,6 +79,9 @@ async def _create_august_with_devices(hass, devices, api_call_side_effects=None)
def get_lock_detail_side_effect(access_token, device_id):
return _get_device_detail("locks", device_id)
def get_doorbell_detail_side_effect(access_token, device_id):
return _get_device_detail("doorbells", device_id)
def get_operable_locks_side_effect(access_token):
return _get_base_devices("locks")
@ -109,6 +107,8 @@ async def _create_august_with_devices(hass, devices, api_call_side_effects=None)
if "get_lock_detail" not in api_call_side_effects:
api_call_side_effects["get_lock_detail"] = get_lock_detail_side_effect
if "get_doorbell_detail" not in api_call_side_effects:
api_call_side_effects["get_doorbell_detail"] = get_doorbell_detail_side_effect
if "get_operable_locks" not in api_call_side_effects:
api_call_side_effects["get_operable_locks"] = get_operable_locks_side_effect
if "get_doorbells" not in api_call_side_effects:
@ -143,6 +143,11 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects):
if api_call_side_effects["get_doorbells"]:
api_instance.get_doorbells.side_effect = api_call_side_effects["get_doorbells"]
if api_call_side_effects["get_doorbell_detail"]:
api_instance.get_doorbell_detail.side_effect = api_call_side_effects[
"get_doorbell_detail"
]
if api_call_side_effects["get_house_activities"]:
api_instance.get_house_activities.side_effect = api_call_side_effects[
"get_house_activities"
@ -160,106 +165,6 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects):
return await _mock_setup_august(hass, api_instance)
class MockAugustApiFailing(Api):
"""A mock for py-august Api class that always has an AugustApiHTTPError."""
def _call_api(self, *args, **kwargs):
"""Mock the time activity started."""
raise AugustApiHTTPError("This should bubble up as its user consumable")
class MockActivity(Activity):
"""A mock for py-august Activity class."""
def __init__(
self, action=None, activity_start_timestamp=None, activity_end_timestamp=None
):
"""Init the py-august Activity class mock."""
self._action = action
self._activity_start_timestamp = activity_start_timestamp
self._activity_end_timestamp = activity_end_timestamp
@property
def activity_start_time(self):
"""Mock the time activity started."""
return datetime.datetime.fromtimestamp(self._activity_start_timestamp)
@property
def activity_end_time(self):
"""Mock the time activity ended."""
return datetime.datetime.fromtimestamp(self._activity_end_timestamp)
@property
def action(self):
"""Mock the action."""
return self._action
class MockAugustComponentData(AugustData):
"""A wrapper to mock AugustData."""
# AugustData support multiple locks, however for the purposes of
# mocking we currently only mock one lockid
def __init__(
self,
last_lock_status_update_timestamp=1,
last_door_state_update_timestamp=1,
api=MockAugustApiFailing(),
access_token="mocked_access_token",
locks=[],
doorbells=[],
):
"""Mock AugustData."""
self._last_lock_status_update_time_utc = dt.as_utc(
datetime.datetime.fromtimestamp(last_lock_status_update_timestamp)
)
self._last_door_state_update_time_utc = dt.as_utc(
datetime.datetime.fromtimestamp(last_lock_status_update_timestamp)
)
self._api = api
self._access_token = access_token
self._locks = locks
self._doorbells = doorbells
self._lock_status_by_id = {}
self._lock_last_status_update_time_utc_by_id = {}
def set_mocked_locks(self, locks):
"""Set lock mocks."""
self._locks = locks
def set_mocked_doorbells(self, doorbells):
"""Set doorbell mocks."""
self._doorbells = doorbells
def get_last_lock_status_update_time_utc(self, device_id):
"""Mock to get last lock status update time."""
return self._last_lock_status_update_time_utc
def set_last_lock_status_update_time_utc(self, device_id, update_time):
"""Mock to set last lock status update time."""
self._last_lock_status_update_time_utc = update_time
def get_last_door_state_update_time_utc(self, device_id):
"""Mock to get last door state update time."""
return self._last_door_state_update_time_utc
def set_last_door_state_update_time_utc(self, device_id, update_time):
"""Mock to set last door state update time."""
self._last_door_state_update_time_utc = update_time
def _mock_august_authenticator():
authenticator = MagicMock(name="august.authenticator")
authenticator.should_refresh = MagicMock(
name="august.authenticator.should_refresh", return_value=0
)
authenticator.refresh_access_token = MagicMock(
name="august.authenticator.refresh_access_token"
)
return authenticator
def _mock_august_authentication(token_text, token_timestamp):
authentication = MagicMock(name="august.authentication")
type(authentication).state = PropertyMock(
@ -321,20 +226,12 @@ def _mock_august_lock_data(lockid="mocklockid1", houseid="mockhouseid1"):
}
def _mock_operative_august_lock_detail(lockid):
operative_lock_detail_data = _mock_august_lock_data(lockid=lockid)
return LockDetail(operative_lock_detail_data)
async def _mock_operative_august_lock_detail(hass):
return await _mock_lock_from_fixture(hass, "get_lock.online.json")
def _mock_inoperative_august_lock_detail(lockid):
inoperative_lock_detail_data = _mock_august_lock_data(lockid=lockid)
del inoperative_lock_detail_data["Bridge"]
return LockDetail(inoperative_lock_detail_data)
def _mock_doorsense_enabled_august_lock_detail(lockid):
doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid)
return LockDetail(doorsense_lock_detail_data)
async def _mock_inoperative_august_lock_detail(hass):
return await _mock_lock_from_fixture(hass, "get_lock.offline.json")
async def _mock_lock_from_fixture(hass, path):
@ -354,10 +251,12 @@ async def _load_json_fixture(hass, path):
return json.loads(fixture)
def _mock_doorsense_missing_august_lock_detail(lockid):
doorsense_lock_detail_data = _mock_august_lock_data(lockid=lockid)
del doorsense_lock_detail_data["LockStatus"]["doorState"]
return LockDetail(doorsense_lock_detail_data)
async def _mock_doorsense_enabled_august_lock_detail(hass):
return await _mock_lock_from_fixture(hass, "get_lock.online_with_doorsense.json")
async def _mock_doorsense_missing_august_lock_detail(hass):
return await _mock_lock_from_fixture(hass, "get_lock.online_missing_doorsense.json")
def _mock_lock_operation_activity(lock, action):

View File

@ -9,6 +9,7 @@ from homeassistant.const import (
SERVICE_UNLOCK,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
)
from tests.components.august.mocks import (
@ -69,3 +70,21 @@ async def test_create_doorbell(hass):
"binary_sensor.k98gidt45gul_name_ding"
)
assert binary_sensor_k98gidt45gul_name_ding.state == STATE_OFF
async def test_create_doorbell_offline(hass):
"""Test creation of a doorbell that is offline."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
doorbell_details = [doorbell_one]
await _create_august_with_devices(hass, doorbell_details)
binary_sensor_tmt100_name_motion = hass.states.get(
"binary_sensor.tmt100_name_motion"
)
assert binary_sensor_tmt100_name_motion.state == STATE_UNAVAILABLE
binary_sensor_tmt100_name_online = hass.states.get(
"binary_sensor.tmt100_name_online"
)
assert binary_sensor_tmt100_name_online.state == STATE_OFF
binary_sensor_tmt100_name_ding = hass.states.get("binary_sensor.tmt100_name_ding")
assert binary_sensor_tmt100_name_ding.state == STATE_UNAVAILABLE

View File

@ -0,0 +1,195 @@
"""Test the August config flow."""
from asynctest import patch
from august.authenticator import ValidationResult
from homeassistant import config_entries, setup
from homeassistant.components.august.const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
CONF_INSTALL_ID,
CONF_LOGIN_METHOD,
DOMAIN,
VERIFICATION_CODE_KEY,
)
from homeassistant.components.august.exceptions import (
CannotConnect,
InvalidAuth,
RequireValidation,
)
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch(
"homeassistant.components.august.config_flow.AugustGateway.authenticate",
return_value=True,
), patch(
"homeassistant.components.august.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.august.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
CONF_PASSWORD: "test-password",
},
)
assert result2["type"] == "create_entry"
assert result2["title"] == "my@email.tld"
assert result2["data"] == {
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
CONF_PASSWORD: "test-password",
CONF_INSTALL_ID: None,
CONF_TIMEOUT: 10,
CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf",
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_auth(hass):
"""Test we handle invalid auth."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.august.config_flow.AugustGateway.authenticate",
side_effect=InvalidAuth,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
CONF_PASSWORD: "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_cannot_connect(hass):
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.august.config_flow.AugustGateway.authenticate",
side_effect=CannotConnect,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
CONF_PASSWORD: "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_needs_validate(hass):
"""Test we present validation when we need to validate."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.august.config_flow.AugustGateway.authenticate",
side_effect=RequireValidation,
), patch(
"homeassistant.components.august.gateway.Authenticator.send_verification_code",
return_value=True,
) as mock_send_verification_code:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
CONF_PASSWORD: "test-password",
},
)
assert len(mock_send_verification_code.mock_calls) == 1
assert result2["type"] == "form"
assert result2["errors"] is None
assert result2["step_id"] == "validation"
# Try with the WRONG verification code give us the form back again
with patch(
"homeassistant.components.august.config_flow.AugustGateway.authenticate",
side_effect=RequireValidation,
), patch(
"homeassistant.components.august.gateway.Authenticator.validate_verification_code",
return_value=ValidationResult.INVALID_VERIFICATION_CODE,
) as mock_validate_verification_code, patch(
"homeassistant.components.august.gateway.Authenticator.send_verification_code",
return_value=True,
) as mock_send_verification_code, patch(
"homeassistant.components.august.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.august.async_setup_entry", return_value=True
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], {VERIFICATION_CODE_KEY: "incorrect"},
)
# Make sure we do not resend the code again
# so they have a chance to retry
assert len(mock_send_verification_code.mock_calls) == 0
assert len(mock_validate_verification_code.mock_calls) == 1
assert result3["type"] == "form"
assert result3["errors"] is None
assert result3["step_id"] == "validation"
# Try with the CORRECT verification code and we setup
with patch(
"homeassistant.components.august.config_flow.AugustGateway.authenticate",
return_value=True,
), patch(
"homeassistant.components.august.gateway.Authenticator.validate_verification_code",
return_value=ValidationResult.VALIDATED,
) as mock_validate_verification_code, patch(
"homeassistant.components.august.gateway.Authenticator.send_verification_code",
return_value=True,
) as mock_send_verification_code, patch(
"homeassistant.components.august.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.august.async_setup_entry", return_value=True
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result["flow_id"], {VERIFICATION_CODE_KEY: "correct"},
)
assert len(mock_send_verification_code.mock_calls) == 0
assert len(mock_validate_verification_code.mock_calls) == 1
assert result4["type"] == "create_entry"
assert result4["title"] == "my@email.tld"
assert result4["data"] == {
CONF_LOGIN_METHOD: "email",
CONF_USERNAME: "my@email.tld",
CONF_PASSWORD: "test-password",
CONF_INSTALL_ID: None,
CONF_TIMEOUT: 10,
CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf",
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1

View File

@ -0,0 +1,49 @@
"""The gateway tests for the august platform."""
from unittest.mock import MagicMock
from asynctest import mock
from homeassistant.components.august.const import DOMAIN
from homeassistant.components.august.gateway import AugustGateway
from tests.components.august.mocks import _mock_august_authentication, _mock_get_config
async def test_refresh_access_token(hass):
"""Test token refreshes."""
await _patched_refresh_access_token(hass, "new_token", 5678)
@mock.patch("homeassistant.components.august.gateway.Authenticator.authenticate")
@mock.patch("homeassistant.components.august.gateway.Authenticator.should_refresh")
@mock.patch(
"homeassistant.components.august.gateway.Authenticator.refresh_access_token"
)
async def _patched_refresh_access_token(
hass,
new_token,
new_token_expire_time,
refresh_access_token_mock,
should_refresh_mock,
authenticate_mock,
):
authenticate_mock.side_effect = MagicMock(
return_value=_mock_august_authentication("original_token", 1234)
)
august_gateway = AugustGateway(hass)
mocked_config = _mock_get_config()
august_gateway.async_setup(mocked_config[DOMAIN])
august_gateway.authenticate()
should_refresh_mock.return_value = False
await august_gateway.async_refresh_access_token_if_needed()
refresh_access_token_mock.assert_not_called()
should_refresh_mock.return_value = True
refresh_access_token_mock.return_value = _mock_august_authentication(
new_token, new_token_expire_time
)
await august_gateway.async_refresh_access_token_if_needed()
refresh_access_token_mock.assert_called()
assert august_gateway.access_token == new_token
assert august_gateway.authentication.access_token_expires == new_token_expire_time

View File

@ -1,136 +1,146 @@
"""The tests for the august platform."""
import asyncio
from unittest.mock import MagicMock
from asynctest import patch
from august.exceptions import AugustApiHTTPError
from august.lock import LockDetail
from requests import RequestException
from homeassistant.components import august
from homeassistant import setup
from homeassistant.components.august.const import (
CONF_ACCESS_TOKEN_CACHE_FILE,
CONF_INSTALL_ID,
CONF_LOGIN_METHOD,
DEFAULT_AUGUST_CONFIG_FILE,
)
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_PASSWORD,
CONF_TIMEOUT,
CONF_USERNAME,
SERVICE_LOCK,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_ON,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
from tests.components.august.mocks import (
MockAugustApiFailing,
MockAugustComponentData,
_mock_august_authentication,
_mock_august_authenticator,
_mock_august_lock,
_create_august_with_devices,
_mock_doorsense_enabled_august_lock_detail,
_mock_doorsense_missing_august_lock_detail,
_mock_get_config,
_mock_inoperative_august_lock_detail,
_mock_operative_august_lock_detail,
)
def test_get_lock_name():
"""Get the lock name from August data."""
data = MockAugustComponentData(last_lock_status_update_timestamp=1)
lock = _mock_august_lock()
data.set_mocked_locks([lock])
assert data.get_lock_name("mocklockid1") == "mocklockid1 Name"
async def test_unlock_throws_august_api_http_error(hass):
"""Test unlock throws correct error on http error."""
mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
def _unlock_return_activities_side_effect(access_token, device_id):
raise AugustApiHTTPError("This should bubble up as its user consumable")
def test_unlock_throws_august_api_http_error():
"""Test unlock."""
data = MockAugustComponentData(api=MockAugustApiFailing())
lock = _mock_august_lock()
data.set_mocked_locks([lock])
await _create_august_with_devices(
hass,
[mocked_lock_detail],
api_call_side_effects={
"unlock_return_activities": _unlock_return_activities_side_effect
},
)
last_err = None
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
try:
data.unlock("mocklockid1")
await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True)
except HomeAssistantError as err:
last_err = err
assert (
str(last_err)
== "mocklockid1 Name: This should bubble up as its user consumable"
== "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user consumable"
)
def test_lock_throws_august_api_http_error():
"""Test lock."""
data = MockAugustComponentData(api=MockAugustApiFailing())
lock = _mock_august_lock()
data.set_mocked_locks([lock])
async def test_lock_throws_august_api_http_error(hass):
"""Test lock throws correct error on http error."""
mocked_lock_detail = await _mock_operative_august_lock_detail(hass)
def _lock_return_activities_side_effect(access_token, device_id):
raise AugustApiHTTPError("This should bubble up as its user consumable")
await _create_august_with_devices(
hass,
[mocked_lock_detail],
api_call_side_effects={
"lock_return_activities": _lock_return_activities_side_effect
},
)
last_err = None
data = {ATTR_ENTITY_ID: "lock.a6697750d607098bae8d6baa11ef8063_name"}
try:
data.unlock("mocklockid1")
await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True)
except HomeAssistantError as err:
last_err = err
assert (
str(last_err)
== "mocklockid1 Name: This should bubble up as its user consumable"
== "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user consumable"
)
def test_inoperative_locks_are_filtered_out():
async def test_inoperative_locks_are_filtered_out(hass):
"""Ensure inoperative locks do not get setup."""
august_operative_lock = _mock_operative_august_lock_detail("oplockid1")
data = _create_august_data_with_lock_details(
[august_operative_lock, _mock_inoperative_august_lock_detail("inoplockid1")]
august_operative_lock = await _mock_operative_august_lock_detail(hass)
august_inoperative_lock = await _mock_inoperative_august_lock_detail(hass)
await _create_august_with_devices(
hass, [august_operative_lock, august_inoperative_lock]
)
assert len(data.locks) == 1
assert data.locks[0].device_id == "oplockid1"
lock_abc_name = hass.states.get("lock.abc_name")
assert lock_abc_name is None
lock_a6697750d607098bae8d6baa11ef8063_name = hass.states.get(
"lock.a6697750d607098bae8d6baa11ef8063_name"
)
assert lock_a6697750d607098bae8d6baa11ef8063_name.state == STATE_LOCKED
def test_lock_has_doorsense():
async def test_lock_has_doorsense(hass):
"""Check to see if a lock has doorsense."""
data = _create_august_data_with_lock_details(
[
_mock_doorsense_enabled_august_lock_detail("doorsenselock1"),
_mock_doorsense_missing_august_lock_detail("nodoorsenselock1"),
RequestException("mocked request error"),
RequestException("mocked request error"),
]
doorsenselock = await _mock_doorsense_enabled_august_lock_detail(hass)
nodoorsenselock = await _mock_doorsense_missing_august_lock_detail(hass)
await _create_august_with_devices(hass, [doorsenselock, nodoorsenselock])
binary_sensor_online_with_doorsense_name_open = hass.states.get(
"binary_sensor.online_with_doorsense_name_open"
)
assert data.lock_has_doorsense("doorsenselock1") is True
assert data.lock_has_doorsense("nodoorsenselock1") is False
# The api calls are mocked to fail on the second
# run of async_get_lock_detail
#
# This will be switched to await data.async_get_lock_detail("doorsenselock1")
# once we mock the full home assistant setup
data._update_locks_detail()
# doorsenselock1 should be false if we cannot tell due
# to an api error
assert data.lock_has_doorsense("doorsenselock1") is False
async def test__refresh_access_token(hass):
"""Test refresh of the access token."""
authentication = _mock_august_authentication("original_token", 1234)
authenticator = _mock_august_authenticator()
token_refresh_lock = asyncio.Lock()
data = august.AugustData(
hass, MagicMock(name="api"), authentication, authenticator, token_refresh_lock
assert binary_sensor_online_with_doorsense_name_open.state == STATE_ON
binary_sensor_missing_doorsense_id_name_open = hass.states.get(
"binary_sensor.missing_doorsense_id_name_open"
)
await data._async_refresh_access_token_if_needed()
authenticator.refresh_access_token.assert_not_called()
authenticator.should_refresh.return_value = 1
authenticator.refresh_access_token.return_value = _mock_august_authentication(
"new_token", 5678
)
await data._async_refresh_access_token_if_needed()
authenticator.refresh_access_token.assert_called()
assert data._access_token == "new_token"
assert data._access_token_expires == 5678
assert binary_sensor_missing_doorsense_id_name_open is None
def _create_august_data_with_lock_details(lock_details):
locks = []
for lock in lock_details:
if isinstance(lock, LockDetail):
locks.append(_mock_august_lock(lock.device_id))
authentication = _mock_august_authentication("original_token", 1234)
authenticator = _mock_august_authenticator()
token_refresh_lock = MagicMock()
api = MagicMock()
api.get_lock_detail = MagicMock(side_effect=lock_details)
api.get_operable_locks = MagicMock(return_value=locks)
api.get_doorbells = MagicMock(return_value=[])
return august.AugustData(
MagicMock(), api, authentication, authenticator, token_refresh_lock
)
async def test_set_up_from_yaml(hass):
"""Test to make sure config is imported from yaml."""
await setup.async_setup_component(hass, "persistent_notification", {})
with patch(
"homeassistant.components.august.async_setup_august", return_value=True,
) as mock_setup_august, patch(
"homeassistant.components.august.config_flow.AugustGateway.authenticate",
return_value=True,
):
mocked_config = _mock_get_config()
assert await async_setup_component(hass, "august", mocked_config)
await hass.async_block_till_done()
assert len(mock_setup_august.mock_calls) == 1
call = mock_setup_august.call_args
args, kwargs = call
imported_config_entry = args[1]
# The import must use DEFAULT_AUGUST_CONFIG_FILE so they
# do not loose their token when config is migrated
assert imported_config_entry.data == {
CONF_ACCESS_TOKEN_CACHE_FILE: DEFAULT_AUGUST_CONFIG_FILE,
CONF_INSTALL_ID: None,
CONF_LOGIN_METHOD: "email",
CONF_PASSWORD: "mocked_password",
CONF_TIMEOUT: None,
CONF_USERNAME: "mocked_username",
}

View File

@ -6,45 +6,65 @@ from homeassistant.const import (
SERVICE_LOCK,
SERVICE_UNLOCK,
STATE_LOCKED,
STATE_UNAVAILABLE,
STATE_UNLOCKED,
)
from tests.components.august.mocks import (
_create_august_with_devices,
_mock_doorsense_enabled_august_lock_detail,
_mock_lock_from_fixture,
)
async def test_one_lock_operation(hass):
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_lock_from_fixture(
hass, "get_lock.online_with_doorsense.json"
)
lock_one = await _mock_doorsense_enabled_august_lock_detail(hass)
lock_details = [lock_one]
await _create_august_with_devices(hass, lock_details)
lock_abc_name = hass.states.get("lock.abc_name")
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_abc_name.state == STATE_LOCKED
assert lock_online_with_doorsense_name.state == STATE_LOCKED
assert lock_abc_name.attributes.get("battery_level") == 92
assert lock_abc_name.attributes.get("friendly_name") == "ABC Name"
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
data = {}
data[ATTR_ENTITY_ID] = "lock.abc_name"
data[ATTR_ENTITY_ID] = "lock.online_with_doorsense_name"
assert await hass.services.async_call(
LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True
)
lock_abc_name = hass.states.get("lock.abc_name")
assert lock_abc_name.state == STATE_UNLOCKED
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_UNLOCKED
assert lock_abc_name.attributes.get("battery_level") == 92
assert lock_abc_name.attributes.get("friendly_name") == "ABC Name"
assert lock_online_with_doorsense_name.attributes.get("battery_level") == 92
assert (
lock_online_with_doorsense_name.attributes.get("friendly_name")
== "online_with_doorsense Name"
)
assert await hass.services.async_call(
LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True
)
lock_abc_name = hass.states.get("lock.abc_name")
assert lock_abc_name.state == STATE_LOCKED
lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name")
assert lock_online_with_doorsense_name.state == STATE_LOCKED
async def test_one_lock_unknown_state(hass):
"""Test creation of a lock with doorsense and bridge."""
lock_one = await _mock_lock_from_fixture(
hass, "get_lock.online.unknown_state.json",
)
lock_details = [lock_one]
await _create_august_with_devices(hass, lock_details)
lock_brokenid_name = hass.states.get("lock.brokenid_name")
# Once we have bridge_is_online support in py-august
# this can change to STATE_UNKNOWN
assert lock_brokenid_name.state == STATE_UNAVAILABLE

View File

@ -0,0 +1,84 @@
"""The sensor tests for the august platform."""
from tests.components.august.mocks import (
_create_august_with_devices,
_mock_doorbell_from_fixture,
_mock_lock_from_fixture,
)
async def test_create_doorbell(hass):
"""Test creation of a doorbell."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json")
await _create_august_with_devices(hass, [doorbell_one])
sensor_k98gidt45gul_name_battery = hass.states.get(
"sensor.k98gidt45gul_name_battery"
)
assert sensor_k98gidt45gul_name_battery.state == "96"
assert sensor_k98gidt45gul_name_battery.attributes["unit_of_measurement"] == "%"
async def test_create_doorbell_offline(hass):
"""Test creation of a doorbell that is offline."""
doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.offline.json")
await _create_august_with_devices(hass, [doorbell_one])
entity_registry = await hass.helpers.entity_registry.async_get_registry()
sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery")
assert sensor_tmt100_name_battery.state == "81"
assert sensor_tmt100_name_battery.attributes["unit_of_measurement"] == "%"
entry = entity_registry.async_get("sensor.tmt100_name_battery")
assert entry
assert entry.unique_id == "tmt100_device_battery"
async def test_create_doorbell_hardwired(hass):
"""Test creation of a doorbell that is hardwired without a battery."""
doorbell_one = await _mock_doorbell_from_fixture(
hass, "get_doorbell.nobattery.json"
)
await _create_august_with_devices(hass, [doorbell_one])
sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery")
assert sensor_tmt100_name_battery is None
async def test_create_lock_with_linked_keypad(hass):
"""Test creation of a lock with a linked keypad that both have a battery."""
lock_one = await _mock_lock_from_fixture(hass, "get_lock.doorsense_init.json")
await _create_august_with_devices(hass, [lock_one])
entity_registry = await hass.helpers.entity_registry.async_get_registry()
sensor_a6697750d607098bae8d6baa11ef8063_name_battery = hass.states.get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
)
assert sensor_a6697750d607098bae8d6baa11ef8063_name_battery.state == "88"
assert (
sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[
"unit_of_measurement"
]
== "%"
)
entry = entity_registry.async_get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_battery"
)
assert entry
assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_device_battery"
sensor_a6697750d607098bae8d6baa11ef8063_name_keypad_battery = hass.states.get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_keypad_battery"
)
assert sensor_a6697750d607098bae8d6baa11ef8063_name_keypad_battery.state == "60"
assert (
sensor_a6697750d607098bae8d6baa11ef8063_name_keypad_battery.attributes[
"unit_of_measurement"
]
== "%"
)
entry = entity_registry.async_get(
"sensor.a6697750d607098bae8d6baa11ef8063_name_keypad_battery"
)
assert entry
assert entry.unique_id == "A6697750D607098BAE8D6BAA11EF8063_linked_keypad_battery"

View File

@ -0,0 +1,80 @@
{
"status_timestamp" : 1512811834532,
"appID" : "august-iphone",
"LockID" : "BBBB1F5F11114C24CCCC97571DD6AAAA",
"recentImage" : {
"original_filename" : "file",
"placeholder" : false,
"bytes" : 24476,
"height" : 640,
"format" : "jpg",
"width" : 480,
"version" : 1512892814,
"resource_type" : "image",
"etag" : "54966926be2e93f77d498a55f247661f",
"tags" : [],
"public_id" : "qqqqt4ctmxwsysylaaaa",
"url" : "http://image.com/vmk16naaaa7ibuey7sar.jpg",
"created_at" : "2017-12-10T08:01:35Z",
"signature" : "75z47ca21b5e8ffda21d2134e478a2307c4625da",
"secure_url" : "https://image.com/vmk16naaaa7ibuey7sar.jpg",
"type" : "upload"
},
"settings" : {
"keepEncoderRunning" : true,
"videoResolution" : "640x480",
"minACNoScaling" : 40,
"irConfiguration" : 8448272,
"directLink" : true,
"overlayEnabled" : true,
"notify_when_offline" : true,
"micVolume" : 100,
"bitrateCeiling" : 512000,
"initialBitrate" : 384000,
"IVAEnabled" : false,
"turnOffCamera" : false,
"ringSoundEnabled" : true,
"JPGQuality" : 70,
"motion_notifications" : true,
"speakerVolume" : 92,
"buttonpush_notifications" : true,
"ABREnabled" : true,
"debug" : false,
"batteryLowThreshold" : 3.1,
"batteryRun" : false,
"IREnabled" : true,
"batteryUseThreshold" : 3.4
},
"doorbellServerURL" : "https://doorbells.august.com",
"name" : "Front Door",
"createdAt" : "2016-11-26T22:27:11.176Z",
"installDate" : "2016-11-26T22:27:11.176Z",
"serialNumber" : "tBXZR0Z35E",
"dvrSubscriptionSetupDone" : true,
"caps" : [
"reconnect"
],
"doorbellID" : "K98GiDT45GUL",
"HouseID" : "3dd2accaea08",
"telemetry" : {
"signal_level" : -56,
"date" : "2017-12-10 08:05:12",
"steady_ac_in" : 22.196405,
"BSSID" : "88:ee:00:dd:aa:11",
"SSID" : "foo_ssid",
"updated_at" : "2017-12-10T08:05:13.650Z",
"temperature" : 28.25,
"wifi_freq" : 5745,
"load_average" : "0.50 0.47 0.35 1/154 9345",
"link_quality" : 54,
"uptime" : "16168.75 13830.49",
"ip_addr" : "10.0.1.11",
"doorbell_low_battery" : false,
"ac_in" : 23.856874
},
"installUserID" : "c3b2a94e-373e-aaaa-bbbb-36e996827777",
"status" : "doorbell_call_status_online",
"firmwareVersion" : "2.3.0-RC153+201711151527",
"pubsubChannel" : "7c7a6672-59c8-3333-ffff-dcd98705cccc",
"updatedAt" : "2017-12-10T08:05:13.650Z"
}

View File

@ -0,0 +1,130 @@
{
"recentImage" : {
"tags" : [],
"height" : 576,
"public_id" : "fdsfds",
"bytes" : 50013,
"resource_type" : "image",
"original_filename" : "file",
"version" : 1582242766,
"format" : "jpg",
"signature" : "fdsfdsf",
"created_at" : "2020-02-20T23:52:46Z",
"type" : "upload",
"placeholder" : false,
"url" : "http://res.cloudinary.com/august-com/image/upload/ccc/ccccc.jpg",
"secure_url" : "https://res.cloudinary.com/august-com/image/upload/cc/cccc.jpg",
"etag" : "zds",
"width" : 720
},
"firmwareVersion" : "3.1.0-HYDRC75+201909251139",
"doorbellServerURL" : "https://doorbells.august.com",
"installUserID" : "mock",
"caps" : [
"reconnect",
"webrtc",
"tcp_wakeup"
],
"messagingProtocol" : "pubnub",
"createdAt" : "2020-02-12T03:52:28.719Z",
"invitations" : [],
"appID" : "august-iphone-v5",
"HouseID" : "houseid1",
"doorbellID" : "tmt100",
"name" : "Front Door",
"settings" : {
"batteryUseThreshold" : 3.4,
"brightness" : 50,
"batteryChargeCurrent" : 60,
"overCurrentThreshold" : -250,
"irLedBrightness" : 40,
"videoResolution" : "720x576",
"pirPulseCounter" : 1,
"contrast" : 50,
"micVolume" : 50,
"directLink" : true,
"auto_contrast_mode" : 0,
"saturation" : 50,
"motion_notifications" : true,
"pirSensitivity" : 20,
"pirBlindTime" : 7,
"notify_when_offline" : false,
"nightModeAlsThreshold" : 10,
"minACNoScaling" : 40,
"DVRRecordingTimeout" : 15,
"turnOffCamera" : false,
"debug" : false,
"keepEncoderRunning" : true,
"pirWindowTime" : 0,
"bitrateCeiling" : 2000000,
"backlight_comp" : false,
"buttonpush_notifications" : true,
"buttonpush_notifications_partners" : false,
"minimumSnapshotInterval" : 30,
"pirConfiguration" : 272,
"batteryLowThreshold" : 3.1,
"sharpness" : 50,
"ABREnabled" : true,
"hue" : 50,
"initialBitrate" : 1000000,
"ringSoundEnabled" : true,
"IVAEnabled" : false,
"overlayEnabled" : true,
"speakerVolume" : 92,
"ringRepetitions" : 3,
"powerProfilePreset" : -1,
"irConfiguration" : 16836880,
"JPGQuality" : 70,
"IREnabled" : true
},
"updatedAt" : "2020-02-20T23:58:21.580Z",
"serialNumber" : "abc",
"installDate" : "2019-02-12T03:52:28.719Z",
"dvrSubscriptionSetupDone" : true,
"pubsubChannel" : "mock",
"chimes" : [
{
"updatedAt" : "2020-02-12T03:55:38.805Z",
"_id" : "cccc",
"type" : 1,
"serialNumber" : "ccccc",
"doorbellID" : "tmt100",
"name" : "Living Room",
"chimeID" : "cccc",
"createdAt" : "2020-02-12T03:55:38.805Z",
"firmware" : "3.1.16"
}
],
"telemetry" : {
"battery" : 3.985,
"battery_soc" : 81,
"load_average" : "0.45 0.18 0.07 4/98 831",
"ip_addr" : "192.168.100.174",
"BSSID" : "snp",
"uptime" : "96.55 70.59",
"SSID" : "bob",
"updated_at" : "2020-02-20T23:53:09.586Z",
"dtim_period" : 0,
"wifi_freq" : 2462,
"date" : "2020-02-20 11:47:36",
"BSSIDManufacturer" : "Ubiquiti - Ubiquiti Networks Inc.",
"battery_temp" : 22,
"battery_avg_cur" : -291,
"beacon_interval" : 0,
"signal_level" : -49,
"battery_soh" : 95,
"doorbell_low_battery" : false
},
"secChipCertSerial" : "",
"tcpKeepAlive" : {
"keepAliveUUID" : "mock",
"wakeUp" : {
"token" : "wakemeup",
"lastUpdated" : 1582242723931
}
},
"statusUpdatedAtMs" : 1582243101579,
"status" : "doorbell_offline",
"type" : "hydra1",
"HouseName" : "housename"
}

View File

@ -0,0 +1,59 @@
{
"LockName": "Side Door",
"Type": 1001,
"Created": "2019-10-07T01:49:06.831Z",
"Updated": "2019-10-07T01:49:06.831Z",
"LockID": "BROKENID",
"HouseID": "abc",
"HouseName": "dog",
"Calibrated": false,
"timeZone": "America/Chicago",
"battery": 0.9524716174964851,
"hostLockInfo": {
"serialNumber": "YR",
"manufacturer": "yale",
"productID": 1536,
"productTypeID": 32770
},
"supportsEntryCodes": true,
"skuNumber": "AUG-MD01",
"macAddress": "MAC",
"SerialNumber": "M1FXZ00EZ9",
"LockStatus": {
"status": "unknown_error_during_connect",
"dateTime": "2020-02-22T02:48:11.741Z",
"isLockStatusChanged": true,
"valid": true,
"doorState": "closed"
},
"currentFirmwareVersion": "undefined-4.3.0-1.8.14",
"homeKitEnabled": true,
"zWaveEnabled": false,
"isGalileo": false,
"Bridge": {
"_id": "id",
"mfgBridgeID": "id",
"deviceModel": "august-connect",
"firmwareVersion": "2.2.1",
"operative": true,
"status": {
"current": "online",
"updated": "2020-02-21T15:06:47.001Z",
"lastOnline": "2020-02-21T15:06:47.001Z",
"lastOffline": "2020-02-06T17:33:21.265Z"
},
"hyperBridge": true
},
"parametersToSet": {},
"ruleHash": {},
"cameras": [],
"geofenceLimits": {
"ios": {
"debounceInterval": 90,
"gpsAccuracyMultiplier": 2.5,
"maximumGeofence": 5000,
"minimumGeofence": 100,
"minGPSAccuracyRequired": 80
}
}
}

View File

@ -0,0 +1,50 @@
{
"Bridge" : {
"_id" : "bridgeid",
"deviceModel" : "august-connect",
"firmwareVersion" : "2.2.1",
"hyperBridge" : true,
"mfgBridgeID" : "C5WY200WSH",
"operative" : true,
"status" : {
"current" : "online",
"lastOffline" : "2000-00-00T00:00:00.447Z",
"lastOnline" : "2000-00-00T00:00:00.447Z",
"updated" : "2000-00-00T00:00:00.447Z"
}
},
"Calibrated" : false,
"Created" : "2000-00-00T00:00:00.447Z",
"HouseID" : "123",
"HouseName" : "Test",
"LockID" : "missing_doorsense_id",
"LockName" : "Online door missing doorsense",
"LockStatus" : {
"dateTime" : "2017-12-10T04:48:30.272Z",
"isLockStatusChanged" : false,
"status" : "locked",
"valid" : true
},
"SerialNumber" : "XY",
"Type" : 1001,
"Updated" : "2000-00-00T00:00:00.447Z",
"battery" : 0.922,
"currentFirmwareVersion" : "undefined-4.3.0-1.8.14",
"homeKitEnabled" : true,
"hostLockInfo" : {
"manufacturer" : "yale",
"productID" : 1536,
"productTypeID" : 32770,
"serialNumber" : "ABC"
},
"isGalileo" : false,
"macAddress" : "12:22",
"pins" : {
"created" : [],
"loaded" : []
},
"skuNumber" : "AUG-MD01",
"supportsEntryCodes" : true,
"timeZone" : "Pacific/Hawaii",
"zWaveEnabled" : false
}

View File

@ -17,7 +17,7 @@
"Created" : "2000-00-00T00:00:00.447Z",
"HouseID" : "123",
"HouseName" : "Test",
"LockID" : "ABC",
"LockID" : "online_with_doorsense",
"LockName" : "Online door with doorsense",
"LockStatus" : {
"dateTime" : "2017-12-10T04:48:30.272Z",