mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Refactor Ring data handling (#30777)
* Refactor Ring data handling * Add async_ to methods
This commit is contained in:
parent
de26108b23
commit
1e82813c3b
@ -4,16 +4,16 @@ from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
from typing import Optional
|
||||
|
||||
from oauthlib.oauth2 import AccessDeniedError
|
||||
from ring_doorbell import Auth, Ring
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
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
|
||||
|
||||
@ -24,16 +24,8 @@ ATTRIBUTION = "Data provided by Ring.com"
|
||||
NOTIFICATION_ID = "ring_notification"
|
||||
NOTIFICATION_TITLE = "Ring Setup"
|
||||
|
||||
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)
|
||||
|
||||
PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera")
|
||||
|
||||
@ -93,9 +85,36 @@ async def async_setup_entry(hass, entry):
|
||||
auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater)
|
||||
ring = Ring(auth)
|
||||
|
||||
await hass.async_add_executor_job(ring.update_data)
|
||||
try:
|
||||
await hass.async_add_executor_job(ring.update_data)
|
||||
except AccessDeniedError:
|
||||
_LOGGER.error("Access token is no longer valid. Please set up Ring again")
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ring
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
"api": ring,
|
||||
"devices": ring.devices(),
|
||||
"device_data": GlobalDataUpdater(
|
||||
hass, entry.entry_id, ring, "update_devices", timedelta(minutes=1)
|
||||
),
|
||||
"dings_data": GlobalDataUpdater(
|
||||
hass, entry.entry_id, ring, "update_dings", timedelta(seconds=10)
|
||||
),
|
||||
"history_data": DeviceDataUpdater(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
ring,
|
||||
lambda device: device.history(limit=10),
|
||||
timedelta(minutes=1),
|
||||
),
|
||||
"health_data": DeviceDataUpdater(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
ring,
|
||||
lambda device: device.update_health_data(),
|
||||
timedelta(minutes=1),
|
||||
),
|
||||
}
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
@ -105,25 +124,16 @@ async def async_setup_entry(hass, entry):
|
||||
if hass.services.has_service(DOMAIN, "update"):
|
||||
return True
|
||||
|
||||
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)
|
||||
async def async_refresh_all(_):
|
||||
"""Refresh all ring data."""
|
||||
for info in hass.data[DOMAIN].values():
|
||||
await info["device_data"].async_refresh_all()
|
||||
await info["dings_data"].async_refresh_all()
|
||||
await hass.async_add_executor_job(info["history_data"].refresh_all)
|
||||
await hass.async_add_executor_job(info["health_data"].refresh_all)
|
||||
|
||||
# register service
|
||||
hass.services.async_register(DOMAIN, "update", refresh_all)
|
||||
|
||||
# register scan interval for ring
|
||||
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)
|
||||
hass.services.async_register(DOMAIN, "update", async_refresh_all)
|
||||
|
||||
return True
|
||||
|
||||
@ -146,98 +156,141 @@ async def async_unload_entry(hass, entry):
|
||||
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)
|
||||
# Last entry unloaded, clean up service
|
||||
hass.services.async_remove(DOMAIN, "update")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HealthDataUpdater:
|
||||
"""Data storage for health data."""
|
||||
class GlobalDataUpdater:
|
||||
"""Data storage for single API endpoint."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Track devices that need health data updated."""
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry_id: str,
|
||||
ring: Ring,
|
||||
update_method: str,
|
||||
update_interval: timedelta,
|
||||
):
|
||||
"""Initialize global data updater."""
|
||||
self.hass = hass
|
||||
self.config_entry_id = config_entry_id
|
||||
self.ring = ring
|
||||
self.update_method = update_method
|
||||
self.update_interval = update_interval
|
||||
self.listeners = []
|
||||
self._unsub_interval = None
|
||||
|
||||
@callback
|
||||
def async_add_listener(self, update_callback):
|
||||
"""Listen for data updates."""
|
||||
# This is the first listener, set up interval.
|
||||
if not self.listeners:
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self.hass, self.async_refresh_all, self.update_interval
|
||||
)
|
||||
|
||||
self.listeners.append(update_callback)
|
||||
|
||||
@callback
|
||||
def async_remove_listener(self, update_callback):
|
||||
"""Remove data update."""
|
||||
self.listeners.remove(update_callback)
|
||||
|
||||
if not self.listeners:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
|
||||
async def async_refresh_all(self, _now: Optional[int] = None) -> None:
|
||||
"""Time to update."""
|
||||
if not self.listeners:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
getattr(self.ring, self.update_method)
|
||||
)
|
||||
except AccessDeniedError:
|
||||
_LOGGER.error("Ring access token is no longer valid. Set up Ring again")
|
||||
await self.hass.config_entries.async_unload(self.config_entry_id)
|
||||
return
|
||||
|
||||
for update_callback in self.listeners:
|
||||
update_callback()
|
||||
|
||||
|
||||
class DeviceDataUpdater:
|
||||
"""Data storage for device data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry_id: str,
|
||||
ring: Ring,
|
||||
update_method: str,
|
||||
update_interval: timedelta,
|
||||
):
|
||||
"""Initialize device data updater."""
|
||||
self.hass = hass
|
||||
self.config_entry_id = config_entry_id
|
||||
self.ring = ring
|
||||
self.update_method = update_method
|
||||
self.update_interval = update_interval
|
||||
self.devices = {}
|
||||
self._unsub_interval = None
|
||||
|
||||
async def track_device(self, config_entry_id, device):
|
||||
async def async_track_device(self, device, update_callback):
|
||||
"""Track a device."""
|
||||
if not self.devices:
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self.hass, self.refresh_all, SCAN_INTERVAL
|
||||
self.hass, self.refresh_all, self.update_interval
|
||||
)
|
||||
|
||||
key = (config_entry_id, device.device_id)
|
||||
|
||||
if key not in self.devices:
|
||||
self.devices[key] = {
|
||||
if device.device_id not in self.devices:
|
||||
self.devices[device.device_id] = {
|
||||
"device": device,
|
||||
"count": 1,
|
||||
"update_callbacks": [update_callback],
|
||||
"data": None,
|
||||
}
|
||||
# Store task so that other concurrent requests can wait for us to finish and
|
||||
# data be available.
|
||||
self.devices[device.device_id]["task"] = asyncio.current_task()
|
||||
self.devices[device.device_id][
|
||||
"data"
|
||||
] = await self.hass.async_add_executor_job(self.update_method, device)
|
||||
self.devices[device.device_id].pop("task")
|
||||
else:
|
||||
self.devices[key]["count"] += 1
|
||||
self.devices[device.device_id]["update_callbacks"].append(update_callback)
|
||||
# If someone is currently fetching data as part of the initialization, wait for them
|
||||
if "task" in self.devices[device.device_id]:
|
||||
await self.devices[device.device_id]["task"]
|
||||
|
||||
await self.hass.async_add_executor_job(device.update_health_data)
|
||||
update_callback(self.devices[device.device_id]["data"])
|
||||
|
||||
@callback
|
||||
def untrack_device(self, config_entry_id, device):
|
||||
def async_untrack_device(self, device, update_callback):
|
||||
"""Untrack a device."""
|
||||
key = (config_entry_id, device.device_id)
|
||||
self.devices[key]["count"] -= 1
|
||||
self.devices[device.device_id]["update_callbacks"].remove(update_callback)
|
||||
|
||||
if self.devices[key]["count"] == 0:
|
||||
self.devices.pop(key)
|
||||
if not self.devices[device.device_id]["update_callbacks"]:
|
||||
self.devices.pop(device.device_id)
|
||||
|
||||
if not self.devices:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
|
||||
def refresh_all(self, _):
|
||||
def refresh_all(self, _=None):
|
||||
"""Refresh all registered devices."""
|
||||
for info in self.devices.values():
|
||||
info["device"].update_health_data()
|
||||
try:
|
||||
data = info["data"] = self.update_method(info["device"])
|
||||
except AccessDeniedError:
|
||||
_LOGGER.error("Ring access token is no longer valid. Set up Ring again")
|
||||
self.hass.add_job(
|
||||
self.hass.config_entries.async_unload(self.config_entry_id)
|
||||
)
|
||||
return
|
||||
|
||||
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
|
||||
for update_callback in info["update_callbacks"]:
|
||||
self.hass.loop.call_soon_threadsafe(update_callback, data)
|
||||
|
@ -1,13 +1,12 @@
|
||||
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
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, DOMAIN, SIGNAL_UPDATE_RING
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -22,8 +21,8 @@ SENSOR_TYPES = {
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Ring binary sensors from a config entry."""
|
||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices = ring.devices()
|
||||
ring = hass.data[DOMAIN][config_entry.entry_id]["api"]
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
sensors = []
|
||||
|
||||
@ -33,49 +32,62 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
continue
|
||||
|
||||
for device in devices[device_type]:
|
||||
sensors.append(RingBinarySensor(ring, device, sensor_type))
|
||||
sensors.append(
|
||||
RingBinarySensor(config_entry.entry_id, ring, device, sensor_type)
|
||||
)
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class RingBinarySensor(BinarySensorDevice):
|
||||
class RingBinarySensor(RingEntityMixin, BinarySensorDevice):
|
||||
"""A binary sensor implementation for Ring device."""
|
||||
|
||||
def __init__(self, ring, device, sensor_type):
|
||||
_active_alert = None
|
||||
|
||||
def __init__(self, config_entry_id, ring, device, sensor_type):
|
||||
"""Initialize a sensor for Ring device."""
|
||||
self._sensor_type = sensor_type
|
||||
super().__init__(config_entry_id, device)
|
||||
self._ring = ring
|
||||
self._device = device
|
||||
self._sensor_type = sensor_type
|
||||
self._name = "{0} {1}".format(
|
||||
self._device.name, SENSOR_TYPES.get(self._sensor_type)[0]
|
||||
self._device.name, SENSOR_TYPES.get(sensor_type)[0]
|
||||
)
|
||||
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
|
||||
self._device_class = SENSOR_TYPES.get(sensor_type)[2]
|
||||
self._state = None
|
||||
self._unique_id = f"{self._device.id}-{self._sensor_type}"
|
||||
self._disp_disconnect = None
|
||||
self._unique_id = f"{device.id}-{sensor_type}"
|
||||
self._update_alert()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._disp_disconnect = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_RING, self._update_callback
|
||||
)
|
||||
await super().async_added_to_hass()
|
||||
self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback)
|
||||
self._dings_update_callback()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
if self._disp_disconnect:
|
||||
self._disp_disconnect()
|
||||
self._disp_disconnect = None
|
||||
await super().async_will_remove_from_hass()
|
||||
self.ring_objects["dings_data"].async_remove_listener(
|
||||
self._dings_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
def _dings_update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
_LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name)
|
||||
self._update_alert()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False, updates are controlled via the hub."""
|
||||
return False
|
||||
@callback
|
||||
def _update_alert(self):
|
||||
"""Update active alert."""
|
||||
self._active_alert = next(
|
||||
(
|
||||
alert
|
||||
for alert in self._ring.active_alerts()
|
||||
if alert["kind"] == self._sensor_type
|
||||
and alert["doorbot_id"] == self._device.id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -85,7 +97,7 @@ class RingBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._state
|
||||
return self._active_alert is not None
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@ -97,32 +109,17 @@ class RingBinarySensor(BinarySensorDevice):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device.name,
|
||||
"model": self._device.model,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||
attrs = super().device_state_attributes
|
||||
|
||||
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")
|
||||
if self._active_alert is None:
|
||||
return attrs
|
||||
|
||||
attrs["state"] = self._active_alert["state"]
|
||||
attrs["expires_at"] = datetime.fromtimestamp(
|
||||
self._active_alert.get("now") + self._active_alert.get("expires_in")
|
||||
).isoformat()
|
||||
|
||||
return attrs
|
||||
|
||||
async def async_update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
self._state = any(
|
||||
alert["kind"] == self._sensor_type
|
||||
and alert["doorbot_id"] == self._device.id
|
||||
for alert in self._ring.active_alerts()
|
||||
)
|
||||
|
@ -12,10 +12,10 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
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_HISTORY, DOMAIN, SIGNAL_UPDATE_RING
|
||||
from . import ATTRIBUTION, DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
|
||||
|
||||
@ -24,8 +24,7 @@ _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 = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices = ring.devices()
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
cams = []
|
||||
for camera in chain(
|
||||
@ -36,42 +35,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera))
|
||||
|
||||
async_add_entities(cams, True)
|
||||
async_add_entities(cams)
|
||||
|
||||
|
||||
class RingCam(Camera):
|
||||
class RingCam(RingEntityMixin, Camera):
|
||||
"""An implementation of a Ring Door Bell camera."""
|
||||
|
||||
def __init__(self, config_entry_id, ffmpeg, device):
|
||||
"""Initialize a Ring Door Bell camera."""
|
||||
super().__init__()
|
||||
self._config_entry_id = config_entry_id
|
||||
self._device = device
|
||||
super().__init__(config_entry_id, device)
|
||||
|
||||
self._name = self._device.name
|
||||
self._ffmpeg = ffmpeg
|
||||
self._last_event = None
|
||||
self._last_video_id = None
|
||||
self._video_url = None
|
||||
self._utcnow = dt_util.utcnow()
|
||||
self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL
|
||||
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
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self.ring_objects["history_data"].async_track_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
if self._disp_disconnect:
|
||||
self._disp_disconnect()
|
||||
self._disp_disconnect = None
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects["history_data"].async_untrack_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
def _history_update_callback(self, history_data):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
_LOGGER.debug("Updating Ring camera %s (callback)", self.name)
|
||||
if history_data:
|
||||
self._last_event = history_data[0]
|
||||
self.async_schedule_update_ha_state(True)
|
||||
else:
|
||||
self._last_event = None
|
||||
self._last_video_id = None
|
||||
self._video_url = None
|
||||
self._expires_at = self._utcnow
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -83,16 +92,6 @@ class RingCam(Camera):
|
||||
"""Return a unique ID."""
|
||||
return self._device.id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device.name,
|
||||
"model": self._device.model,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
@ -104,7 +103,6 @@ class RingCam(Camera):
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
||||
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
|
||||
if self._video_url is None:
|
||||
@ -136,33 +134,23 @@ class RingCam(Camera):
|
||||
|
||||
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()
|
||||
|
||||
data = await self.hass.data[DATA_HISTORY].async_get_history(
|
||||
self._config_entry_id, self._device
|
||||
)
|
||||
|
||||
if not data:
|
||||
if self._last_event is None:
|
||||
return
|
||||
|
||||
last_event = data[0]
|
||||
last_recording_id = last_event["id"]
|
||||
video_status = last_event["recording"]["status"]
|
||||
if self._last_event["recording"]["status"] != "ready":
|
||||
return
|
||||
|
||||
if video_status == "ready" and (
|
||||
self._last_video_id != last_recording_id or self._utcnow >= self._expires_at
|
||||
if (
|
||||
self._last_video_id == self._last_event["id"]
|
||||
and self._utcnow <= self._expires_at
|
||||
):
|
||||
return
|
||||
|
||||
video_url = await self.hass.async_add_executor_job(
|
||||
self._device.recording_url, last_recording_id
|
||||
)
|
||||
video_url = await self.hass.async_add_executor_job(
|
||||
self._device.recording_url, self._last_event["id"]
|
||||
)
|
||||
|
||||
if video_url:
|
||||
_LOGGER.debug("Ring DoorBell properties refreshed")
|
||||
|
||||
# update attributes if new video or if URL has expired
|
||||
self._last_video_id = last_recording_id
|
||||
self._video_url = video_url
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
||||
if video_url:
|
||||
self._last_video_id = self._last_event["id"]
|
||||
self._video_url = video_url
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
||||
|
@ -39,9 +39,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
|
53
homeassistant/components/ring/entity.py
Normal file
53
homeassistant/components/ring/entity.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Base class for Ring entity."""
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import ATTRIBUTION, DOMAIN
|
||||
|
||||
|
||||
class RingEntityMixin:
|
||||
"""Base implementation for Ring device."""
|
||||
|
||||
def __init__(self, config_entry_id, device):
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super().__init__()
|
||||
self._config_entry_id = config_entry_id
|
||||
self._device = device
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.ring_objects["device_data"].async_add_listener(self._update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
self.ring_objects["device_data"].async_remove_listener(self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def ring_objects(self):
|
||||
"""Return the Ring API objects."""
|
||||
return self.hass.data[DOMAIN][self._config_entry_id]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False, updates are controlled via the hub."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device.name,
|
||||
"model": self._device.model,
|
||||
"manufacturer": "Ring",
|
||||
}
|
@ -4,10 +4,10 @@ import logging
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import DOMAIN, SIGNAL_UPDATE_RING
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -25,51 +25,35 @@ OFF_STATE = "off"
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the lights for the Ring devices."""
|
||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
devices = ring.devices()
|
||||
lights = []
|
||||
|
||||
for device in devices["stickup_cams"]:
|
||||
if device.has_capability("light"):
|
||||
lights.append(RingLight(device))
|
||||
lights.append(RingLight(config_entry.entry_id, device))
|
||||
|
||||
async_add_entities(lights, True)
|
||||
async_add_entities(lights)
|
||||
|
||||
|
||||
class RingLight(Light):
|
||||
class RingLight(RingEntityMixin, Light):
|
||||
"""Creates a switch to turn the ring cameras light on and off."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, config_entry_id, device):
|
||||
"""Initialize the light."""
|
||||
self._device = device
|
||||
self._unique_id = self._device.id
|
||||
self._light_on = False
|
||||
super().__init__(config_entry_id, device)
|
||||
self._unique_id = device.id
|
||||
self._light_on = device.lights == ON_STATE
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
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."""
|
||||
_LOGGER.debug("Updating Ring light %s (callback)", self.name)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update controlled via the hub."""
|
||||
return False
|
||||
self._light_on = self._device.lights == ON_STATE
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -86,22 +70,12 @@ class RingLight(Light):
|
||||
"""If the switch is currently on or off."""
|
||||
return self._light_on
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device.name,
|
||||
"model": self._device.model,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
def _set_light(self, new_state):
|
||||
"""Update light state, and causes Home Assistant to correctly update."""
|
||||
self._device.lights = new_state
|
||||
self._light_on = new_state == ON_STATE
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.async_schedule_update_ha_state(True)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on for 30 seconds."""
|
||||
@ -110,11 +84,3 @@ class RingLight(Light):
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self._set_light(OFF_STATE)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update current state of the light."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
_LOGGER.debug("Skipping update...")
|
||||
return
|
||||
|
||||
self._light_on = self._device.lights == ON_STATE
|
||||
|
@ -1,88 +1,20 @@
|
||||
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
from . import (
|
||||
ATTRIBUTION,
|
||||
DATA_HEALTH_DATA_TRACKER,
|
||||
DATA_HISTORY,
|
||||
DOMAIN,
|
||||
SIGNAL_UPDATE_HEALTH_RING,
|
||||
SIGNAL_UPDATE_RING,
|
||||
)
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Sensor types: Name, category, units, icon, kind, device_class
|
||||
SENSOR_TYPES = {
|
||||
"battery": [
|
||||
"Battery",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
"%",
|
||||
None,
|
||||
None,
|
||||
"battery",
|
||||
],
|
||||
"last_activity": [
|
||||
"Last Activity",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
None,
|
||||
"timestamp",
|
||||
],
|
||||
"last_ding": [
|
||||
"Last Ding",
|
||||
["doorbots", "authorized_doorbots"],
|
||||
None,
|
||||
"history",
|
||||
"ding",
|
||||
"timestamp",
|
||||
],
|
||||
"last_motion": [
|
||||
"Last Motion",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
"motion",
|
||||
"timestamp",
|
||||
],
|
||||
"volume": [
|
||||
"Volume",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"bell-ring",
|
||||
None,
|
||||
None,
|
||||
],
|
||||
"wifi_signal_category": [
|
||||
"WiFi Signal Category",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"wifi",
|
||||
None,
|
||||
None,
|
||||
],
|
||||
"wifi_signal_strength": [
|
||||
"WiFi Signal Strength",
|
||||
["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 = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices = ring.devices()
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
# Makes a ton of requests. We will make this a config entry option in the future
|
||||
wifi_enabled = False
|
||||
|
||||
@ -100,72 +32,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
if device_type == "battery" and device.battery_life is None:
|
||||
continue
|
||||
|
||||
sensors.append(RingSensor(config_entry.entry_id, device, sensor_type))
|
||||
sensors.append(
|
||||
SENSOR_TYPES[sensor_type][6](
|
||||
config_entry.entry_id, device, sensor_type
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class RingSensor(Entity):
|
||||
class RingSensor(RingEntityMixin, Entity):
|
||||
"""A sensor implementation for Ring device."""
|
||||
|
||||
def __init__(self, config_entry_id, device, sensor_type):
|
||||
"""Initialize a sensor for Ring device."""
|
||||
self._config_entry_id = config_entry_id
|
||||
super().__init__(config_entry_id, device)
|
||||
self._sensor_type = sensor_type
|
||||
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._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3])
|
||||
self._kind = SENSOR_TYPES.get(sensor_type)[4]
|
||||
self._name = "{0} {1}".format(
|
||||
self._device.name, SENSOR_TYPES.get(self._sensor_type)[0]
|
||||
self._device.name, SENSOR_TYPES.get(sensor_type)[0]
|
||||
)
|
||||
self._state = None
|
||||
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
|
||||
)
|
||||
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."""
|
||||
if self._disp_disconnect:
|
||||
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."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
self._unique_id = f"{device.id}-{sensor_type}"
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -180,7 +69,11 @@ class RingSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
if self._sensor_type == "volume":
|
||||
return self._device.volume
|
||||
|
||||
if self._sensor_type == "battery":
|
||||
return self._device.battery_life
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@ -192,37 +85,12 @@ class RingSensor(Entity):
|
||||
"""Return sensor device class."""
|
||||
return SENSOR_TYPES[self._sensor_type][5]
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device.name,
|
||||
"model": self._device.model,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
|
||||
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||
|
||||
if self._extra and self._sensor_type.startswith("last_"):
|
||||
attrs["created_at"] = self._extra["created_at"]
|
||||
attrs["answered"] = self._extra["answered"]
|
||||
attrs["recording_status"] = self._extra["recording"]["status"]
|
||||
attrs["category"] = self._extra["kind"]
|
||||
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
if self._sensor_type == "battery" and self._state is not None:
|
||||
if self._sensor_type == "battery" and self._device.battery_life is not None:
|
||||
return icon_for_battery_level(
|
||||
battery_level=int(self._state), charging=False
|
||||
battery_level=self._device.battery_life, charging=False
|
||||
)
|
||||
return self._icon
|
||||
|
||||
@ -231,34 +99,168 @@ class RingSensor(Entity):
|
||||
"""Return the units of measurement."""
|
||||
return SENSOR_TYPES.get(self._sensor_type)[2]
|
||||
|
||||
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._device.volume
|
||||
class HealthDataRingSensor(RingSensor):
|
||||
"""Ring sensor that relies on health data."""
|
||||
|
||||
if self._sensor_type == "battery":
|
||||
self._state = self._device.battery_life
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if self._sensor_type.startswith("last_"):
|
||||
history = await self.hass.data[DATA_HISTORY].async_get_history(
|
||||
self._config_entry_id, self._device
|
||||
)
|
||||
await self.ring_objects["health_data"].async_track_device(
|
||||
self._device, self._health_update_callback
|
||||
)
|
||||
|
||||
found = None
|
||||
for entry in history:
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects["health_data"].async_untrack_device(
|
||||
self._device, self._health_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _health_update_callback(self, _health_data):
|
||||
"""Call update method."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._sensor_type == "wifi_signal_category":
|
||||
return self._device.wifi_signal_category
|
||||
|
||||
if self._sensor_type == "wifi_signal_strength":
|
||||
return self._device.wifi_signal_strength
|
||||
|
||||
|
||||
class HistoryRingSensor(RingSensor):
|
||||
"""Ring sensor that relies on history data."""
|
||||
|
||||
_latest_event = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self.ring_objects["history_data"].async_track_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects["history_data"].async_untrack_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _history_update_callback(self, history_data):
|
||||
"""Call update method."""
|
||||
if not history_data:
|
||||
return
|
||||
|
||||
found = None
|
||||
if self._kind is None:
|
||||
found = history_data[0]
|
||||
else:
|
||||
for entry in history_data:
|
||||
if entry["kind"] == self._kind:
|
||||
found = entry
|
||||
break
|
||||
|
||||
if found:
|
||||
self._extra = found
|
||||
created_at = found["created_at"]
|
||||
self._state = created_at.isoformat()
|
||||
if not found:
|
||||
return
|
||||
|
||||
if self._sensor_type == "wifi_signal_category":
|
||||
self._state = self._device.wifi_signal_category
|
||||
self._latest_event = found
|
||||
self.async_write_ha_state()
|
||||
|
||||
if self._sensor_type == "wifi_signal_strength":
|
||||
self._state = self._device.wifi_signal_strength
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._latest_event is None:
|
||||
return None
|
||||
|
||||
return self._latest_event["created_at"].isoformat()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = super().device_state_attributes
|
||||
|
||||
if self._latest_event:
|
||||
attrs["created_at"] = self._latest_event["created_at"]
|
||||
attrs["answered"] = self._latest_event["answered"]
|
||||
attrs["recording_status"] = self._latest_event["recording"]["status"]
|
||||
attrs["category"] = self._latest_event["kind"]
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
# Sensor types: Name, category, units, icon, kind, device_class, class
|
||||
SENSOR_TYPES = {
|
||||
"battery": [
|
||||
"Battery",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
"%",
|
||||
None,
|
||||
None,
|
||||
"battery",
|
||||
RingSensor,
|
||||
],
|
||||
"last_activity": [
|
||||
"Last Activity",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
None,
|
||||
"timestamp",
|
||||
HistoryRingSensor,
|
||||
],
|
||||
"last_ding": [
|
||||
"Last Ding",
|
||||
["doorbots", "authorized_doorbots"],
|
||||
None,
|
||||
"history",
|
||||
"ding",
|
||||
"timestamp",
|
||||
HistoryRingSensor,
|
||||
],
|
||||
"last_motion": [
|
||||
"Last Motion",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
"motion",
|
||||
"timestamp",
|
||||
HistoryRingSensor,
|
||||
],
|
||||
"volume": [
|
||||
"Volume",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"bell-ring",
|
||||
None,
|
||||
None,
|
||||
RingSensor,
|
||||
],
|
||||
"wifi_signal_category": [
|
||||
"WiFi Signal Category",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"wifi",
|
||||
None,
|
||||
None,
|
||||
HealthDataRingSensor,
|
||||
],
|
||||
"wifi_signal_strength": [
|
||||
"WiFi Signal Strength",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
"dBm",
|
||||
"wifi",
|
||||
None,
|
||||
"signal_strength",
|
||||
HealthDataRingSensor,
|
||||
],
|
||||
}
|
||||
|
@ -4,10 +4,10 @@ import logging
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import DOMAIN, SIGNAL_UPDATE_RING
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -24,49 +24,24 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5)
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the switches for the Ring devices."""
|
||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices = ring.devices()
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
switches = []
|
||||
|
||||
for device in devices["stickup_cams"]:
|
||||
if device.has_capability("siren"):
|
||||
switches.append(SirenSwitch(device))
|
||||
switches.append(SirenSwitch(config_entry.entry_id, device))
|
||||
|
||||
async_add_entities(switches, True)
|
||||
async_add_entities(switches)
|
||||
|
||||
|
||||
class BaseRingSwitch(SwitchDevice):
|
||||
class BaseRingSwitch(RingEntityMixin, SwitchDevice):
|
||||
"""Represents a switch for controlling an aspect of a ring device."""
|
||||
|
||||
def __init__(self, device, device_type):
|
||||
def __init__(self, config_entry_id, device, device_type):
|
||||
"""Initialize the switch."""
|
||||
self._device = device
|
||||
super().__init__(config_entry_id, device)
|
||||
self._device_type = device_type
|
||||
self._unique_id = f"{self._device.id}-{self._device_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."""
|
||||
_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):
|
||||
@ -78,25 +53,24 @@ class BaseRingSwitch(SwitchDevice):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device.name,
|
||||
"model": self._device.model,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
|
||||
class SirenSwitch(BaseRingSwitch):
|
||||
"""Creates a switch to turn the ring cameras siren on and off."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, config_entry_id, device):
|
||||
"""Initialize the switch for a device with a siren."""
|
||||
super().__init__(device, "siren")
|
||||
super().__init__(config_entry_id, device, "siren")
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
self._siren_on = False
|
||||
self._siren_on = device.siren > 0
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
|
||||
self._siren_on = self._device.siren > 0
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _set_switch(self, new_state):
|
||||
"""Update switch state, and causes Home Assistant to correctly update."""
|
||||
@ -122,10 +96,3 @@ class SirenSwitch(BaseRingSwitch):
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return SIREN_ICON
|
||||
|
||||
async def async_update(self):
|
||||
"""Update state of the siren."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
_LOGGER.debug("Skipping update...")
|
||||
return
|
||||
self._siren_on = self._device.siren > 0
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""The tests for the Ring binary sensor platform."""
|
||||
from time import time
|
||||
from unittest.mock import patch
|
||||
|
||||
from .common import setup_platform
|
||||
@ -8,7 +9,15 @@ 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}],
|
||||
return_value=[
|
||||
{
|
||||
"kind": "motion",
|
||||
"doorbot_id": 987654,
|
||||
"state": "ringing",
|
||||
"now": time(),
|
||||
"expires_in": 180,
|
||||
}
|
||||
],
|
||||
):
|
||||
await setup_platform(hass, "binary_sensor")
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user