Support for August doorbell (#11124)

* Add support for August doorbell

* Address PR comment for August platform

* Address PR comment for August binary sensor

* Address PR comment for August camera

* Addressed PR comment for August lock

* - Fixed houndci-bot error

* - Updated configurator description

* - Fixed stale docstring

* Added august module to .coveragerc
pull/12126/head^2
Joe Lu 2018-02-18 00:24:51 -08:00 committed by Martin Hjelmare
parent e8d8b75c07
commit a8444b22e7
6 changed files with 518 additions and 0 deletions

View File

@ -38,6 +38,9 @@ omit =
homeassistant/components/asterisk_mbox.py
homeassistant/components/*/asterisk_mbox.py
homeassistant/components/august.py
homeassistant/components/*/august.py
homeassistant/components/axis.py
homeassistant/components/*/axis.py

View File

@ -0,0 +1,257 @@
"""
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)
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
_CONFIGURING = {}
REQUIREMENTS = ['py-august==0.3.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(api, authentication.access_token)
for component in AUGUST_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
elif state == AuthenticationState.BAD_PASSWORD:
return False
elif 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
conf = config[DOMAIN]
api = Api(timeout=conf.get(CONF_TIMEOUT))
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))
return setup_august(hass, config, api, authenticator)
class AugustData:
"""August data object."""
def __init__(self, api, access_token):
"""Init August data object."""
self._api = api
self._access_token = access_token
self._doorbells = self._api.get_doorbells(self._access_token) or []
self._locks = self._api.get_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._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."""
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."""
for house_id in self.house_ids:
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]
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 = {}
for doorbell in self._doorbells:
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
self._access_token, doorbell.device_id)
self._doorbell_detail_by_id = detail_by_id
def get_lock_status(self, lock_id):
"""Return lock status."""
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)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_locks(self):
status_by_id = {}
detail_by_id = {}
for lock in self._locks:
status_by_id[lock.device_id] = self._api.get_lock_status(
self._access_token, lock.device_id)
detail_by_id[lock.device_id] = self._api.get_lock_detail(
self._access_token, lock.device_id)
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)

View File

@ -0,0 +1,97 @@
"""
Support for August binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.august/
"""
from datetime import timedelta, datetime
from homeassistant.components.august import DATA_AUGUST
from homeassistant.components.binary_sensor import (BinarySensorDevice)
DEPENDENCIES = ['august']
SCAN_INTERVAL = timedelta(seconds=5)
def _retrieve_online_state(data, doorbell):
"""Get the latest state of the sensor."""
detail = data.get_doorbell_detail(doorbell.device_id)
return detail.is_online
def _retrieve_motion_state(data, doorbell):
from august.activity import ActivityType
return _activity_time_based_state(data, doorbell,
[ActivityType.DOORBELL_MOTION,
ActivityType.DOORBELL_DING])
def _retrieve_ding_state(data, doorbell):
from august.activity import ActivityType
return _activity_time_based_state(data, doorbell,
[ActivityType.DOORBELL_DING])
def _activity_time_based_state(data, doorbell, activity_types):
"""Get the latest state of the sensor."""
latest = data.get_latest_device_activity(doorbell.device_id,
*activity_types)
if latest is not None:
start = latest.activity_start_time
end = latest.activity_end_time + timedelta(seconds=30)
return start <= datetime.now() <= end
return None
# Sensor types: Name, device_class, state_provider
SENSOR_TYPES = {
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the August binary sensors."""
data = hass.data[DATA_AUGUST]
devices = []
for doorbell in data.doorbells:
for sensor_type in SENSOR_TYPES:
devices.append(AugustBinarySensor(data, sensor_type, doorbell))
add_devices(devices, True)
class AugustBinarySensor(BinarySensorDevice):
"""Representation of an August binary sensor."""
def __init__(self, data, sensor_type, doorbell):
"""Initialize the sensor."""
self._data = data
self._sensor_type = sensor_type
self._doorbell = doorbell
self._state = None
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return SENSOR_TYPES[self._sensor_type][1]
@property
def name(self):
"""Return the name of the binary sensor."""
return "{} {}".format(self._doorbell.device_name,
SENSOR_TYPES[self._sensor_type][0])
def update(self):
"""Get the latest state of the sensor."""
state_provider = SENSOR_TYPES[self._sensor_type][2]
self._state = state_provider(self._data, self._doorbell)

View File

@ -0,0 +1,76 @@
"""
Support for August camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.august/
"""
from datetime import timedelta
import requests
from homeassistant.components.august import DATA_AUGUST, DEFAULT_TIMEOUT
from homeassistant.components.camera import Camera
DEPENDENCIES = ['august']
SCAN_INTERVAL = timedelta(seconds=5)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up August cameras."""
data = hass.data[DATA_AUGUST]
devices = []
for doorbell in data.doorbells:
devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT))
add_devices(devices, True)
class AugustCamera(Camera):
"""An implementation of a Canary security camera."""
def __init__(self, data, doorbell, timeout):
"""Initialize a Canary security camera."""
super().__init__()
self._data = data
self._doorbell = doorbell
self._timeout = timeout
self._image_url = None
self._image_content = None
@property
def name(self):
"""Return the name of this device."""
return self._doorbell.device_name
@property
def is_recording(self):
"""Return true if the device is recording."""
return self._doorbell.has_subscription
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return True
@property
def brand(self):
"""Return the camera brand."""
return 'August'
@property
def model(self):
"""Return the camera model."""
return 'Doorbell'
def camera_image(self):
"""Return bytes of camera image."""
latest = self._data.get_doorbell_detail(self._doorbell.device_id)
if self._image_url is not latest.image_url:
self._image_url = latest.image_url
self._image_content = requests.get(self._image_url,
timeout=self._timeout).content
return self._image_content

View File

@ -0,0 +1,82 @@
"""
Support for August lock.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.august/
"""
from datetime import timedelta
from homeassistant.components.august import DATA_AUGUST
from homeassistant.components.lock import LockDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
DEPENDENCIES = ['august']
SCAN_INTERVAL = timedelta(seconds=5)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up August locks."""
data = hass.data[DATA_AUGUST]
devices = []
for lock in data.locks:
devices.append(AugustLock(data, lock))
add_devices(devices, True)
class AugustLock(LockDevice):
"""Representation of an August lock."""
def __init__(self, data, lock):
"""Initialize the lock."""
self._data = data
self._lock = lock
self._lock_status = None
self._lock_detail = None
self._changed_by = None
def lock(self, **kwargs):
"""Lock the device."""
self._data.lock(self._lock.device_id)
def unlock(self, **kwargs):
"""Unlock the device."""
self._data.unlock(self._lock.device_id)
def update(self):
"""Get the latest state of the sensor."""
self._lock_status = self._data.get_lock_status(self._lock.device_id)
self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
from august.activity import ActivityType
activity = self._data.get_latest_device_activity(
self._lock.device_id,
ActivityType.LOCK_OPERATION)
if activity is not None:
self._changed_by = activity.operated_by
@property
def name(self):
"""Return the name of this device."""
return self._lock.device_name
@property
def is_locked(self):
"""Return true if device is on."""
from august.lock import LockStatus
return self._lock_status is LockStatus.LOCKED
@property
def changed_by(self):
"""Last change triggered by."""
return self._changed_by
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
return {
ATTR_BATTERY_LEVEL: self._lock_detail.battery_level,
}

View File

@ -612,6 +612,9 @@ pushetta==1.0.15
# homeassistant.components.light.rpi_gpio_pwm
pwmled==1.2.1
# homeassistant.components.august
py-august==0.3.0
# homeassistant.components.canary
py-canary==0.4.0