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:
parent
92991b53c4
commit
5e7465a261
9 changed files with 118 additions and 38 deletions
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
2
homeassistant/components/ring/services.yaml
Normal file
2
homeassistant/components/ring/services.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
update:
|
||||
description: Updates the data we have for all your ring devices
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
3
tests/fixtures/ring_devices.json
vendored
3
tests/fixtures/ring_devices.json
vendored
|
@ -213,5 +213,6 @@
|
|||
"stolen": false,
|
||||
"subscribed": true,
|
||||
"subscribed_motions": true,
|
||||
"time_zone": "America/New_York"}]
|
||||
"time_zone": "America/New_York"
|
||||
}]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue