diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 669e91a1302..5c86fa0bd7c 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -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 diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index a12954f6c29..fa13ded209f 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -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)) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 6a641fc93e3..2c29143eac3 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -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: diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 8b36cf70ea3..4fb9ab38d30 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -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 diff --git a/homeassistant/components/ring/services.yaml b/homeassistant/components/ring/services.yaml new file mode 100644 index 00000000000..a1500e1360f --- /dev/null +++ b/homeassistant/components/ring/services.yaml @@ -0,0 +1,2 @@ +update: + description: Updates the data we have for all your ring devices \ No newline at end of file diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 84ff9672850..9ee9becf85d 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -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, diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 223f3df7077..da78f4da1df 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -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 diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 872e647aced..70385cf3636 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -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 diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index 4248bbf812d..3e9171789d9 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -213,5 +213,6 @@ "stolen": false, "subscribed": true, "subscribed_motions": true, - "time_zone": "America/New_York"}] + "time_zone": "America/New_York" + }] }