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/random/* @fabaff
homeassistant/components/repetier/* @MTrab homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/ring/* @balloob
homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt homeassistant/components/roomba/* @pschmitt
homeassistant/components/saj/* @fredericvl homeassistant/components/saj/* @fredericvl

View File

@ -4,15 +4,17 @@ from datetime import timedelta
from functools import partial from functools import partial
import logging import logging
from pathlib import Path from pathlib import Path
from time import time
from ring_doorbell import Auth, Ring from ring_doorbell import Auth, Ring
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -22,14 +24,14 @@ ATTRIBUTION = "Data provided by Ring.com"
NOTIFICATION_ID = "ring_notification" NOTIFICATION_ID = "ring_notification"
NOTIFICATION_TITLE = "Ring Setup" NOTIFICATION_TITLE = "Ring Setup"
DATA_RING_DOORBELLS = "ring_doorbells" DATA_HISTORY = "ring_history"
DATA_RING_STICKUP_CAMS = "ring_stickup_cams" DATA_HEALTH_DATA_TRACKER = "ring_health_data"
DATA_RING_CHIMES = "ring_chimes"
DATA_TRACK_INTERVAL = "ring_track_interval" DATA_TRACK_INTERVAL = "ring_track_interval"
DOMAIN = "ring" DOMAIN = "ring"
DEFAULT_ENTITY_NAMESPACE = "ring" DEFAULT_ENTITY_NAMESPACE = "ring"
SIGNAL_UPDATE_RING = "ring_update" SIGNAL_UPDATE_RING = "ring_update"
SIGNAL_UPDATE_HEALTH_RING = "ring_health_update"
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
@ -88,51 +90,42 @@ async def async_setup_entry(hass, entry):
), ),
).result() ).result()
auth = Auth(entry.data["token"], token_updater) auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater)
ring = Ring(auth) 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: for component in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component) hass.config_entries.async_forward_entry_setup(entry, component)
) )
return True if hass.services.has_service(DOMAIN, "update"):
return True
async def refresh_all(_):
def finish_setup_entry(hass, ring): """Refresh all ring accounts."""
"""Finish setting up entry.""" await asyncio.gather(
devices = ring.devices *[
hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"] hass.async_add_executor_job(api.update_data)
hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"] for api in hass.data[DOMAIN].values()
hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"] ]
)
ring_devices = chimes + doorbells + stickup_cams async_dispatcher_send(hass, SIGNAL_UPDATE_RING)
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 # register service
hass.services.register(DOMAIN, "update", service_hub_refresh) hass.services.async_register(DOMAIN, "update", refresh_all)
# register scan interval for ring # register scan interval for ring
hass.data[DATA_TRACK_INTERVAL] = track_time_interval( hass.data[DATA_TRACK_INTERVAL] = async_track_time_interval(
hass, timer_hub_refresh, SCAN_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): async def async_unload_entry(hass, entry):
@ -148,13 +141,103 @@ async def async_unload_entry(hass, entry):
if not unload_ok: if not unload_ok:
return False 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.services.async_remove(DOMAIN, "update")
hass.data.pop(DATA_RING_DOORBELLS) return True
hass.data.pop(DATA_RING_STICKUP_CAMS)
hass.data.pop(DATA_RING_CHIMES)
hass.data.pop(DATA_TRACK_INTERVAL)
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.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_ATTRIBUTION 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__) _LOGGER = logging.getLogger(__name__)
@ -13,26 +15,25 @@ SCAN_INTERVAL = timedelta(seconds=10)
# Sensor types: Name, category, device_class # Sensor types: Name, category, device_class
SENSOR_TYPES = { SENSOR_TYPES = {
"ding": ["Ding", ["doorbell"], "occupancy"], "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"],
"motion": ["Motion", ["doorbell", "stickup_cams"], "motion"], "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"],
} }
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Ring binary sensors from a config entry.""" """Set up the Ring binary sensors from a config entry."""
ring_doorbells = hass.data[DATA_RING_DOORBELLS] ring = hass.data[DOMAIN][config_entry.entry_id]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] devices = ring.devices()
sensors = [] 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: for sensor_type in SENSOR_TYPES:
if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: if device_type not in SENSOR_TYPES[sensor_type][1]:
sensors.append(RingBinarySensor(hass, device, sensor_type)) continue
for device in devices[device_type]:
sensors.append(RingBinarySensor(ring, device, sensor_type))
async_add_entities(sensors, True) async_add_entities(sensors, True)
@ -40,17 +41,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class RingBinarySensor(BinarySensorDevice): class RingBinarySensor(BinarySensorDevice):
"""A binary sensor implementation for Ring device.""" """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.""" """Initialize a sensor for Ring device."""
super().__init__()
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._data = data self._ring = ring
self._device = device
self._name = "{0} {1}".format( 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._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
self._state = None 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 @property
def name(self): def name(self):
@ -76,10 +101,9 @@ class RingBinarySensor(BinarySensorDevice):
def device_info(self): def device_info(self):
"""Return device info.""" """Return device info."""
return { return {
"identifiers": {(DOMAIN, self._data.id)}, "identifiers": {(DOMAIN, self._device.device_id)},
"sw_version": self._data.firmware, "name": self._device.name,
"name": self._data.name, "model": self._device.model,
"model": self._data.kind,
"manufacturer": "Ring", "manufacturer": "Ring",
} }
@ -89,22 +113,16 @@ class RingBinarySensor(BinarySensorDevice):
attrs = {} attrs = {}
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
attrs["timezone"] = self._data.timezone if self._device.alert and self._device.alert_expires_at:
attrs["expires_at"] = self._device.alert_expires_at
if self._data.alert and self._data.alert_expires_at: attrs["state"] = self._device.alert.get("state")
attrs["expires_at"] = self._data.alert_expires_at
attrs["state"] = self._data.alert.get("state")
return attrs return attrs
def update(self): async def async_update(self):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""
self._data.check_alerts() self._state = any(
alert["kind"] == self._sensor_type
if self._data.alert: and alert["doorbot_id"] == self._device.id
if self._sensor_type == self._data.alert.get( for alert in self._ring.active_alerts()
"kind" )
) and self._data.account_id == self._data.alert.get("doorbot_id"):
self._state = True
else:
self._state = False

View File

@ -1,6 +1,7 @@
"""This component provides support to the Ring Door Bell camera.""" """This component provides support to the Ring Door Bell camera."""
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from itertools import chain
import logging import logging
from haffmpeg.camera import CameraMjpeg 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.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import ( from . import ATTRIBUTION, DATA_HISTORY, DOMAIN, SIGNAL_UPDATE_RING
ATTRIBUTION,
DATA_RING_DOORBELLS,
DATA_RING_STICKUP_CAMS,
DOMAIN,
SIGNAL_UPDATE_RING,
)
FORCE_REFRESH_INTERVAL = timedelta(minutes=45) 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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Ring Door Bell and StickUp Camera.""" """Set up a Ring Door Bell and StickUp Camera."""
ring_doorbell = hass.data[DATA_RING_DOORBELLS] ring = hass.data[DOMAIN][config_entry.entry_id]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] devices = ring.devices()
cams = [] 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: if not camera.has_subscription:
continue continue
camera = await hass.async_add_executor_job(RingCam, hass, camera) cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera))
cams.append(camera)
async_add_entities(cams, True) async_add_entities(cams, True)
@ -46,17 +42,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class RingCam(Camera): class RingCam(Camera):
"""An implementation of a Ring Door Bell 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.""" """Initialize a Ring Door Bell camera."""
super().__init__() super().__init__()
self._camera = camera self._config_entry_id = config_entry_id
self._hass = hass self._device = device
self._name = self._camera.name self._name = self._device.name
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = ffmpeg
self._last_video_id = self._camera.last_recording_id self._last_video_id = None
self._video_url = self._camera.recording_url(self._last_video_id) self._video_url = None
self._utcnow = dt_util.utcnow() 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 self._disp_disconnect = None
async def async_added_to_hass(self): async def async_added_to_hass(self):
@ -85,16 +81,15 @@ class RingCam(Camera):
@property @property
def unique_id(self): def unique_id(self):
"""Return a unique ID.""" """Return a unique ID."""
return self._camera.id return self._device.id
@property @property
def device_info(self): def device_info(self):
"""Return device info.""" """Return device info."""
return { return {
"identifiers": {(DOMAIN, self._camera.id)}, "identifiers": {(DOMAIN, self._device.device_id)},
"sw_version": self._camera.firmware, "name": self._device.name,
"name": self._camera.name, "model": self._device.model,
"model": self._camera.kind,
"manufacturer": "Ring", "manufacturer": "Ring",
} }
@ -103,7 +98,6 @@ class RingCam(Camera):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,
"timezone": self._camera.timezone,
"video_url": self._video_url, "video_url": self._video_url,
"last_video_id": self._last_video_id, "last_video_id": self._last_video_id,
} }
@ -123,7 +117,6 @@ class RingCam(Camera):
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
if self._video_url is None: if self._video_url is None:
return return
@ -141,22 +134,20 @@ class RingCam(Camera):
finally: finally:
await stream.close() await stream.close()
@property async def async_update(self):
def should_poll(self):
"""Return False, updates are controlled via the hub."""
return False
def update(self):
"""Update camera entity and refresh attributes.""" """Update camera entity and refresh attributes."""
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url") _LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
self._utcnow = dt_util.utcnow() self._utcnow = dt_util.utcnow()
try: data = await self.hass.data[DATA_HISTORY].async_get_history(
last_event = self._camera.history(limit=1)[0] self._config_entry_id, self._device
except (IndexError, TypeError): )
if not data:
return return
last_event = data[0]
last_recording_id = last_event["id"] last_recording_id = last_event["id"]
video_status = last_event["recording"]["status"] 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 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: 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 # update attributes if new video or if URL has expired
self._last_video_id = last_recording_id self._last_video_id = last_recording_id

View File

@ -5,7 +5,7 @@ from oauthlib.oauth2 import AccessDeniedError, MissingTokenError
from ring_doorbell import Auth from ring_doorbell import Auth
import voluptuous as vol 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 from . import DOMAIN # pylint: disable=unused-import
@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
async def validate_input(hass: core.HomeAssistant, data): async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.""" """Validate the user input allows us to connect."""
auth = Auth() auth = Auth(f"HomeAssistant/{const.__version__}")
try: try:
token = await hass.async_add_executor_job( 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 from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.dt as dt_util 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__) _LOGGER = logging.getLogger(__name__)
@ -25,10 +25,12 @@ OFF_STATE = "off"
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Create the lights for the Ring devices.""" """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 = [] lights = []
for device in cameras: for device in devices["stickup_cams"]:
if device.has_capability("light"): if device.has_capability("light"):
lights.append(RingLight(device)) lights.append(RingLight(device))
@ -64,6 +66,11 @@ class RingLight(Light):
_LOGGER.debug("Updating Ring light %s (callback)", self.name) _LOGGER.debug("Updating Ring light %s (callback)", self.name)
self.async_schedule_update_ha_state(True) self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
"""Update controlled via the hub."""
return False
@property @property
def name(self): def name(self):
"""Name of the light.""" """Name of the light."""
@ -74,11 +81,6 @@ class RingLight(Light):
"""Return a unique ID.""" """Return a unique ID."""
return self._unique_id return self._unique_id
@property
def should_poll(self):
"""Update controlled via the hub."""
return False
@property @property
def is_on(self): def is_on(self):
"""If the switch is currently on or off.""" """If the switch is currently on or off."""
@ -88,10 +90,9 @@ class RingLight(Light):
def device_info(self): def device_info(self):
"""Return device info.""" """Return device info."""
return { return {
"identifiers": {(DOMAIN, self._device.id)}, "identifiers": {(DOMAIN, self._device.device_id)},
"sw_version": self._device.firmware,
"name": self._device.name, "name": self._device.name,
"model": self._device.kind, "model": self._device.model,
"manufacturer": "Ring", "manufacturer": "Ring",
} }
@ -110,7 +111,7 @@ class RingLight(Light):
"""Turn the light off.""" """Turn the light off."""
self._set_light(OFF_STATE) self._set_light(OFF_STATE)
def update(self): async def async_update(self):
"""Update current state of the light.""" """Update current state of the light."""
if self._no_updates_until > dt_util.utcnow(): if self._no_updates_until > dt_util.utcnow():
_LOGGER.debug("Skipping update...") _LOGGER.debug("Skipping update...")

View File

@ -2,8 +2,8 @@
"domain": "ring", "domain": "ring",
"name": "Ring", "name": "Ring",
"documentation": "https://www.home-assistant.io/integrations/ring", "documentation": "https://www.home-assistant.io/integrations/ring",
"requirements": ["ring_doorbell==0.5.0"], "requirements": ["ring_doorbell==0.6.0"],
"dependencies": ["ffmpeg"], "dependencies": ["ffmpeg"],
"codeowners": [], "codeowners": ["@balloob"],
"config_flow": true "config_flow": true
} }

View File

@ -9,93 +9,98 @@ from homeassistant.helpers.icon import icon_for_battery_level
from . import ( from . import (
ATTRIBUTION, ATTRIBUTION,
DATA_RING_CHIMES, DATA_HEALTH_DATA_TRACKER,
DATA_RING_DOORBELLS, DATA_HISTORY,
DATA_RING_STICKUP_CAMS,
DOMAIN, DOMAIN,
SIGNAL_UPDATE_HEALTH_RING,
SIGNAL_UPDATE_RING, SIGNAL_UPDATE_RING,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Sensor types: Name, category, units, icon, kind # Sensor types: Name, category, units, icon, kind, device_class
SENSOR_TYPES = { SENSOR_TYPES = {
"battery": ["Battery", ["doorbell", "stickup_cams"], "%", "battery-50", None], "battery": [
"Battery",
["doorbots", "authorized_doorbots", "stickup_cams"],
"%",
None,
None,
"battery",
],
"last_activity": [ "last_activity": [
"Last Activity", "Last Activity",
["doorbell", "stickup_cams"], ["doorbots", "authorized_doorbots", "stickup_cams"],
None, None,
"history", "history",
None, 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": [
"Last Motion", "Last Motion",
["doorbell", "stickup_cams"], ["doorbots", "authorized_doorbots", "stickup_cams"],
None, None,
"history", "history",
"motion", "motion",
"timestamp",
], ],
"volume": [ "volume": [
"Volume", "Volume",
["chime", "doorbell", "stickup_cams"], ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
None, None,
"bell-ring", "bell-ring",
None, None,
None,
], ],
"wifi_signal_category": [ "wifi_signal_category": [
"WiFi Signal Category", "WiFi Signal Category",
["chime", "doorbell", "stickup_cams"], ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
None, None,
"wifi", "wifi",
None, None,
None,
], ],
"wifi_signal_strength": [ "wifi_signal_strength": [
"WiFi Signal Strength", "WiFi Signal Strength",
["chime", "doorbell", "stickup_cams"], ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
"dBm", "dBm",
"wifi", "wifi",
None, None,
"signal_strength",
], ],
} }
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a sensor for a Ring device.""" """Set up a sensor for a Ring device."""
ring_chimes = hass.data[DATA_RING_CHIMES] ring = hass.data[DOMAIN][config_entry.entry_id]
ring_doorbells = hass.data[DATA_RING_DOORBELLS] devices = ring.devices()
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] # Makes a ton of requests. We will make this a config entry option in the future
wifi_enabled = False
sensors = [] sensors = []
for device in ring_chimes:
for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"):
for sensor_type in SENSOR_TYPES: 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 continue
if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): if not wifi_enabled and sensor_type.startswith("wifi_"):
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]:
continue continue
if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): for device in devices[device_type]:
await hass.async_add_executor_job(device.update) if device_type == "battery" and device.battery_life is None:
continue
sensors.append(RingSensor(hass, device, sensor_type)) sensors.append(RingSensor(config_entry.entry_id, 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))
async_add_entities(sensors, True) async_add_entities(sensors, True)
@ -103,28 +108,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class RingSensor(Entity): class RingSensor(Entity):
"""A sensor implementation for Ring device.""" """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.""" """Initialize a sensor for Ring device."""
super().__init__() self._config_entry_id = config_entry_id
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._data = data self._device = device
self._extra = None self._extra = None
self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3]) self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3])
self._kind = SENSOR_TYPES.get(self._sensor_type)[4] self._kind = SENSOR_TYPES.get(self._sensor_type)[4]
self._name = "{0} {1}".format( 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._state = None
self._tz = str(hass.config.time_zone) self._unique_id = f"{self._device.id}-{self._sensor_type}"
self._unique_id = f"{self._data.id}-{self._sensor_type}"
self._disp_disconnect = None self._disp_disconnect = None
self._disp_disconnect_health = None
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
self._disp_disconnect = async_dispatcher_connect( self._disp_disconnect = async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_RING, self._update_callback 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): async def async_will_remove_from_hass(self):
"""Disconnect callbacks.""" """Disconnect callbacks."""
@ -132,6 +151,17 @@ class RingSensor(Entity):
self._disp_disconnect() self._disp_disconnect()
self._disp_disconnect = None 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 @callback
def _update_callback(self): def _update_callback(self):
"""Call update method.""" """Call update method."""
@ -157,14 +187,18 @@ class RingSensor(Entity):
"""Return a unique ID.""" """Return a unique ID."""
return self._unique_id return self._unique_id
@property
def device_class(self):
"""Return sensor device class."""
return SENSOR_TYPES[self._sensor_type][5]
@property @property
def device_info(self): def device_info(self):
"""Return device info.""" """Return device info."""
return { return {
"identifiers": {(DOMAIN, self._data.id)}, "identifiers": {(DOMAIN, self._device.device_id)},
"sw_version": self._data.firmware, "name": self._device.name,
"name": self._data.name, "model": self._device.model,
"model": self._data.kind,
"manufacturer": "Ring", "manufacturer": "Ring",
} }
@ -174,8 +208,6 @@ class RingSensor(Entity):
attrs = {} attrs = {}
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION 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_"): if self._extra and self._sensor_type.startswith("last_"):
attrs["created_at"] = self._extra["created_at"] attrs["created_at"] = self._extra["created_at"]
@ -199,29 +231,34 @@ class RingSensor(Entity):
"""Return the units of measurement.""" """Return the units of measurement."""
return SENSOR_TYPES.get(self._sensor_type)[2] return SENSOR_TYPES.get(self._sensor_type)[2]
def update(self): async def async_update(self):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""
_LOGGER.debug("Updating data from %s sensor", self._name) _LOGGER.debug("Updating data from %s sensor", self._name)
if self._sensor_type == "volume": if self._sensor_type == "volume":
self._state = self._data.volume self._state = self._device.volume
if self._sensor_type == "battery": if self._sensor_type == "battery":
self._state = self._data.battery_life self._state = self._device.battery_life
if self._sensor_type.startswith("last_"): if self._sensor_type.startswith("last_"):
history = self._data.history( history = await self.hass.data[DATA_HISTORY].async_get_history(
limit=5, timezone=self._tz, kind=self._kind, enforce_limit=True self._config_entry_id, self._device
) )
if history:
self._extra = history[0] found = None
created_at = self._extra["created_at"] for entry in history:
self._state = "{0:0>2}:{1:0>2}".format( if entry["kind"] == self._kind:
created_at.hour, created_at.minute 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": 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": 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 from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.dt as dt_util 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__) _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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Create the switches for the Ring devices.""" """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 = [] switches = []
for device in cameras:
for device in devices["stickup_cams"]:
if device.has_capability("siren"): if device.has_capability("siren"):
switches.append(SirenSwitch(device)) switches.append(SirenSwitch(device))
@ -58,9 +60,14 @@ class BaseRingSwitch(SwitchDevice):
@callback @callback
def _update_callback(self): def _update_callback(self):
"""Call update method.""" """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) self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
"""Update controlled via the hub."""
return False
@property @property
def name(self): def name(self):
"""Name of the device.""" """Name of the device."""
@ -71,19 +78,13 @@ class BaseRingSwitch(SwitchDevice):
"""Return a unique ID.""" """Return a unique ID."""
return self._unique_id return self._unique_id
@property
def should_poll(self):
"""Update controlled via the hub."""
return False
@property @property
def device_info(self): def device_info(self):
"""Return device info.""" """Return device info."""
return { return {
"identifiers": {(DOMAIN, self._device.id)}, "identifiers": {(DOMAIN, self._device.device_id)},
"sw_version": self._device.firmware,
"name": self._device.name, "name": self._device.name,
"model": self._device.kind, "model": self._device.model,
"manufacturer": "Ring", "manufacturer": "Ring",
} }
@ -122,7 +123,7 @@ class SirenSwitch(BaseRingSwitch):
"""Return the icon.""" """Return the icon."""
return SIREN_ICON return SIREN_ICON
def update(self): async def async_update(self):
"""Update state of the siren.""" """Update state of the siren."""
if self._no_updates_until > dt_util.utcnow(): if self._no_updates_until > dt_util.utcnow():
_LOGGER.debug("Skipping update...") _LOGGER.debug("Skipping update...")

View File

@ -146,7 +146,8 @@ class EntityPlatform:
warn_task = hass.loop.call_later( warn_task = hass.loop.call_later(
SLOW_SETUP_WARNING, SLOW_SETUP_WARNING,
logger.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, self.platform_name,
SLOW_SETUP_WARNING, SLOW_SETUP_WARNING,
) )

View File

@ -1753,7 +1753,7 @@ rfk101py==0.0.1
rflink==0.0.50 rflink==0.0.50
# homeassistant.components.ring # homeassistant.components.ring
ring_doorbell==0.5.0 ring_doorbell==0.6.0
# homeassistant.components.fleetgo # homeassistant.components.fleetgo
ritassist==0.9.2 ritassist==0.9.2

View File

@ -567,7 +567,7 @@ restrictedpython==5.0
rflink==0.0.50 rflink==0.0.50
# homeassistant.components.ring # homeassistant.components.ring
ring_doorbell==0.5.0 ring_doorbell==0.6.0
# homeassistant.components.yamaha # homeassistant.components.yamaha
rxv==0.6.0 rxv==0.6.0

View File

@ -1,4 +1,6 @@
"""Configuration for Ring tests.""" """Configuration for Ring tests."""
import re
import pytest import pytest
import requests_mock import requests_mock
@ -33,17 +35,19 @@ def requests_mock_fixture():
) )
# Mocks the response for getting the history of a device # Mocks the response for getting the history of a device
mock.get( 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"), text=load_fixture("ring_doorbots.json"),
) )
# Mocks the response for getting the health of a device # Mocks the response for getting the health of a device
mock.get( 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"), text=load_fixture("ring_doorboot_health_attrs.json"),
) )
# Mocks the response for getting a chimes health # Mocks the response for getting a chimes health
mock.get( 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"), text=load_fixture("ring_chime_health_attrs.json"),
) )

View File

@ -1,87 +1,22 @@
"""The tests for the Ring binary sensor platform.""" """The tests for the Ring binary sensor platform."""
from asyncio import run_coroutine_threadsafe
import unittest
from unittest.mock import patch from unittest.mock import patch
import requests_mock from .common import setup_platform
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
class TestRingBinarySensorSetup(unittest.TestCase): async def test_binary_sensor(hass, requests_mock):
"""Test the Ring Binary Sensor platform.""" """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): ding_state = hass.states.get("binary_sensor.front_door_ding")
"""Mock add devices.""" assert ding_state is not None
for device in devices: assert ding_state.state == "off"
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"]

View File

@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock):
entity_registry = await hass.helpers.entity_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry()
entry = entity_registry.async_get("light.front_light") 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") 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): 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 # Mocks the response for turning a light on
requests_mock.put( 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"), text=load_fixture("ring_doorbot_siren_on_response.json"),
) )

View File

@ -1,122 +1,46 @@
"""The tests for the Ring sensor platform.""" """The tests for the Ring sensor platform."""
from asyncio import run_coroutine_threadsafe from .common import setup_platform
import unittest
from unittest.mock import patch
import requests_mock WIFI_ENABLED = False
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
class TestRingSensorSetup(unittest.TestCase): async def test_sensor(hass, requests_mock):
"""Test the Ring platform.""" """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): front_door_battery_state = hass.states.get("sensor.front_door_battery")
"""Mock add devices.""" assert front_door_battery_state is not None
for device in devices: assert front_door_battery_state.state == "100"
self.DEVICES.append(device)
def setUp(self): downstairs_volume_state = hass.states.get("sensor.downstairs_volume")
"""Initialize values for this testcase class.""" assert downstairs_volume_state is not None
self.hass = get_test_home_assistant() assert downstairs_volume_state.state == "2"
self.config = {
"username": "foo",
"password": "bar",
"monitored_conditions": [
"battery",
"last_activity",
"last_ding",
"last_motion",
"volume",
"wifi_signal_category",
"wifi_signal_strength",
],
}
def tearDown(self): front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity")
"""Stop everything that was started.""" assert front_door_last_activity_state is not None
self.hass.stop()
@requests_mock.Mocker() downstairs_wifi_signal_strength_state = hass.states.get(
def test_sensor(self, mock): "sensor.downstairs_wifi_signal_strength"
"""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"),
)
with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): if not WIFI_ENABLED:
run_coroutine_threadsafe( return
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: assert downstairs_wifi_signal_strength_state is not None
# Mimick add to hass assert downstairs_wifi_signal_strength_state.state == "-39"
device.hass = self.hass
run_coroutine_threadsafe(
device.async_added_to_hass(), self.hass.loop,
).result()
# Entity update data from ring data front_door_wifi_signal_category_state = hass.states.get(
device.update() "sensor.front_door_wifi_signal_category"
if device.name == "Front Battery": )
expected_icon = icon_for_battery_level( assert front_door_wifi_signal_category_state is not None
battery_level=int(device.state), charging=False assert front_door_wifi_signal_category_state.state == "good"
)
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"]
if device.name == "Downstairs WiFi Signal Strength": front_door_wifi_signal_strength_state = hass.states.get(
assert -39 == device.state "sensor.front_door_wifi_signal_strength"
)
if device.name == "Front Door WiFi Signal Category": assert front_door_wifi_signal_strength_state is not None
assert "good" == device.state assert front_door_wifi_signal_strength_state.state == "-58"
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

View File

@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock):
entity_registry = await hass.helpers.entity_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry()
entry = entity_registry.async_get("switch.front_siren") 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") 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): 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 # Mocks the response for turning a siren on
requests_mock.put( 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"), text=load_fixture("ring_doorbot_siren_on_response.json"),
) )

View File

@ -9,7 +9,7 @@
"do_not_disturb": {"seconds_left": 0}, "do_not_disturb": {"seconds_left": 0},
"features": {"ringtones_enabled": true}, "features": {"ringtones_enabled": true},
"firmware_version": "1.2.3", "firmware_version": "1.2.3",
"id": 999999, "id": 123456,
"kind": "chime", "kind": "chime",
"latitude": 12.000000, "latitude": 12.000000,
"longitude": -70.12345, "longitude": -70.12345,
@ -42,7 +42,7 @@
"shadow_correction_enabled": false, "shadow_correction_enabled": false,
"show_recordings": true}, "show_recordings": true},
"firmware_version": "1.4.26", "firmware_version": "1.4.26",
"id": 987652, "id": 987654,
"kind": "lpd_v1", "kind": "lpd_v1",
"latitude": 12.000000, "latitude": 12.000000,
"longitude": -70.12345, "longitude": -70.12345,
@ -93,7 +93,7 @@
"shadow_correction_enabled": false, "shadow_correction_enabled": false,
"show_recordings": true}, "show_recordings": true},
"firmware_version": "1.9.3", "firmware_version": "1.9.3",
"id": 987652, "id": 765432,
"kind": "hp_cam_v1", "kind": "hp_cam_v1",
"latitude": 12.000000, "latitude": 12.000000,
"led_status": "off", "led_status": "off",
@ -231,7 +231,7 @@
"shadow_correction_enabled": false, "shadow_correction_enabled": false,
"show_recordings": true}, "show_recordings": true},
"firmware_version": "1.9.3", "firmware_version": "1.9.3",
"id": 987652, "id": 345678,
"kind": "hp_cam_v1", "kind": "hp_cam_v1",
"latitude": 12.000000, "latitude": 12.000000,
"led_status": "on", "led_status": "on",

View File

@ -9,7 +9,7 @@
"do_not_disturb": {"seconds_left": 0}, "do_not_disturb": {"seconds_left": 0},
"features": {"ringtones_enabled": true}, "features": {"ringtones_enabled": true},
"firmware_version": "1.2.3", "firmware_version": "1.2.3",
"id": 999999, "id": 123456,
"kind": "chime", "kind": "chime",
"latitude": 12.000000, "latitude": 12.000000,
"longitude": -70.12345, "longitude": -70.12345,
@ -42,7 +42,7 @@
"shadow_correction_enabled": false, "shadow_correction_enabled": false,
"show_recordings": true}, "show_recordings": true},
"firmware_version": "1.4.26", "firmware_version": "1.4.26",
"id": 987652, "id": 987654,
"kind": "lpd_v1", "kind": "lpd_v1",
"latitude": 12.000000, "latitude": 12.000000,
"longitude": -70.12345, "longitude": -70.12345,
@ -93,7 +93,7 @@
"shadow_correction_enabled": false, "shadow_correction_enabled": false,
"show_recordings": true}, "show_recordings": true},
"firmware_version": "1.9.3", "firmware_version": "1.9.3",
"id": 987652, "id": 765432,
"kind": "hp_cam_v1", "kind": "hp_cam_v1",
"latitude": 12.000000, "latitude": 12.000000,
"led_status": "on", "led_status": "on",
@ -231,7 +231,7 @@
"shadow_correction_enabled": false, "shadow_correction_enabled": false,
"show_recordings": true}, "show_recordings": true},
"firmware_version": "1.9.3", "firmware_version": "1.9.3",
"id": 987652, "id": 345678,
"kind": "hp_cam_v1", "kind": "hp_cam_v1",
"latitude": 12.000000, "latitude": 12.000000,
"led_status": "off", "led_status": "off",