Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions homeassistant/components/binary_sensor/ring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
This component provides HA sensor support for Ring Door Bell/Chimes.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ring/
"""
import logging
from datetime import timedelta

import voluptuous as vol
import homeassistant.helpers.config_validation as cv

from homeassistant.components.ring import (
CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE)

from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS)

from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)

DEPENDENCIES = ['ring']

_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = timedelta(seconds=5)

# Sensor types: Name, category, device_class
SENSOR_TYPES = {
'ding': ['Ding', ['doorbell'], 'occupancy'],
'motion': ['Motion', ['doorbell'], 'motion'],
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for a Ring device."""
ring = hass.data.get('ring')

sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
for device in ring.doorbells:
if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass,
device,
sensor_type))
add_devices(sensors, True)
return True


class RingBinarySensor(BinarySensorDevice):
"""A binary sensor implementation for Ring device."""

def __init__(self, hass, data, sensor_type):
"""Initialize a sensor for Ring device."""
super(RingBinarySensor, self).__init__()
self._sensor_type = sensor_type
self._data = data
self._name = "{0} {1}".format(self._data.name,
SENSOR_TYPES.get(self._sensor_type)[0])
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
self._state = None

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@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 the binary sensor."""
return self._device_class

@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION

attrs['device_id'] = self._data.id
attrs['firmware'] = self._data.firmware
attrs['timezone'] = self._data.timezone

if self._data.alert and self._data.alert_expires_at:
attrs['expires_at'] = self._data.alert_expires_at
attrs['state'] = self._data.alert.get('state')

return attrs

def update(self):
"""Get the latest data and updates the state."""
self._data.check_alerts()

if self._data.alert:
self._state = (self._sensor_type ==
self._data.alert.get('kind'))
else:
self._state = False
63 changes: 63 additions & 0 deletions homeassistant/components/ring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Support for Ring Doorbell/Chimes.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/ring/
"""
from datetime import timedelta
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv

from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
import homeassistant.loader as loader

from requests.exceptions import HTTPError, ConnectTimeout

REQUIREMENTS = ['ring_doorbell==0.1.3']

_LOGGER = logging.getLogger(__name__)

CONF_ATTRIBUTION = "Data provided by Ring.com"

NOTIFICATION_ID = 'ring_notification'
NOTIFICATION_TITLE = 'Ring Sensor Setup'

DOMAIN = 'ring'
DEFAULT_CACHEDB = '.ring_cache.pickle'
DEFAULT_ENTITY_NAMESPACE = 'ring'
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
"""Set up Ring component."""
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)

persistent_notification = loader.get_component('persistent_notification')
try:
from ring_doorbell import Ring

cache = hass.config.path(DEFAULT_CACHEDB)
ring = Ring(username=username, password=password, cache_file=cache)
if not ring.is_connected:
return False
hass.data['ring'] = ring
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Ring service: %s", str(ex))
persistent_notification.create(
hass, 'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
return True
64 changes: 21 additions & 43 deletions homeassistant/components/sensor/ring.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,32 @@
https://home-assistant.io/components/sensor.ring/
"""
import logging
from datetime import timedelta

import voluptuous as vol

import homeassistant.loader as loader
import homeassistant.helpers.config_validation as cv
from homeassistant.components.ring import (
CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DEFAULT_SCAN_INTERVAL)
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL,
CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN,
ATTR_ATTRIBUTION)
STATE_UNKNOWN, ATTR_ATTRIBUTION)
from homeassistant.helpers.entity import Entity

from requests.exceptions import HTTPError, ConnectTimeout

REQUIREMENTS = ['ring_doorbell==0.1.0']
DEPENDENCIES = ['ring']

_LOGGER = logging.getLogger(__name__)

NOTIFICATION_ID = 'ring_notification'
NOTIFICATION_TITLE = 'Ring Sensor Setup'

DEFAULT_ENTITY_NAMESPACE = 'ring'
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)

CONF_ATTRIBUTION = "Data provided by Ring.com"

# Sensor types: Name, category, units, icon
# Sensor types: Name, category, units, icon, kind
SENSOR_TYPES = {
'battery': ['Battery', ['doorbell'], '%', 'battery-50'],
'last_activity': ['Last Activity', ['doorbell'], None, 'history'],
'volume': ['Volume', ['chime', 'doorbell'], None, 'bell-ring'],
'battery': ['Battery', ['doorbell'], '%', 'battery-50', None],
'last_activity': ['Last Activity', ['doorbell'], None, 'history', None],
'last_ding': ['Last Ding', ['doorbell'], None, 'history', 'ding'],
'last_motion': ['Last Motion', ['doorbell'], None, 'history', 'motion'],
'volume': ['Volume', ['chime', 'doorbell'], None, 'bell-ring', None],
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
Expand All @@ -53,22 +42,7 @@

def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for a Ring device."""
from ring_doorbell import Ring

ring = Ring(config.get(CONF_USERNAME), config.get(CONF_PASSWORD))

persistent_notification = loader.get_component('persistent_notification')
try:
ring.is_connected
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Ring service: %s", str(ex))
persistent_notification.create(
hass, 'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
ring = hass.data.get('ring')

sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
Expand Down Expand Up @@ -98,6 +72,7 @@ def __init__(self, hass, data, sensor_type):
self._data = data
self._extra = None
self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[3])
self._kind = SENSOR_TYPES.get(self._sensor_type)[4]
self._name = "{0} {1}".format(self._data.name,
SENSOR_TYPES.get(self._sensor_type)[0])
self._state = STATE_UNKNOWN
Expand Down Expand Up @@ -125,7 +100,7 @@ def device_state_attributes(self):
attrs['timezone'] = self._data.timezone
attrs['type'] = self._data.family

if self._extra and self._sensor_type == 'last_activity':
if self._extra and self._sensor_type.startswith('last_'):
attrs['created_at'] = self._extra['created_at']
attrs['answered'] = self._extra['answered']
attrs['recording_status'] = self._extra['recording']['status']
Expand Down Expand Up @@ -153,8 +128,11 @@ def update(self):
if self._sensor_type == 'battery':
self._state = self._data.battery_life

if self._sensor_type == 'last_activity':
self._extra = self._data.history(limit=1, timezone=self._tz)[0]
created_at = self._extra['created_at']
self._state = '{0:0>2}:{1:0>2}'.format(created_at.hour,
created_at.minute)
if self._sensor_type.startswith('last_'):
history = self._data.history(timezone=self._tz,
kind=self._kind)
if history:
self._extra = history[0]
created_at = self._extra['created_at']
self._state = '{0:0>2}:{1:0>2}'.format(created_at.hour,
created_at.minute)
4 changes: 2 additions & 2 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,8 @@ radiotherm==1.2
# homeassistant.components.rflink
rflink==0.0.28

# homeassistant.components.sensor.ring
ring_doorbell==0.1.0
# homeassistant.components.ring
ring_doorbell==0.1.3

# homeassistant.components.switch.rpi_rf
# rpi-rf==0.9.6
Expand Down
63 changes: 63 additions & 0 deletions tests/components/binary_sensor/test_ring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""The tests for the Ring binary sensor platform."""
import unittest
import requests_mock

from homeassistant.components.binary_sensor import ring
from homeassistant.components import ring as base_ring

from tests.components.test_ring import ATTRIBUTION, VALID_CONFIG
from tests.common import get_test_home_assistant, load_fixture


class TestRingBinarySensorSetup(unittest.TestCase):
"""Test the Ring Binary Sensor platform."""

DEVICES = []

def add_devices(self, devices, action):
"""Mock add devices."""
for device in devices:
self.DEVICES.append(device)

def setUp(self):
"""Initialize values for this testcase class."""
self.hass = get_test_home_assistant()
self.config = {
'username': 'foo',
'password': 'bar',
'monitored_conditions': ['ding', 'motion'],
}

def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()

@requests_mock.Mocker()
def test_binary_sensor(self, mock):
"""Test the Ring sensor class and methods."""
mock.post('https://api.ring.com/clients_api/session',
text=load_fixture('ring_session.json'))
mock.get('https://api.ring.com/clients_api/ring_devices',
text=load_fixture('ring_devices.json'))
mock.get('https://api.ring.com/clients_api/dings/active',
text=load_fixture('ring_ding_active.json'))

base_ring.setup(self.hass, VALID_CONFIG)
ring.setup_platform(self.hass,
self.config,
self.add_devices,
None)

for device in self.DEVICES:
device.update()
if device.name == 'Front Door Ding':
self.assertEqual('on', device.state)
self.assertEqual('America/New_York',
device.device_state_attributes['timezone'])
elif device.name == 'Front Door Motion':
self.assertEqual('off', device.state)
self.assertEqual('motion', device.device_class)

self.assertIsNone(device.entity_picture)
self.assertEqual(ATTRIBUTION,
device.device_state_attributes['attribution'])
Loading