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
pull/30803/head
Paulus Schoutsen 2020-01-14 12:54:45 -08:00
parent 8e5f46d5b5
commit 4bc520c724
19 changed files with 417 additions and 418 deletions

View File

@ -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

View File

@ -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

View File

@ -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()
)

View File

@ -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

View File

@ -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(

View File

@ -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...")

View File

@ -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
}

View File

@ -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

View File

@ -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...")

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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"),
)

View File

@ -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"

View File

@ -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"),
)

View File

@ -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"

View File

@ -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"),
)

View File

@ -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",

View File

@ -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",