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
parent
900714a3ee
commit
2925e0617c
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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"]
|
|
@ -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."""
|
|
@ -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()
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ FLOWS = [
|
|||
"almond",
|
||||
"ambiclimate",
|
||||
"ambient_station",
|
||||
"august",
|
||||
"axis",
|
||||
"brother",
|
||||
"cast",
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue