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 printpull/30803/head
parent
8e5f46d5b5
commit
4bc520c724
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue