Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduced Ring binary sensors and refactored Ring component #6520

Merged
merged 20 commits into from Mar 31, 2017
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
107 changes: 107 additions & 0 deletions homeassistant/components/binary_sensor/ring.py
@@ -0,0 +1,107 @@
"""
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
import voluptuous as vol
import homeassistant.helpers.config_validation as cv

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

from homeassistant.loader import get_component
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS,
ATTR_ATTRIBUTION)

DEPENDENCIES = ['ring']

_LOGGER = logging.getLogger(__name__)

# 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 = get_component('ring')

sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
for device in ring.RING.data.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._cache = hass.config.path(DEFAULT_CACHEDB)
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(cache=self._cache)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to pass in the cache? That is something that should be handled by the shared data class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache file is required for multiple sensors. It basically will save a pickle data file with the notification state shared by different sensors. The cache if required because it is shared on the object level and not on the account level which is shared by hass.data.


if self._data.alert:
self._state = bool(self._sensor_type ==
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The == will always return a boolean, no need to make it one.

self._data.alert.get('kind'))
else:
self._state = False
74 changes: 74 additions & 0 deletions homeassistant/components/ring.py
@@ -0,0 +1,74 @@
"""
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.2']

_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)
SCAN_INTERVAL = timedelta(seconds=5)

RING = None

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."""
global RING
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

ring = Ring(username, password)
if ring.is_connected:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to return False if not connected, otherwise the component will be seen as set up correctly but the platforms will not be able to find the entry in hass.data

RING = RingData(ring)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use hass.data to store RING. Do not use globals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Thanks for the heads.

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


class RingData(object):
"""Stores the data retrived for Ring device."""

def __init__(self, data):
"""Initialize the data object."""
self.data = data
71 changes: 26 additions & 45 deletions homeassistant/components/sensor/ring.py
Expand Up @@ -5,43 +5,35 @@
https://home-assistant.io/components/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, 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)
from homeassistant.helpers.entity import Entity
import homeassistant.loader as loader
STATE_UNKNOWN, ATTR_ATTRIBUTION)

from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.loader import get_component
from homeassistant.helpers.entity import Entity

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,32 +45,17 @@

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 = get_component('ring')

sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
for device in ring.chimes:
for device in ring.RING.data.chimes:
if 'chime' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingSensor(hass,
device,
sensor_type))

for device in ring.doorbells:
for device in ring.RING.data.doorbells:
if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingSensor(hass,
device,
Expand All @@ -98,6 +75,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 +103,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 +131,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
Expand Up @@ -649,8 +649,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.2

# homeassistant.components.switch.rpi_rf
# rpi-rf==0.9.6
Expand Down