mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +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
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import time
|
from typing import Optional
|
||||||
|
|
||||||
|
from oauthlib.oauth2 import AccessDeniedError
|
||||||
from ring_doorbell import Auth, Ring
|
from ring_doorbell import Auth, Ring
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__
|
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
|
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.helpers.event import async_track_time_interval
|
||||||
from homeassistant.util.async_ import run_callback_threadsafe
|
from homeassistant.util.async_ import run_callback_threadsafe
|
||||||
|
|
||||||
@ -24,16 +24,8 @@ ATTRIBUTION = "Data provided by Ring.com"
|
|||||||
NOTIFICATION_ID = "ring_notification"
|
NOTIFICATION_ID = "ring_notification"
|
||||||
NOTIFICATION_TITLE = "Ring Setup"
|
NOTIFICATION_TITLE = "Ring Setup"
|
||||||
|
|
||||||
DATA_HISTORY = "ring_history"
|
|
||||||
DATA_HEALTH_DATA_TRACKER = "ring_health_data"
|
|
||||||
DATA_TRACK_INTERVAL = "ring_track_interval"
|
|
||||||
|
|
||||||
DOMAIN = "ring"
|
DOMAIN = "ring"
|
||||||
DEFAULT_ENTITY_NAMESPACE = "ring"
|
DEFAULT_ENTITY_NAMESPACE = "ring"
|
||||||
SIGNAL_UPDATE_RING = "ring_update"
|
|
||||||
SIGNAL_UPDATE_HEALTH_RING = "ring_health_update"
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
|
||||||
|
|
||||||
PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera")
|
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)
|
auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater)
|
||||||
ring = Ring(auth)
|
ring = Ring(auth)
|
||||||
|
|
||||||
|
try:
|
||||||
await hass.async_add_executor_job(ring.update_data)
|
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:
|
for component in PLATFORMS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
@ -105,25 +124,16 @@ async def async_setup_entry(hass, entry):
|
|||||||
if hass.services.has_service(DOMAIN, "update"):
|
if hass.services.has_service(DOMAIN, "update"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def refresh_all(_):
|
async def async_refresh_all(_):
|
||||||
"""Refresh all ring accounts."""
|
"""Refresh all ring data."""
|
||||||
await asyncio.gather(
|
for info in hass.data[DOMAIN].values():
|
||||||
*[
|
await info["device_data"].async_refresh_all()
|
||||||
hass.async_add_executor_job(api.update_data)
|
await info["dings_data"].async_refresh_all()
|
||||||
for api in hass.data[DOMAIN].values()
|
await hass.async_add_executor_job(info["history_data"].refresh_all)
|
||||||
]
|
await hass.async_add_executor_job(info["health_data"].refresh_all)
|
||||||
)
|
|
||||||
async_dispatcher_send(hass, SIGNAL_UPDATE_RING)
|
|
||||||
|
|
||||||
# register service
|
# register service
|
||||||
hass.services.async_register(DOMAIN, "update", refresh_all)
|
hass.services.async_register(DOMAIN, "update", async_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)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -146,98 +156,141 @@ async def async_unload_entry(hass, entry):
|
|||||||
if len(hass.data[DOMAIN]) != 0:
|
if len(hass.data[DOMAIN]) != 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Last entry unloaded, clean up
|
# Last entry unloaded, clean up service
|
||||||
hass.data.pop(DATA_TRACK_INTERVAL)()
|
|
||||||
hass.data.pop(DATA_HEALTH_DATA_TRACKER)
|
|
||||||
hass.data.pop(DATA_HISTORY)
|
|
||||||
hass.services.async_remove(DOMAIN, "update")
|
hass.services.async_remove(DOMAIN, "update")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class HealthDataUpdater:
|
class GlobalDataUpdater:
|
||||||
"""Data storage for health data."""
|
"""Data storage for single API endpoint."""
|
||||||
|
|
||||||
def __init__(self, hass):
|
def __init__(
|
||||||
"""Track devices that need health data updated."""
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry_id: str,
|
||||||
|
ring: Ring,
|
||||||
|
update_method: str,
|
||||||
|
update_interval: timedelta,
|
||||||
|
):
|
||||||
|
"""Initialize global data updater."""
|
||||||
self.hass = hass
|
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.devices = {}
|
||||||
self._unsub_interval = None
|
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."""
|
"""Track a device."""
|
||||||
if not self.devices:
|
if not self.devices:
|
||||||
self._unsub_interval = async_track_time_interval(
|
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 device.device_id not in self.devices:
|
||||||
|
self.devices[device.device_id] = {
|
||||||
if key not in self.devices:
|
|
||||||
self.devices[key] = {
|
|
||||||
"device": device,
|
"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:
|
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
|
@callback
|
||||||
def untrack_device(self, config_entry_id, device):
|
def async_untrack_device(self, device, update_callback):
|
||||||
"""Untrack a device."""
|
"""Untrack a device."""
|
||||||
key = (config_entry_id, device.device_id)
|
self.devices[device.device_id]["update_callbacks"].remove(update_callback)
|
||||||
self.devices[key]["count"] -= 1
|
|
||||||
|
|
||||||
if self.devices[key]["count"] == 0:
|
if not self.devices[device.device_id]["update_callbacks"]:
|
||||||
self.devices.pop(key)
|
self.devices.pop(device.device_id)
|
||||||
|
|
||||||
if not self.devices:
|
if not self.devices:
|
||||||
self._unsub_interval()
|
self._unsub_interval()
|
||||||
self._unsub_interval = None
|
self._unsub_interval = None
|
||||||
|
|
||||||
def refresh_all(self, _):
|
def refresh_all(self, _=None):
|
||||||
"""Refresh all registered devices."""
|
"""Refresh all registered devices."""
|
||||||
for info in self.devices.values():
|
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)
|
for update_callback in info["update_callbacks"]:
|
||||||
|
self.hass.loop.call_soon_threadsafe(update_callback, data)
|
||||||
|
|
||||||
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
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
|
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.const import ATTR_ATTRIBUTION
|
|
||||||
from homeassistant.core import callback
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -22,8 +21,8 @@ SENSOR_TYPES = {
|
|||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the Ring binary sensors from a config entry."""
|
"""Set up the Ring binary sensors from a config entry."""
|
||||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
ring = hass.data[DOMAIN][config_entry.entry_id]["api"]
|
||||||
devices = ring.devices()
|
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||||
|
|
||||||
sensors = []
|
sensors = []
|
||||||
|
|
||||||
@ -33,49 +32,62 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for device in devices[device_type]:
|
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."""
|
"""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."""
|
"""Initialize a sensor for Ring device."""
|
||||||
self._sensor_type = sensor_type
|
super().__init__(config_entry_id, device)
|
||||||
self._ring = ring
|
self._ring = ring
|
||||||
self._device = device
|
self._sensor_type = sensor_type
|
||||||
self._name = "{0} {1}".format(
|
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._state = None
|
||||||
self._unique_id = f"{self._device.id}-{self._sensor_type}"
|
self._unique_id = f"{device.id}-{sensor_type}"
|
||||||
self._disp_disconnect = None
|
self._update_alert()
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
self._disp_disconnect = async_dispatcher_connect(
|
await super().async_added_to_hass()
|
||||||
self.hass, SIGNAL_UPDATE_RING, self._update_callback
|
self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback)
|
||||||
)
|
self._dings_update_callback()
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self):
|
||||||
"""Disconnect callbacks."""
|
"""Disconnect callbacks."""
|
||||||
if self._disp_disconnect:
|
await super().async_will_remove_from_hass()
|
||||||
self._disp_disconnect()
|
self.ring_objects["dings_data"].async_remove_listener(
|
||||||
self._disp_disconnect = None
|
self._dings_update_callback
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_callback(self):
|
def _dings_update_callback(self):
|
||||||
"""Call update method."""
|
"""Call update method."""
|
||||||
self.async_schedule_update_ha_state(True)
|
self._update_alert()
|
||||||
_LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name)
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@property
|
@callback
|
||||||
def should_poll(self):
|
def _update_alert(self):
|
||||||
"""Return False, updates are controlled via the hub."""
|
"""Update active alert."""
|
||||||
return False
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -85,7 +97,7 @@ class RingBinarySensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return True if the binary sensor is on."""
|
"""Return True if the binary sensor is on."""
|
||||||
return self._state
|
return self._active_alert is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
@ -97,32 +109,17 @@ class RingBinarySensor(BinarySensorDevice):
|
|||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
return self._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
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
attrs = {}
|
attrs = super().device_state_attributes
|
||||||
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
|
|
||||||
|
|
||||||
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
|
return attrs
|
||||||
|
|
||||||
async def async_update(self):
|
attrs["state"] = self._active_alert["state"]
|
||||||
"""Get the latest data and updates the state."""
|
attrs["expires_at"] = datetime.fromtimestamp(
|
||||||
self._state = any(
|
self._active_alert.get("now") + self._active_alert.get("expires_in")
|
||||||
alert["kind"] == self._sensor_type
|
).isoformat()
|
||||||
and alert["doorbot_id"] == self._device.id
|
|
||||||
for alert in self._ring.active_alerts()
|
return attrs
|
||||||
)
|
|
||||||
|
@ -12,10 +12,10 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG
|
|||||||
from homeassistant.const import ATTR_ATTRIBUTION
|
from homeassistant.const import ATTR_ATTRIBUTION
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
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 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)
|
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):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up a Ring Door Bell and StickUp Camera."""
|
"""Set up a Ring Door Bell and StickUp Camera."""
|
||||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||||
devices = ring.devices()
|
|
||||||
|
|
||||||
cams = []
|
cams = []
|
||||||
for camera in chain(
|
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))
|
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."""
|
"""An implementation of a Ring Door Bell camera."""
|
||||||
|
|
||||||
def __init__(self, config_entry_id, ffmpeg, device):
|
def __init__(self, config_entry_id, ffmpeg, device):
|
||||||
"""Initialize a Ring Door Bell camera."""
|
"""Initialize a Ring Door Bell camera."""
|
||||||
super().__init__()
|
super().__init__(config_entry_id, device)
|
||||||
self._config_entry_id = config_entry_id
|
|
||||||
self._device = device
|
|
||||||
self._name = self._device.name
|
self._name = self._device.name
|
||||||
self._ffmpeg = ffmpeg
|
self._ffmpeg = ffmpeg
|
||||||
|
self._last_event = None
|
||||||
self._last_video_id = None
|
self._last_video_id = None
|
||||||
self._video_url = None
|
self._video_url = None
|
||||||
self._utcnow = dt_util.utcnow()
|
self._utcnow = dt_util.utcnow()
|
||||||
self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL
|
self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL
|
||||||
self._disp_disconnect = None
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
self._disp_disconnect = async_dispatcher_connect(
|
await super().async_added_to_hass()
|
||||||
self.hass, SIGNAL_UPDATE_RING, self._update_callback
|
|
||||||
|
await self.ring_objects["history_data"].async_track_device(
|
||||||
|
self._device, self._history_update_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self):
|
async def async_will_remove_from_hass(self):
|
||||||
"""Disconnect callbacks."""
|
"""Disconnect callbacks."""
|
||||||
if self._disp_disconnect:
|
await super().async_will_remove_from_hass()
|
||||||
self._disp_disconnect()
|
|
||||||
self._disp_disconnect = None
|
self.ring_objects["history_data"].async_untrack_device(
|
||||||
|
self._device, self._history_update_callback
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_callback(self):
|
def _history_update_callback(self, history_data):
|
||||||
"""Call update method."""
|
"""Call update method."""
|
||||||
|
if history_data:
|
||||||
|
self._last_event = history_data[0]
|
||||||
self.async_schedule_update_ha_state(True)
|
self.async_schedule_update_ha_state(True)
|
||||||
_LOGGER.debug("Updating Ring camera %s (callback)", self.name)
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -83,16 +92,6 @@ class RingCam(Camera):
|
|||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
return self._device.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
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
@ -104,7 +103,6 @@ class RingCam(Camera):
|
|||||||
|
|
||||||
async def async_camera_image(self):
|
async def async_camera_image(self):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
|
|
||||||
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
|
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
|
||||||
|
|
||||||
if self._video_url is None:
|
if self._video_url is None:
|
||||||
@ -136,33 +134,23 @@ class RingCam(Camera):
|
|||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Update camera entity and refresh attributes."""
|
"""Update camera entity and refresh attributes."""
|
||||||
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
|
if self._last_event is None:
|
||||||
|
|
||||||
self._utcnow = dt_util.utcnow()
|
|
||||||
|
|
||||||
data = await self.hass.data[DATA_HISTORY].async_get_history(
|
|
||||||
self._config_entry_id, self._device
|
|
||||||
)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
last_event = data[0]
|
if self._last_event["recording"]["status"] != "ready":
|
||||||
last_recording_id = last_event["id"]
|
return
|
||||||
video_status = last_event["recording"]["status"]
|
|
||||||
|
|
||||||
if video_status == "ready" and (
|
if (
|
||||||
self._last_video_id != last_recording_id or self._utcnow >= self._expires_at
|
self._last_video_id == self._last_event["id"]
|
||||||
|
and self._utcnow <= self._expires_at
|
||||||
):
|
):
|
||||||
|
return
|
||||||
|
|
||||||
video_url = await self.hass.async_add_executor_job(
|
video_url = await self.hass.async_add_executor_job(
|
||||||
self._device.recording_url, last_recording_id
|
self._device.recording_url, self._last_event["id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if video_url:
|
if video_url:
|
||||||
_LOGGER.debug("Ring DoorBell properties refreshed")
|
self._last_video_id = self._last_event["id"]
|
||||||
|
|
||||||
# update attributes if new video or if URL has expired
|
|
||||||
self._last_video_id = last_recording_id
|
|
||||||
self._video_url = video_url
|
self._video_url = video_url
|
||||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
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):
|
async def async_step_user(self, user_input=None):
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
if self._async_current_entries():
|
|
||||||
return self.async_abort(reason="already_configured")
|
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
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.components.light import Light
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import DOMAIN, SIGNAL_UPDATE_RING
|
from . import DOMAIN
|
||||||
|
from .entity import RingEntityMixin
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -25,51 +25,35 @@ OFF_STATE = "off"
|
|||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Create the lights for the Ring devices."""
|
"""Create the lights for the Ring devices."""
|
||||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||||
|
|
||||||
devices = ring.devices()
|
|
||||||
lights = []
|
lights = []
|
||||||
|
|
||||||
for device in devices["stickup_cams"]:
|
for device in devices["stickup_cams"]:
|
||||||
if device.has_capability("light"):
|
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."""
|
"""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."""
|
"""Initialize the light."""
|
||||||
self._device = device
|
super().__init__(config_entry_id, device)
|
||||||
self._unique_id = self._device.id
|
self._unique_id = device.id
|
||||||
self._light_on = False
|
self._light_on = device.lights == ON_STATE
|
||||||
self._no_updates_until = dt_util.utcnow()
|
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
|
@callback
|
||||||
def _update_callback(self):
|
def _update_callback(self):
|
||||||
"""Call update method."""
|
"""Call update method."""
|
||||||
_LOGGER.debug("Updating Ring light %s (callback)", self.name)
|
if self._no_updates_until > dt_util.utcnow():
|
||||||
self.async_schedule_update_ha_state(True)
|
return
|
||||||
|
|
||||||
@property
|
self._light_on = self._device.lights == ON_STATE
|
||||||
def should_poll(self):
|
self.async_write_ha_state()
|
||||||
"""Update controlled via the hub."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -86,22 +70,12 @@ class RingLight(Light):
|
|||||||
"""If the switch is currently on or off."""
|
"""If the switch is currently on or off."""
|
||||||
return self._light_on
|
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):
|
def _set_light(self, new_state):
|
||||||
"""Update light state, and causes Home Assistant to correctly update."""
|
"""Update light state, and causes Home Assistant to correctly update."""
|
||||||
self._device.lights = new_state
|
self._device.lights = new_state
|
||||||
self._light_on = new_state == ON_STATE
|
self._light_on = new_state == ON_STATE
|
||||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
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):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the light on for 30 seconds."""
|
"""Turn the light on for 30 seconds."""
|
||||||
@ -110,11 +84,3 @@ class RingLight(Light):
|
|||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the light off."""
|
"""Turn the light off."""
|
||||||
self._set_light(OFF_STATE)
|
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."""
|
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import ATTR_ATTRIBUTION
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.icon import icon_for_battery_level
|
from homeassistant.helpers.icon import icon_for_battery_level
|
||||||
|
|
||||||
from . import (
|
from . import DOMAIN
|
||||||
ATTRIBUTION,
|
from .entity import RingEntityMixin
|
||||||
DATA_HEALTH_DATA_TRACKER,
|
|
||||||
DATA_HISTORY,
|
|
||||||
DOMAIN,
|
|
||||||
SIGNAL_UPDATE_HEALTH_RING,
|
|
||||||
SIGNAL_UPDATE_RING,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up a sensor for a Ring device."""
|
"""Set up a sensor for a Ring device."""
|
||||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||||
devices = ring.devices()
|
|
||||||
# Makes a ton of requests. We will make this a config entry option in the future
|
# Makes a ton of requests. We will make this a config entry option in the future
|
||||||
wifi_enabled = False
|
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:
|
if device_type == "battery" and device.battery_life is None:
|
||||||
continue
|
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."""
|
"""A sensor implementation for Ring device."""
|
||||||
|
|
||||||
def __init__(self, config_entry_id, device, sensor_type):
|
def __init__(self, config_entry_id, device, sensor_type):
|
||||||
"""Initialize a sensor for Ring device."""
|
"""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._sensor_type = sensor_type
|
||||||
self._device = device
|
|
||||||
self._extra = None
|
self._extra = None
|
||||||
self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3])
|
self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3])
|
||||||
self._kind = SENSOR_TYPES.get(self._sensor_type)[4]
|
self._kind = SENSOR_TYPES.get(sensor_type)[4]
|
||||||
self._name = "{0} {1}".format(
|
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"{device.id}-{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
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -180,7 +69,11 @@ class RingSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""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
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
@ -192,37 +85,12 @@ class RingSensor(Entity):
|
|||||||
"""Return sensor device class."""
|
"""Return sensor device class."""
|
||||||
return SENSOR_TYPES[self._sensor_type][5]
|
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
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
"""Icon to use in the frontend, if any."""
|
"""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(
|
return icon_for_battery_level(
|
||||||
battery_level=int(self._state), charging=False
|
battery_level=self._device.battery_life, charging=False
|
||||||
)
|
)
|
||||||
return self._icon
|
return self._icon
|
||||||
|
|
||||||
@ -231,34 +99,168 @@ class RingSensor(Entity):
|
|||||||
"""Return the units of measurement."""
|
"""Return the units of measurement."""
|
||||||
return SENSOR_TYPES.get(self._sensor_type)[2]
|
return SENSOR_TYPES.get(self._sensor_type)[2]
|
||||||
|
|
||||||
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":
|
class HealthDataRingSensor(RingSensor):
|
||||||
self._state = self._device.volume
|
"""Ring sensor that relies on health data."""
|
||||||
|
|
||||||
if self._sensor_type == "battery":
|
async def async_added_to_hass(self):
|
||||||
self._state = self._device.battery_life
|
"""Register callbacks."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
if self._sensor_type.startswith("last_"):
|
await self.ring_objects["health_data"].async_track_device(
|
||||||
history = await self.hass.data[DATA_HISTORY].async_get_history(
|
self._device, self._health_update_callback
|
||||||
self._config_entry_id, self._device
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
found = None
|
||||||
for entry in history:
|
if self._kind is None:
|
||||||
|
found = history_data[0]
|
||||||
|
else:
|
||||||
|
for entry in history_data:
|
||||||
if entry["kind"] == self._kind:
|
if entry["kind"] == self._kind:
|
||||||
found = entry
|
found = entry
|
||||||
break
|
break
|
||||||
|
|
||||||
if found:
|
if not found:
|
||||||
self._extra = found
|
return
|
||||||
created_at = found["created_at"]
|
|
||||||
self._state = created_at.isoformat()
|
|
||||||
|
|
||||||
if self._sensor_type == "wifi_signal_category":
|
self._latest_event = found
|
||||||
self._state = self._device.wifi_signal_category
|
self.async_write_ha_state()
|
||||||
|
|
||||||
if self._sensor_type == "wifi_signal_strength":
|
@property
|
||||||
self._state = self._device.wifi_signal_strength
|
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.components.switch import SwitchDevice
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import DOMAIN, SIGNAL_UPDATE_RING
|
from . import DOMAIN
|
||||||
|
from .entity import RingEntityMixin
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Create the switches for the Ring devices."""
|
"""Create the switches for the Ring devices."""
|
||||||
ring = hass.data[DOMAIN][config_entry.entry_id]
|
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||||
devices = ring.devices()
|
|
||||||
switches = []
|
switches = []
|
||||||
|
|
||||||
for device in devices["stickup_cams"]:
|
for device in devices["stickup_cams"]:
|
||||||
if device.has_capability("siren"):
|
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."""
|
"""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."""
|
"""Initialize the switch."""
|
||||||
self._device = device
|
super().__init__(config_entry_id, device)
|
||||||
self._device_type = device_type
|
self._device_type = device_type
|
||||||
self._unique_id = f"{self._device.id}-{self._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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -78,25 +53,24 @@ class BaseRingSwitch(SwitchDevice):
|
|||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
return self._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):
|
class SirenSwitch(BaseRingSwitch):
|
||||||
"""Creates a switch to turn the ring cameras siren on and off."""
|
"""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."""
|
"""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._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):
|
def _set_switch(self, new_state):
|
||||||
"""Update switch state, and causes Home Assistant to correctly update."""
|
"""Update switch state, and causes Home Assistant to correctly update."""
|
||||||
@ -122,10 +96,3 @@ class SirenSwitch(BaseRingSwitch):
|
|||||||
def icon(self):
|
def icon(self):
|
||||||
"""Return the icon."""
|
"""Return the icon."""
|
||||||
return SIREN_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."""
|
"""The tests for the Ring binary sensor platform."""
|
||||||
|
from time import time
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from .common import setup_platform
|
from .common import setup_platform
|
||||||
@ -8,7 +9,15 @@ async def test_binary_sensor(hass, requests_mock):
|
|||||||
"""Test the Ring binary sensors."""
|
"""Test the Ring binary sensors."""
|
||||||
with patch(
|
with patch(
|
||||||
"ring_doorbell.Ring.active_alerts",
|
"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")
|
await setup_platform(hass, "binary_sensor")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user