Change how ring polls for changes to allow more platforms to be added (#25534)

* Add in a switch to control lights and sirens

* Improve the way sensors are updated

* fixes following flake8

* remove light platform, and fix breaking test.

* Resolve issues with tests

* add tests for the switch platform

* fix up flake8 errors

* fix the long strings

* fix naming on private method.

* updates following p/r

* further fixes following pr

* removed import

* add additional tests to improve code coverage

* forgot to check this in
This commit is contained in:
Ross Dargan 2019-07-31 19:08:40 +01:00 committed by Paulus Schoutsen
parent 92991b53c4
commit 5e7465a261
9 changed files with 118 additions and 38 deletions

View file

@ -1,10 +1,14 @@
"""Support for Ring Doorbell/Chimes."""
import logging
from datetime import timedelta
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, \
CONF_SCAN_INTERVAL
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@ -14,15 +18,23 @@ ATTRIBUTION = "Data provided by Ring.com"
NOTIFICATION_ID = 'ring_notification'
NOTIFICATION_TITLE = 'Ring Setup'
DATA_RING = 'ring'
DATA_RING_DOORBELLS = 'ring_doorbells'
DATA_RING_STICKUP_CAMS = 'ring_stickup_cams'
DATA_RING_CHIMES = 'ring_chimes'
DOMAIN = 'ring'
DEFAULT_CACHEDB = '.ring_cache.pickle'
DEFAULT_ENTITY_NAMESPACE = 'ring'
SIGNAL_UPDATE_RING = 'ring_update'
SCAN_INTERVAL = timedelta(seconds=10)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
}),
}, extra=vol.ALLOW_EXTRA)
@ -32,6 +44,7 @@ def setup(hass, config):
conf = config[DOMAIN]
username = conf[CONF_USERNAME]
password = conf[CONF_PASSWORD]
scan_interval = conf[CONF_SCAN_INTERVAL]
try:
from ring_doorbell import Ring
@ -40,7 +53,12 @@ def setup(hass, config):
ring = Ring(username=username, password=password, cache_file=cache)
if not ring.is_connected:
return False
hass.data['ring'] = ring
hass.data[DATA_RING_CHIMES] = chimes = ring.chimes
hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells
hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams
ring_devices = chimes + doorbells + stickup_cams
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Ring service: %s", str(ex))
hass.components.persistent_notification.create(
@ -50,4 +68,27 @@ def setup(hass, config):
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
def service_hub_refresh(service):
hub_refresh()
def timer_hub_refresh(event_time):
hub_refresh()
def hub_refresh():
"""Call ring to refresh information."""
_LOGGER.debug("Updating Ring Hub component")
for camera in ring_devices:
_LOGGER.debug("Updating camera %s", camera.name)
camera.update()
dispatcher_send(hass, SIGNAL_UPDATE_RING)
# register service
hass.services.register(DOMAIN, 'update', service_hub_refresh)
# register scan interval for ring
track_time_interval(hass, timer_hub_refresh, scan_interval)
return True

View file

@ -10,7 +10,8 @@ from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS)
import homeassistant.helpers.config_validation as cv
from . import ATTRIBUTION, DATA_RING, DEFAULT_ENTITY_NAMESPACE
from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS,\
DEFAULT_ENTITY_NAMESPACE
_LOGGER = logging.getLogger(__name__)
@ -32,15 +33,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a sensor for a Ring device."""
ring = hass.data[DATA_RING]
ring_doorbells = hass.data[DATA_RING_DOORBELLS]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
sensors = []
for device in ring.doorbells: # ring.doorbells is doing I/O
for device in ring_doorbells: # ring.doorbells is doing I/O
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass, device, sensor_type))
for device in ring.stickup_cams: # ring.stickup_cams is doing I/O
for device in ring_stickup_cams: # ring.stickup_cams is doing I/O
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass, device, sensor_type))

View file

@ -7,12 +7,15 @@ import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.util import dt as dt_util
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
from . import ATTRIBUTION, DATA_RING, NOTIFICATION_ID
from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, \
NOTIFICATION_ID, SIGNAL_UPDATE_RING
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
@ -22,27 +25,19 @@ _LOGGER = logging.getLogger(__name__)
NOTIFICATION_TITLE = 'Ring Camera Setup'
SCAN_INTERVAL = timedelta(seconds=90)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Ring Door Bell and StickUp Camera."""
ring = hass.data[DATA_RING]
ring_doorbell = hass.data[DATA_RING_DOORBELLS]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
cams = []
cams_no_plan = []
for camera in ring.doorbells:
if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
cams_no_plan.append(camera)
for camera in ring.stickup_cams:
for camera in ring_doorbell + ring_stickup_cams:
if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
@ -83,6 +78,17 @@ class RingCam(Camera):
self._utcnow = dt_util.utcnow()
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_RING, self._update_callback)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
_LOGGER.debug("Updating Ring camera %s (callback)", self.name)
@property
def name(self):
"""Return the name of this camera."""
@ -141,14 +147,13 @@ class RingCam(Camera):
@property
def should_poll(self):
"""Update the image periodically."""
return True
"""Updates controlled via the hub."""
return False
def update(self):
"""Update camera entity and refresh attributes."""
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
self._camera.update()
self._utcnow = dt_util.utcnow()
try:

View file

@ -1,5 +1,4 @@
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
from datetime import timedelta
import logging
import voluptuous as vol
@ -10,13 +9,15 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
from . import ATTRIBUTION, DATA_RING, DEFAULT_ENTITY_NAMESPACE
from . import ATTRIBUTION, DATA_RING_CHIMES, DATA_RING_DOORBELLS, \
DATA_RING_STICKUP_CAMS, DEFAULT_ENTITY_NAMESPACE, \
SIGNAL_UPDATE_RING
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
# Sensor types: Name, category, units, icon, kind
SENSOR_TYPES = {
'battery': [
@ -55,20 +56,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a sensor for a Ring device."""
ring = hass.data[DATA_RING]
ring_chimes = hass.data[DATA_RING_CHIMES]
ring_doorbells = hass.data[DATA_RING_DOORBELLS]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
sensors = []
for device in ring.chimes: # ring.chimes is doing I/O
for device in ring_chimes:
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'chime' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingSensor(hass, device, sensor_type))
for device in ring.doorbells: # ring.doorbells is doing I/O
for device in ring_doorbells:
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'doorbell' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingSensor(hass, device, sensor_type))
for device in ring.stickup_cams: # ring.stickup_cams is doing I/O
for device in ring_stickup_cams:
for sensor_type in config[CONF_MONITORED_CONDITIONS]:
if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingSensor(hass, device, sensor_type))
@ -94,6 +97,21 @@ class RingSensor(Entity):
self._tz = str(hass.config.time_zone)
self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type)
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_RING, self._update_callback)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
"""Updates controlled via the hub."""
return False
@property
def name(self):
"""Return the name of the sensor."""
@ -145,9 +163,7 @@ class RingSensor(Entity):
def update(self):
"""Get the latest data and updates the state."""
_LOGGER.debug("Pulling data from %s sensor", self._name)
self._data.update()
_LOGGER.debug("Updating data from %s sensor", self._name)
if self._sensor_type == 'volume':
self._state = self._data.volume

View file

@ -0,0 +1,2 @@
update:
description: Updates the data we have for all your ring devices

View file

@ -54,6 +54,8 @@ class TestRingBinarySensorSetup(unittest.TestCase):
text=load_fixture('ring_ding_active.json'))
mock.get('https://api.ring.com/clients_api/doorbots/987652/health',
text=load_fixture('ring_doorboot_health_attrs.json'))
mock.get('https://api.ring.com/clients_api/chimes/999999/health',
text=load_fixture('ring_chime_health_attrs.json'))
base_ring.setup(self.hass, VALID_CONFIG)
ring.setup_platform(self.hass,

View file

@ -3,7 +3,7 @@ from copy import deepcopy
import os
import unittest
import requests_mock
from datetime import timedelta
from homeassistant import setup
import homeassistant.components.ring as ring
@ -16,6 +16,7 @@ VALID_CONFIG = {
"ring": {
"username": "foo",
"password": "bar",
"scan_interval": timedelta(10)
}
}
@ -46,6 +47,12 @@ class TestRing(unittest.TestCase):
text=load_fixture('ring_oauth.json'))
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/chimes/999999/health',
text=load_fixture('ring_chime_health_attrs.json'))
mock.get('https://api.ring.com/clients_api/doorbots/987652/health',
text=load_fixture('ring_doorboot_health_attrs.json'))
response = ring.setup(self.hass, self.config)
assert response

View file

@ -5,7 +5,7 @@ import requests_mock
import homeassistant.components.ring.sensor as ring
from homeassistant.components import ring as base_ring
from homeassistant.helpers.icon import icon_for_battery_level
from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG
from tests.common import (
get_test_config_dir, get_test_home_assistant, load_fixture)
@ -72,6 +72,9 @@ class TestRingSensorSetup(unittest.TestCase):
for device in self.DEVICES:
device.update()
if device.name == 'Front Battery':
expected_icon = icon_for_battery_level(
battery_level=int(device.state), charging=False)
assert device.icon == expected_icon
assert 80 == device.state
assert 'hp_cam_v1' == \
device.device_state_attributes['kind']
@ -109,3 +112,4 @@ class TestRingSensorSetup(unittest.TestCase):
assert device.entity_picture is None
assert ATTRIBUTION == \
device.device_state_attributes['attribution']
assert not device.should_poll

View file

@ -213,5 +213,6 @@
"stolen": false,
"subscribed": true,
"subscribed_motions": true,
"time_zone": "America/New_York"}]
"time_zone": "America/New_York"
}]
}