From 4bc520c724b339864c2f37f3be805fd3e6167c6a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jan 2020 12:54:45 -0800 Subject: [PATCH] Update Ring to 0.6.0 (#30748) * Update Ring to 0.6.0 * Update sensor tests * update -> async_update * Delete temp files * Address comments * Final tweaks * Remove stale print --- CODEOWNERS | 1 + homeassistant/components/ring/__init__.py | 169 +++++++++++++----- .../components/ring/binary_sensor.py | 90 ++++++---- homeassistant/components/ring/camera.py | 70 ++++---- homeassistant/components/ring/config_flow.py | 4 +- homeassistant/components/ring/light.py | 25 +-- homeassistant/components/ring/manifest.json | 4 +- homeassistant/components/ring/sensor.py | 163 ++++++++++------- homeassistant/components/ring/switch.py | 27 +-- homeassistant/helpers/entity_platform.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/conftest.py | 10 +- tests/components/ring/test_binary_sensor.py | 95 ++-------- tests/components/ring/test_light.py | 6 +- tests/components/ring/test_sensor.py | 142 ++++----------- tests/components/ring/test_switch.py | 6 +- tests/fixtures/ring_devices.json | 8 +- tests/fixtures/ring_devices_updated.json | 8 +- 19 files changed, 417 insertions(+), 418 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b30e15d36ca..6e4ea0e8b77 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -274,6 +274,7 @@ homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen +homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt homeassistant/components/saj/* @fredericvl diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 7addc116b06..b35ff630310 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -4,15 +4,17 @@ from datetime import timedelta from functools import partial import logging from pathlib import Path +from time import time from ring_doorbell import Auth, Ring import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -22,14 +24,14 @@ ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = "ring_notification" NOTIFICATION_TITLE = "Ring Setup" -DATA_RING_DOORBELLS = "ring_doorbells" -DATA_RING_STICKUP_CAMS = "ring_stickup_cams" -DATA_RING_CHIMES = "ring_chimes" +DATA_HISTORY = "ring_history" +DATA_HEALTH_DATA_TRACKER = "ring_health_data" DATA_TRACK_INTERVAL = "ring_track_interval" DOMAIN = "ring" DEFAULT_ENTITY_NAMESPACE = "ring" SIGNAL_UPDATE_RING = "ring_update" +SIGNAL_UPDATE_HEALTH_RING = "ring_health_update" SCAN_INTERVAL = timedelta(seconds=10) @@ -88,51 +90,42 @@ async def async_setup_entry(hass, entry): ), ).result() - auth = Auth(entry.data["token"], token_updater) + auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater) ring = Ring(auth) - await hass.async_add_executor_job(finish_setup_entry, hass, ring) + await hass.async_add_executor_job(ring.update_data) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ring for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) - return True + if hass.services.has_service(DOMAIN, "update"): + return True - -def finish_setup_entry(hass, ring): - """Finish setting up entry.""" - devices = ring.devices - hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"] - hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"] - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"] - - ring_devices = chimes + doorbells + stickup_cams - - 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) + async def refresh_all(_): + """Refresh all ring accounts.""" + await asyncio.gather( + *[ + hass.async_add_executor_job(api.update_data) + for api in hass.data[DOMAIN].values() + ] + ) + async_dispatcher_send(hass, SIGNAL_UPDATE_RING) # register service - hass.services.register(DOMAIN, "update", service_hub_refresh) + hass.services.async_register(DOMAIN, "update", refresh_all) # register scan interval for ring - hass.data[DATA_TRACK_INTERVAL] = track_time_interval( - hass, timer_hub_refresh, SCAN_INTERVAL + hass.data[DATA_TRACK_INTERVAL] = async_track_time_interval( + hass, refresh_all, SCAN_INTERVAL ) + hass.data[DATA_HEALTH_DATA_TRACKER] = HealthDataUpdater(hass) + hass.data[DATA_HISTORY] = HistoryCache(hass) + + return True async def async_unload_entry(hass, entry): @@ -148,13 +141,103 @@ async def async_unload_entry(hass, entry): if not unload_ok: return False - await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL]) + hass.data[DOMAIN].pop(entry.entry_id) + if len(hass.data[DOMAIN]) != 0: + return True + + # Last entry unloaded, clean up + hass.data.pop(DATA_TRACK_INTERVAL)() + hass.data.pop(DATA_HEALTH_DATA_TRACKER) + hass.data.pop(DATA_HISTORY) hass.services.async_remove(DOMAIN, "update") - hass.data.pop(DATA_RING_DOORBELLS) - hass.data.pop(DATA_RING_STICKUP_CAMS) - hass.data.pop(DATA_RING_CHIMES) - hass.data.pop(DATA_TRACK_INTERVAL) + return True - return unload_ok + +class HealthDataUpdater: + """Data storage for health data.""" + + def __init__(self, hass): + """Track devices that need health data updated.""" + self.hass = hass + self.devices = {} + self._unsub_interval = None + + async def track_device(self, config_entry_id, device): + """Track a device.""" + if not self.devices: + self._unsub_interval = async_track_time_interval( + self.hass, self.refresh_all, SCAN_INTERVAL + ) + + key = (config_entry_id, device.device_id) + + if key not in self.devices: + self.devices[key] = { + "device": device, + "count": 1, + } + else: + self.devices[key]["count"] += 1 + + await self.hass.async_add_executor_job(device.update_health_data) + + @callback + def untrack_device(self, config_entry_id, device): + """Untrack a device.""" + key = (config_entry_id, device.device_id) + self.devices[key]["count"] -= 1 + + if self.devices[key]["count"] == 0: + self.devices.pop(key) + + if not self.devices: + self._unsub_interval() + self._unsub_interval = None + + def refresh_all(self, _): + """Refresh all registered devices.""" + for info in self.devices.values(): + info["device"].update_health_data() + + dispatcher_send(self.hass, SIGNAL_UPDATE_HEALTH_RING) + + +class HistoryCache: + """Helper to fetch history.""" + + STALE_AFTER = 10 # seconds + + def __init__(self, hass): + """Initialize history cache.""" + self.hass = hass + self.cache = {} + + async def async_get_history(self, config_entry_id, device): + """Get history of a device.""" + key = (config_entry_id, device.device_id) + + if key in self.cache: + info = self.cache[key] + + # We're already fetching data, join that task + if "task" in info: + return await info["task"] + + # We have valid cache info, return that + if time() - info["created_at"] < self.STALE_AFTER: + return info["data"] + + self.cache.pop(key) + + # Fetch data + task = self.hass.async_add_executor_job(partial(device.history, limit=10)) + + self.cache[key] = {"task": task} + + data = await task + + self.cache[key] = {"created_at": time(), "data": data} + + return data diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 29337f29689..2dd3682951f 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -4,8 +4,10 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN +from . import ATTRIBUTION, DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -13,26 +15,25 @@ SCAN_INTERVAL = timedelta(seconds=10) # Sensor types: Name, category, device_class SENSOR_TYPES = { - "ding": ["Ding", ["doorbell"], "occupancy"], - "motion": ["Motion", ["doorbell", "stickup_cams"], "motion"], + "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"], + "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"], } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Ring binary sensors from a config entry.""" - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() sensors = [] - for device in ring_doorbells: # ring.doorbells is doing I/O - for sensor_type in SENSOR_TYPES: - 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_type in ("doorbots", "authorized_doorbots", "stickup_cams"): for sensor_type in SENSOR_TYPES: - if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, device, sensor_type)) + if device_type not in SENSOR_TYPES[sensor_type][1]: + continue + + for device in devices[device_type]: + sensors.append(RingBinarySensor(ring, device, sensor_type)) async_add_entities(sensors, True) @@ -40,17 +41,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingBinarySensor(BinarySensorDevice): """A binary sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + def __init__(self, ring, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() self._sensor_type = sensor_type - self._data = data + self._ring = ring + self._device = device self._name = "{0} {1}".format( - self._data.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] ) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None - self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._unique_id = f"{self._device.id}-{self._sensor_type}" + self._disp_disconnect = None + + async def async_added_to_hass(self): + """Register callbacks.""" + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + _LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name) + + @property + def should_poll(self): + """Return False, updates are controlled via the hub.""" + return False @property def name(self): @@ -76,10 +101,9 @@ class RingBinarySensor(BinarySensorDevice): def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._data.id)}, - "sw_version": self._data.firmware, - "name": self._data.name, - "model": self._data.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -89,22 +113,16 @@ class RingBinarySensor(BinarySensorDevice): attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - 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") + if self._device.alert and self._device.alert_expires_at: + attrs["expires_at"] = self._device.alert_expires_at + attrs["state"] = self._device.alert.get("state") return attrs - def update(self): + async def async_update(self): """Get the latest data and updates the state.""" - self._data.check_alerts() - - if self._data.alert: - if self._sensor_type == self._data.alert.get( - "kind" - ) and self._data.account_id == self._data.alert.get("doorbot_id"): - self._state = True - else: - self._state = False + self._state = any( + alert["kind"] == self._sensor_type + and alert["doorbot_id"] == self._device.id + for alert in self._ring.active_alerts() + ) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 2b0fe14a1d4..8ef876e4a00 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,6 +1,7 @@ """This component provides support to the Ring Door Bell camera.""" import asyncio from datetime import timedelta +from itertools import chain import logging from haffmpeg.camera import CameraMjpeg @@ -14,13 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util -from . import ( - ATTRIBUTION, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - DOMAIN, - SIGNAL_UPDATE_RING, -) +from . import ATTRIBUTION, DATA_HISTORY, DOMAIN, SIGNAL_UPDATE_RING FORCE_REFRESH_INTERVAL = timedelta(minutes=45) @@ -29,16 +24,17 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" - ring_doorbell = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() cams = [] - for camera in ring_doorbell + ring_stickup_cams: + for camera in chain( + devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] + ): if not camera.has_subscription: continue - camera = await hass.async_add_executor_job(RingCam, hass, camera) - cams.append(camera) + cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) async_add_entities(cams, True) @@ -46,17 +42,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingCam(Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, hass, camera): + def __init__(self, config_entry_id, ffmpeg, device): """Initialize a Ring Door Bell camera.""" super().__init__() - self._camera = camera - self._hass = hass - self._name = self._camera.name - self._ffmpeg = hass.data[DATA_FFMPEG] - self._last_video_id = self._camera.last_recording_id - self._video_url = self._camera.recording_url(self._last_video_id) + self._config_entry_id = config_entry_id + self._device = device + self._name = self._device.name + self._ffmpeg = ffmpeg + self._last_video_id = None + self._video_url = None self._utcnow = dt_util.utcnow() - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL self._disp_disconnect = None async def async_added_to_hass(self): @@ -85,16 +81,15 @@ class RingCam(Camera): @property def unique_id(self): """Return a unique ID.""" - return self._camera.id + return self._device.id @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._camera.id)}, - "sw_version": self._camera.firmware, - "name": self._camera.name, - "model": self._camera.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -103,7 +98,6 @@ class RingCam(Camera): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "timezone": self._camera.timezone, "video_url": self._video_url, "last_video_id": self._last_video_id, } @@ -123,7 +117,6 @@ class RingCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - if self._video_url is None: return @@ -141,22 +134,20 @@ class RingCam(Camera): finally: await stream.close() - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False - - def update(self): + async def async_update(self): """Update camera entity and refresh attributes.""" _LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url") self._utcnow = dt_util.utcnow() - try: - last_event = self._camera.history(limit=1)[0] - except (IndexError, TypeError): + data = await self.hass.data[DATA_HISTORY].async_get_history( + self._config_entry_id, self._device + ) + + if not data: return + last_event = data[0] last_recording_id = last_event["id"] video_status = last_event["recording"]["status"] @@ -164,9 +155,12 @@ class RingCam(Camera): self._last_video_id != last_recording_id or self._utcnow >= self._expires_at ): - video_url = self._camera.recording_url(last_recording_id) + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, last_recording_id + ) + if video_url: - _LOGGER.info("Ring DoorBell properties refreshed") + _LOGGER.debug("Ring DoorBell properties refreshed") # update attributes if new video or if URL has expired self._last_video_id = last_recording_id diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 6d177a4db49..57f873bd1a6 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -5,7 +5,7 @@ from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, const, core, exceptions from . import DOMAIN # pylint: disable=unused-import @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - auth = Auth() + auth = Auth(f"HomeAssistant/{const.__version__}") try: token = await hass.async_add_executor_job( diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index b7fa67a391f..10572e2e0ae 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -25,10 +25,12 @@ OFF_STATE = "off" async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + + devices = ring.devices() lights = [] - for device in cameras: + for device in devices["stickup_cams"]: if device.has_capability("light"): lights.append(RingLight(device)) @@ -64,6 +66,11 @@ class RingLight(Light): _LOGGER.debug("Updating Ring light %s (callback)", self.name) self.async_schedule_update_ha_state(True) + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + @property def name(self): """Name of the light.""" @@ -74,11 +81,6 @@ class RingLight(Light): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - @property def is_on(self): """If the switch is currently on or off.""" @@ -88,10 +90,9 @@ class RingLight(Light): def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._device.id)}, - "sw_version": self._device.firmware, + "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.name, - "model": self._device.kind, + "model": self._device.model, "manufacturer": "Ring", } @@ -110,7 +111,7 @@ class RingLight(Light): """Turn the light off.""" self._set_light(OFF_STATE) - def update(self): + async def async_update(self): """Update current state of the light.""" if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index fccbf9a5319..d46f12af511 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,8 +2,8 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.5.0"], + "requirements": ["ring_doorbell==0.6.0"], "dependencies": ["ffmpeg"], - "codeowners": [], + "codeowners": ["@balloob"], "config_flow": true } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 874c056ec7d..fe909636e83 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -9,93 +9,98 @@ from homeassistant.helpers.icon import icon_for_battery_level from . import ( ATTRIBUTION, - DATA_RING_CHIMES, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, + DATA_HEALTH_DATA_TRACKER, + DATA_HISTORY, DOMAIN, + SIGNAL_UPDATE_HEALTH_RING, SIGNAL_UPDATE_RING, ) _LOGGER = logging.getLogger(__name__) -# Sensor types: Name, category, units, icon, kind +# Sensor types: Name, category, units, icon, kind, device_class SENSOR_TYPES = { - "battery": ["Battery", ["doorbell", "stickup_cams"], "%", "battery-50", None], + "battery": [ + "Battery", + ["doorbots", "authorized_doorbots", "stickup_cams"], + "%", + None, + None, + "battery", + ], "last_activity": [ "Last Activity", - ["doorbell", "stickup_cams"], + ["doorbots", "authorized_doorbots", "stickup_cams"], None, "history", None, + "timestamp", + ], + "last_ding": [ + "Last Ding", + ["doorbots", "authorized_doorbots"], + None, + "history", + "ding", + "timestamp", ], - "last_ding": ["Last Ding", ["doorbell"], None, "history", "ding"], "last_motion": [ "Last Motion", - ["doorbell", "stickup_cams"], + ["doorbots", "authorized_doorbots", "stickup_cams"], None, "history", "motion", + "timestamp", ], "volume": [ "Volume", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], None, "bell-ring", None, + None, ], "wifi_signal_category": [ "WiFi Signal Category", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], None, "wifi", None, + None, ], "wifi_signal_strength": [ "WiFi Signal Strength", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], "dBm", "wifi", None, + "signal_strength", ], } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" - ring_chimes = hass.data[DATA_RING_CHIMES] - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() + # Makes a ton of requests. We will make this a config entry option in the future + wifi_enabled = False sensors = [] - for device in ring_chimes: + + for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"): for sensor_type in SENSOR_TYPES: - if "chime" not in SENSOR_TYPES[sensor_type][1]: + if device_type not in SENSOR_TYPES[sensor_type][1]: continue - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) - - sensors.append(RingSensor(hass, device, sensor_type)) - - for device in ring_doorbells: - for sensor_type in SENSOR_TYPES: - if "doorbell" not in SENSOR_TYPES[sensor_type][1]: + if not wifi_enabled and sensor_type.startswith("wifi_"): continue - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) + for device in devices[device_type]: + if device_type == "battery" and device.battery_life is None: + continue - sensors.append(RingSensor(hass, device, sensor_type)) - - for device in ring_stickup_cams: - for sensor_type in SENSOR_TYPES: - if "stickup_cams" not in SENSOR_TYPES[sensor_type][1]: - continue - - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) - - sensors.append(RingSensor(hass, device, sensor_type)) + sensors.append(RingSensor(config_entry.entry_id, device, sensor_type)) async_add_entities(sensors, True) @@ -103,28 +108,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingSensor(Entity): """A sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + def __init__(self, config_entry_id, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() + self._config_entry_id = config_entry_id self._sensor_type = sensor_type - self._data = data + self._device = device 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._device.name, SENSOR_TYPES.get(self._sensor_type)[0] ) self._state = None - self._tz = str(hass.config.time_zone) - self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._unique_id = f"{self._device.id}-{self._sensor_type}" self._disp_disconnect = None + self._disp_disconnect_health = None async def async_added_to_hass(self): """Register callbacks.""" self._disp_disconnect = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_RING, self._update_callback ) - await self.hass.async_add_executor_job(self._data.update) + if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): + return + + self._disp_disconnect_health = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HEALTH_RING, self._update_callback + ) + await self.hass.data[DATA_HEALTH_DATA_TRACKER].track_device( + self._config_entry_id, self._device + ) + # Write the state, it was not available when doing initial update. + if self._sensor_type == "wifi_signal_category": + self._state = self._device.wifi_signal_category + + if self._sensor_type == "wifi_signal_strength": + self._state = self._device.wifi_signal_strength async def async_will_remove_from_hass(self): """Disconnect callbacks.""" @@ -132,6 +151,17 @@ class RingSensor(Entity): self._disp_disconnect() self._disp_disconnect = None + if self._disp_disconnect_health: + self._disp_disconnect_health() + self._disp_disconnect_health = None + + if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): + return + + self.hass.data[DATA_HEALTH_DATA_TRACKER].untrack_device( + self._config_entry_id, self._device + ) + @callback def _update_callback(self): """Call update method.""" @@ -157,14 +187,18 @@ class RingSensor(Entity): """Return a unique ID.""" return self._unique_id + @property + def device_class(self): + """Return sensor device class.""" + return SENSOR_TYPES[self._sensor_type][5] + @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._data.id)}, - "sw_version": self._data.firmware, - "name": self._data.name, - "model": self._data.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -174,8 +208,6 @@ class RingSensor(Entity): attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["timezone"] = self._data.timezone - attrs["wifi_name"] = self._data.wifi_name if self._extra and self._sensor_type.startswith("last_"): attrs["created_at"] = self._extra["created_at"] @@ -199,29 +231,34 @@ class RingSensor(Entity): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] - def update(self): + async def async_update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating data from %s sensor", self._name) if self._sensor_type == "volume": - self._state = self._data.volume + self._state = self._device.volume if self._sensor_type == "battery": - self._state = self._data.battery_life + self._state = self._device.battery_life if self._sensor_type.startswith("last_"): - history = self._data.history( - limit=5, timezone=self._tz, kind=self._kind, enforce_limit=True + history = await self.hass.data[DATA_HISTORY].async_get_history( + self._config_entry_id, self._device ) - 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 - ) + + found = None + for entry in history: + if entry["kind"] == self._kind: + found = entry + break + + if found: + self._extra = found + created_at = found["created_at"] + self._state = created_at.isoformat() if self._sensor_type == "wifi_signal_category": - self._state = self._data.wifi_signal_category + self._state = self._device.wifi_signal_category if self._sensor_type == "wifi_signal_strength": - self._state = self._data.wifi_signal_strength + self._state = self._device.wifi_signal_strength diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index e23e757d825..06f81732784 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -24,9 +24,11 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5) async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() switches = [] - for device in cameras: + + for device in devices["stickup_cams"]: if device.has_capability("siren"): switches.append(SirenSwitch(device)) @@ -58,9 +60,14 @@ class BaseRingSwitch(SwitchDevice): @callback def _update_callback(self): """Call update method.""" - _LOGGER.debug("Updating Ring sensor %s (callback)", self.name) + _LOGGER.debug("Updating Ring switch %s (callback)", self.name) self.async_schedule_update_ha_state(True) + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + @property def name(self): """Name of the device.""" @@ -71,19 +78,13 @@ class BaseRingSwitch(SwitchDevice): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._device.id)}, - "sw_version": self._device.firmware, + "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.name, - "model": self._device.kind, + "model": self._device.model, "manufacturer": "Ring", } @@ -122,7 +123,7 @@ class SirenSwitch(BaseRingSwitch): """Return the icon.""" return SIREN_ICON - def update(self): + async def async_update(self): """Update state of the siren.""" if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0560cf84fb3..53ad54c5ed1 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -146,7 +146,8 @@ class EntityPlatform: warn_task = hass.loop.call_later( SLOW_SETUP_WARNING, logger.warning, - "Setup of platform %s is taking over %s seconds.", + "Setup of %s platform %s is taking over %s seconds.", + self.domain, self.platform_name, SLOW_SETUP_WARNING, ) diff --git a/requirements_all.txt b/requirements_all.txt index db306c6f5ea..9090aef073d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ rfk101py==0.0.1 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.5.0 +ring_doorbell==0.6.0 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75e647fb3d2..82c740afa93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ restrictedpython==5.0 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.5.0 +ring_doorbell==0.6.0 # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index a4cfaf0065d..5df85662ac8 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,4 +1,6 @@ """Configuration for Ring tests.""" +import re + import pytest import requests_mock @@ -33,17 +35,19 @@ def requests_mock_fixture(): ) # Mocks the response for getting the history of a device mock.get( - "https://api.ring.com/clients_api/doorbots/987652/history", + re.compile( + r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history" + ), text=load_fixture("ring_doorbots.json"), ) # Mocks the response for getting the health of a device mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", + re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"), text=load_fixture("ring_doorboot_health_attrs.json"), ) # Mocks the response for getting a chimes health mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", + re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"), text=load_fixture("ring_chime_health_attrs.json"), ) diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 4ca83b2451b..8615138d56e 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,87 +1,22 @@ """The tests for the Ring binary sensor platform.""" -from asyncio import run_coroutine_threadsafe -import unittest from unittest.mock import patch -import requests_mock - -from homeassistant.components import ring as base_ring -from homeassistant.components.ring import binary_sensor as ring - -from tests.common import get_test_home_assistant, load_fixture, mock_storage -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG +from .common import setup_platform -class TestRingBinarySensorSetup(unittest.TestCase): - """Test the Ring Binary Sensor platform.""" +async def test_binary_sensor(hass, requests_mock): + """Test the Ring binary sensors.""" + with patch( + "ring_doorbell.Ring.active_alerts", + return_value=[{"kind": "motion", "doorbot_id": 987654}], + ): + await setup_platform(hass, "binary_sensor") - DEVICES = [] + motion_state = hass.states.get("binary_sensor.front_door_motion") + assert motion_state is not None + assert motion_state.state == "on" + assert motion_state.attributes["device_class"] == "motion" - def add_entities(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://oauth.ring.com/oauth/token", 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/dings/active", - 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"), - ) - - with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): - run_coroutine_threadsafe( - base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop - ).result() - run_coroutine_threadsafe( - self.hass.async_block_till_done(), self.hass.loop - ).result() - run_coroutine_threadsafe( - ring.async_setup_entry(self.hass, None, self.add_entities), - self.hass.loop, - ).result() - - for device in self.DEVICES: - device.update() - if device.name == "Front Door Ding": - assert "on" == device.state - assert "America/New_York" == device.device_state_attributes["timezone"] - elif device.name == "Front Door Motion": - assert "off" == device.state - assert "motion" == device.device_class - - assert device.entity_picture is None - assert ATTRIBUTION == device.device_state_attributes["attribution"] + ding_state = hass.states.get("binary_sensor.front_door_ding") + assert ding_state is not None + assert ding_state.state == "off" diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 56d39173d63..6cc727b1a1c 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock): entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get("light.front_light") - assert entry.unique_id == "aacdef123" + assert entry.unique_id == 765432 entry = entity_registry.async_get("light.internal_light") - assert entry.unique_id == "aacdef124" + assert entry.unique_id == 345678 async def test_light_off_reports_correctly(hass, requests_mock): @@ -42,7 +42,7 @@ async def test_light_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a light on requests_mock.put( - "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", + "https://api.ring.com/clients_api/doorbots/765432/floodlight_light_on", text=load_fixture("ring_doorbot_siren_on_response.json"), ) diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 039c9d0625f..f86e6b25959 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,122 +1,46 @@ """The tests for the Ring sensor platform.""" -from asyncio import run_coroutine_threadsafe -import unittest -from unittest.mock import patch +from .common import setup_platform -import requests_mock - -from homeassistant.components import ring as base_ring -import homeassistant.components.ring.sensor as ring -from homeassistant.helpers.icon import icon_for_battery_level - -from tests.common import get_test_home_assistant, load_fixture, mock_storage -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG +WIFI_ENABLED = False -class TestRingSensorSetup(unittest.TestCase): - """Test the Ring platform.""" +async def test_sensor(hass, requests_mock): + """Test the Ring sensors.""" + await setup_platform(hass, "sensor") - DEVICES = [] + front_battery_state = hass.states.get("sensor.front_battery") + assert front_battery_state is not None + assert front_battery_state.state == "80" - def add_entities(self, devices, action): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) + front_door_battery_state = hass.states.get("sensor.front_door_battery") + assert front_door_battery_state is not None + assert front_door_battery_state.state == "100" - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.config = { - "username": "foo", - "password": "bar", - "monitored_conditions": [ - "battery", - "last_activity", - "last_ding", - "last_motion", - "volume", - "wifi_signal_category", - "wifi_signal_strength", - ], - } + downstairs_volume_state = hass.states.get("sensor.downstairs_volume") + assert downstairs_volume_state is not None + assert downstairs_volume_state.state == "2" - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") + assert front_door_last_activity_state is not None - @requests_mock.Mocker() - def test_sensor(self, mock): - """Test the Ring sensor class and methods.""" - mock.post( - "https://oauth.ring.com/oauth/token", 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/doorbots/987652/history", - text=load_fixture("ring_doorbots.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"), - ) + downstairs_wifi_signal_strength_state = hass.states.get( + "sensor.downstairs_wifi_signal_strength" + ) - with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): - run_coroutine_threadsafe( - base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop - ).result() - run_coroutine_threadsafe( - self.hass.async_block_till_done(), self.hass.loop - ).result() - run_coroutine_threadsafe( - ring.async_setup_entry(self.hass, None, self.add_entities), - self.hass.loop, - ).result() + if not WIFI_ENABLED: + return - for device in self.DEVICES: - # Mimick add to hass - device.hass = self.hass - run_coroutine_threadsafe( - device.async_added_to_hass(), self.hass.loop, - ).result() + assert downstairs_wifi_signal_strength_state is not None + assert downstairs_wifi_signal_strength_state.state == "-39" - # Entity update data from ring data - 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 - if device.name == "Front Door Battery": - assert 100 == device.state - if device.name == "Downstairs Volume": - assert 2 == device.state - assert "ring_mock_wifi" == device.device_state_attributes["wifi_name"] - assert "mdi:bell-ring" == device.icon - if device.name == "Front Door Last Activity": - assert not device.device_state_attributes["answered"] - assert "America/New_York" == device.device_state_attributes["timezone"] + front_door_wifi_signal_category_state = hass.states.get( + "sensor.front_door_wifi_signal_category" + ) + assert front_door_wifi_signal_category_state is not None + assert front_door_wifi_signal_category_state.state == "good" - if device.name == "Downstairs WiFi Signal Strength": - assert -39 == device.state - - if device.name == "Front Door WiFi Signal Category": - assert "good" == device.state - - if device.name == "Front Door WiFi Signal Strength": - assert -58 == device.state - - assert device.entity_picture is None - assert ATTRIBUTION == device.device_state_attributes["attribution"] - assert not device.should_poll + front_door_wifi_signal_strength_state = hass.states.get( + "sensor.front_door_wifi_signal_strength" + ) + assert front_door_wifi_signal_strength_state is not None + assert front_door_wifi_signal_strength_state.state == "-58" diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 15f4dd86a39..e2a86014f1c 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock): entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get("switch.front_siren") - assert entry.unique_id == "aacdef123-siren" + assert entry.unique_id == "765432-siren" entry = entity_registry.async_get("switch.internal_siren") - assert entry.unique_id == "aacdef124-siren" + assert entry.unique_id == "345678-siren" async def test_siren_off_reports_correctly(hass, requests_mock): @@ -43,7 +43,7 @@ async def test_siren_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a siren on requests_mock.put( - "https://api.ring.com/clients_api/doorbots/987652/siren_on", + "https://api.ring.com/clients_api/doorbots/765432/siren_on", text=load_fixture("ring_doorbot_siren_on_response.json"), ) diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index 557aef3535c..2d2ec893a74 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -9,7 +9,7 @@ "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", - "id": 999999, + "id": 123456, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, @@ -42,7 +42,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", - "id": 987652, + "id": 987654, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, @@ -93,7 +93,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 765432, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", @@ -231,7 +231,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 345678, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "on", diff --git a/tests/fixtures/ring_devices_updated.json b/tests/fixtures/ring_devices_updated.json index fa3c0586101..3668a2b13dc 100644 --- a/tests/fixtures/ring_devices_updated.json +++ b/tests/fixtures/ring_devices_updated.json @@ -9,7 +9,7 @@ "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", - "id": 999999, + "id": 123456, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, @@ -42,7 +42,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", - "id": 987652, + "id": 987654, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, @@ -93,7 +93,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 765432, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "on", @@ -231,7 +231,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 345678, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off",