356 lines
12 KiB
Python
356 lines
12 KiB
Python
"""
|
|
Support for August devices.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/components/august/
|
|
"""
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
import voluptuous as vol
|
|
from requests import RequestException
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.const import (
|
|
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
|
|
from homeassistant.helpers import discovery
|
|
from homeassistant.util import Throttle
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
_CONFIGURING = {}
|
|
|
|
REQUIREMENTS = ['py-august==0.7.0']
|
|
|
|
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'
|
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
|
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
|
LOGIN_METHODS = ['phone', 'email']
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
DOMAIN: vol.Schema({
|
|
vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
|
|
vol.Required(CONF_USERNAME): cv.string,
|
|
vol.Required(CONF_PASSWORD): cv.string,
|
|
vol.Optional(CONF_INSTALL_ID): cv.string,
|
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
|
})
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
AUGUST_COMPONENTS = [
|
|
'camera', 'binary_sensor', 'lock'
|
|
]
|
|
|
|
|
|
def request_configuration(hass, config, api, authenticator):
|
|
"""Request configuration steps from the user."""
|
|
configurator = hass.components.configurator
|
|
|
|
def august_configuration_callback(data):
|
|
"""Run when the configuration callback is called."""
|
|
from august.authenticator import ValidationResult
|
|
|
|
result = authenticator.validate_verification_code(
|
|
data.get('verification_code'))
|
|
|
|
if result == ValidationResult.INVALID_VERIFICATION_CODE:
|
|
configurator.notify_errors(_CONFIGURING[DOMAIN],
|
|
"Invalid verification code")
|
|
elif result == ValidationResult.VALIDATED:
|
|
setup_august(hass, config, api, authenticator)
|
|
|
|
if DOMAIN not in _CONFIGURING:
|
|
authenticator.send_verification_code()
|
|
|
|
conf = config[DOMAIN]
|
|
username = conf.get(CONF_USERNAME)
|
|
login_method = conf.get(CONF_LOGIN_METHOD)
|
|
|
|
_CONFIGURING[DOMAIN] = configurator.request_config(
|
|
NOTIFICATION_TITLE,
|
|
august_configuration_callback,
|
|
description="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'}]
|
|
)
|
|
|
|
|
|
def setup_august(hass, config, api, authenticator):
|
|
"""Set up the August component."""
|
|
from august.authenticator import AuthenticationState
|
|
|
|
authentication = None
|
|
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: {}<br />"
|
|
"You will need to restart hass after fixing."
|
|
"".format(ex),
|
|
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.access_token)
|
|
|
|
for component in AUGUST_COMPONENTS:
|
|
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
|
|
|
return True
|
|
if state == AuthenticationState.BAD_PASSWORD:
|
|
_LOGGER.error("Invalid password provided")
|
|
return False
|
|
if state == AuthenticationState.REQUIRES_VALIDATION:
|
|
request_configuration(hass, config, api, authenticator)
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def setup(hass, config):
|
|
"""Set up the August component."""
|
|
from august.api import Api
|
|
from august.authenticator import Authenticator
|
|
from requests import Session
|
|
|
|
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))
|
|
|
|
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
|
|
|
|
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))
|
|
|
|
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
|
|
|
|
_LOGGER.debug("August HTTP session closed.")
|
|
|
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
|
|
_LOGGER.debug("Registered for HASS stop event")
|
|
|
|
return setup_august(hass, config, api, authenticator)
|
|
|
|
|
|
class AugustData:
|
|
"""August data object."""
|
|
|
|
def __init__(self, hass, api, access_token):
|
|
"""Init August data object."""
|
|
self._hass = hass
|
|
self._api = api
|
|
self._access_token = access_token
|
|
self._doorbells = self._api.get_doorbells(self._access_token) or []
|
|
self._locks = self._api.get_operable_locks(self._access_token) or []
|
|
self._house_ids = [d.house_id for d in self._doorbells + self._locks]
|
|
|
|
self._doorbell_detail_by_id = {}
|
|
self._lock_status_by_id = {}
|
|
self._lock_detail_by_id = {}
|
|
self._door_state_by_id = {}
|
|
self._activities_by_id = {}
|
|
|
|
@property
|
|
def house_ids(self):
|
|
"""Return a list of house_ids."""
|
|
return self._house_ids
|
|
|
|
@property
|
|
def doorbells(self):
|
|
"""Return a list of doorbells."""
|
|
return self._doorbells
|
|
|
|
@property
|
|
def locks(self):
|
|
"""Return a list of locks."""
|
|
return self._locks
|
|
|
|
def get_device_activities(self, device_id, *activity_types):
|
|
"""Return a list of activities."""
|
|
_LOGGER.debug("Getting device activities")
|
|
self._update_device_activities()
|
|
|
|
activities = self._activities_by_id.get(device_id, [])
|
|
if activity_types:
|
|
return [a for a in activities if a.activity_type in activity_types]
|
|
return activities
|
|
|
|
def get_latest_device_activity(self, device_id, *activity_types):
|
|
"""Return latest activity."""
|
|
activities = self.get_device_activities(device_id, *activity_types)
|
|
return next(iter(activities or []), None)
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
|
"""Update data object with latest from August API."""
|
|
_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)
|
|
|
|
device_ids = {a.device_id for a in activities}
|
|
for device_id in device_ids:
|
|
self._activities_by_id[device_id] = [a for a in activities if
|
|
a.device_id == device_id]
|
|
_LOGGER.debug("Completed retrieving device activities")
|
|
|
|
def get_doorbell_detail(self, doorbell_id):
|
|
"""Return doorbell detail."""
|
|
self._update_doorbells()
|
|
return self._doorbell_detail_by_id.get(doorbell_id)
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
def _update_doorbells(self):
|
|
detail_by_id = {}
|
|
|
|
_LOGGER.debug("Start retrieving doorbell details")
|
|
for doorbell in self._doorbells:
|
|
_LOGGER.debug("Updating doorbell status for %s",
|
|
doorbell.device_name)
|
|
try:
|
|
detail_by_id[doorbell.device_id] =\
|
|
self._api.get_doorbell_detail(
|
|
self._access_token, doorbell.device_id)
|
|
except RequestException as ex:
|
|
_LOGGER.error("Request error trying to retrieve doorbell"
|
|
" status for %s. %s", doorbell.device_name, ex)
|
|
detail_by_id[doorbell.device_id] = None
|
|
except Exception:
|
|
detail_by_id[doorbell.device_id] = None
|
|
raise
|
|
|
|
_LOGGER.debug("Completed retrieving doorbell details")
|
|
self._doorbell_detail_by_id = detail_by_id
|
|
|
|
def get_lock_status(self, lock_id):
|
|
"""Return status if the door is locked or unlocked.
|
|
|
|
This is status for the lock itself.
|
|
"""
|
|
self._update_locks()
|
|
return self._lock_status_by_id.get(lock_id)
|
|
|
|
def get_lock_detail(self, lock_id):
|
|
"""Return lock detail."""
|
|
self._update_locks()
|
|
return self._lock_detail_by_id.get(lock_id)
|
|
|
|
def get_door_state(self, lock_id):
|
|
"""Return status if the door is open or closed.
|
|
|
|
This is the status from the door sensor.
|
|
"""
|
|
self._update_doors()
|
|
return self._door_state_by_id.get(lock_id)
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
def _update_doors(self):
|
|
state_by_id = {}
|
|
|
|
_LOGGER.debug("Start retrieving door status")
|
|
for lock in self._locks:
|
|
_LOGGER.debug("Updating door status for %s",
|
|
lock.device_name)
|
|
|
|
try:
|
|
state_by_id[lock.device_id] = self._api.get_lock_door_status(
|
|
self._access_token, lock.device_id)
|
|
except RequestException as ex:
|
|
_LOGGER.error("Request error trying to retrieve door"
|
|
" status for %s. %s", lock.device_name, ex)
|
|
state_by_id[lock.device_id] = None
|
|
except Exception:
|
|
state_by_id[lock.device_id] = None
|
|
raise
|
|
|
|
_LOGGER.debug("Completed retrieving door status")
|
|
self._door_state_by_id = state_by_id
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
def _update_locks(self):
|
|
status_by_id = {}
|
|
detail_by_id = {}
|
|
|
|
_LOGGER.debug("Start retrieving locks status")
|
|
for lock in self._locks:
|
|
_LOGGER.debug("Updating lock status for %s",
|
|
lock.device_name)
|
|
try:
|
|
status_by_id[lock.device_id] = self._api.get_lock_status(
|
|
self._access_token, lock.device_id)
|
|
except RequestException as ex:
|
|
_LOGGER.error("Request error trying to retrieve door"
|
|
" status for %s. %s", lock.device_name, ex)
|
|
status_by_id[lock.device_id] = None
|
|
except Exception:
|
|
status_by_id[lock.device_id] = None
|
|
raise
|
|
|
|
try:
|
|
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
|
self._access_token, lock.device_id)
|
|
except RequestException as ex:
|
|
_LOGGER.error("Request error trying to retrieve door"
|
|
" details for %s. %s", lock.device_name, ex)
|
|
detail_by_id[lock.device_id] = None
|
|
except Exception:
|
|
detail_by_id[lock.device_id] = None
|
|
raise
|
|
|
|
_LOGGER.debug("Completed retrieving locks status")
|
|
self._lock_status_by_id = status_by_id
|
|
self._lock_detail_by_id = detail_by_id
|
|
|
|
def lock(self, device_id):
|
|
"""Lock the device."""
|
|
return self._api.lock(self._access_token, device_id)
|
|
|
|
def unlock(self, device_id):
|
|
"""Unlock the device."""
|
|
return self._api.unlock(self._access_token, device_id)
|