Skip to content

Commit

Permalink
Add coordinator to ring integration (#107088)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdb9696 authored Jan 31, 2024
1 parent 7fe4a34 commit f725258
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 388 deletions.
229 changes: 10 additions & 219 deletions homeassistant/components/ring/__init__.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@
"""Support for Ring Doorbell/Chimes."""
from __future__ import annotations

import asyncio
from collections.abc import Callable
from datetime import timedelta
from functools import partial
import logging
from typing import Any

import ring_doorbell

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.event import async_track_time_interval

from .const import (
DEVICES_SCAN_INTERVAL,
DOMAIN,
HEALTH_SCAN_INTERVAL,
HISTORY_SCAN_INTERVAL,
NOTIFICATIONS_SCAN_INTERVAL,
PLATFORMS,
RING_API,
RING_DEVICES,
RING_DEVICES_COORDINATOR,
RING_HEALTH_COORDINATOR,
RING_HISTORY_COORDINATOR,
RING_NOTIFICATIONS_COORDINATOR,
)
from .coordinator import RingDataCoordinator, RingNotificationsCoordinator

_LOGGER = logging.getLogger(__name__)

Expand All @@ -53,42 +42,16 @@ def token_updater(token):
)
ring = ring_doorbell.Ring(auth)

try:
await hass.async_add_executor_job(ring.update_data)
except ring_doorbell.AuthenticationError as err:
_LOGGER.warning("Ring access token is no longer valid, need to re-authenticate")
raise ConfigEntryAuthFailed(err) from err
devices_coordinator = RingDataCoordinator(hass, ring)
notifications_coordinator = RingNotificationsCoordinator(hass, ring)
await devices_coordinator.async_config_entry_first_refresh()
await notifications_coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
RING_API: ring,
RING_DEVICES: ring.devices(),
RING_DEVICES_COORDINATOR: GlobalDataUpdater(
hass, "device", entry, ring, "update_devices", DEVICES_SCAN_INTERVAL
),
RING_NOTIFICATIONS_COORDINATOR: GlobalDataUpdater(
hass,
"active dings",
entry,
ring,
"update_dings",
NOTIFICATIONS_SCAN_INTERVAL,
),
RING_HISTORY_COORDINATOR: DeviceDataUpdater(
hass,
"history",
entry,
ring,
lambda device: device.history(limit=10),
HISTORY_SCAN_INTERVAL,
),
RING_HEALTH_COORDINATOR: DeviceDataUpdater(
hass,
"health",
entry,
ring,
lambda device: device.update_health_data(),
HEALTH_SCAN_INTERVAL,
),
RING_DEVICES_COORDINATOR: devices_coordinator,
RING_NOTIFICATIONS_COORDINATOR: notifications_coordinator,
}

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
Expand All @@ -99,10 +62,8 @@ def token_updater(token):
async def async_refresh_all(_: ServiceCall) -> None:
"""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)
await info[RING_DEVICES_COORDINATOR].async_refresh()
await info[RING_NOTIFICATIONS_COORDINATOR].async_refresh()

# register service
hass.services.async_register(DOMAIN, "update", async_refresh_all)
Expand Down Expand Up @@ -131,173 +92,3 @@ async def async_remove_config_entry_device(
) -> bool:
"""Remove a config entry from a device."""
return True


class GlobalDataUpdater:
"""Data storage for single API endpoint."""

def __init__(
self,
hass: HomeAssistant,
data_type: str,
config_entry: ConfigEntry,
ring: ring_doorbell.Ring,
update_method: str,
update_interval: timedelta,
) -> None:
"""Initialize global data updater."""
self.hass = hass
self.data_type = data_type
self.config_entry = config_entry
self.ring = ring
self.update_method = update_method
self.update_interval = update_interval
self.listeners: list[Callable[[], None]] = []
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: int | None = 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 ring_doorbell.AuthenticationError:
_LOGGER.warning(
"Ring access token is no longer valid, need to re-authenticate"
)
self.config_entry.async_start_reauth(self.hass)
return
except ring_doorbell.RingTimeout:
_LOGGER.warning(
"Time out fetching Ring %s data",
self.data_type,
)
return
except ring_doorbell.RingError as err:
_LOGGER.warning(
"Error fetching Ring %s data: %s",
self.data_type,
err,
)
return

for update_callback in self.listeners:
update_callback()


class DeviceDataUpdater:
"""Data storage for device data."""

def __init__(
self,
hass: HomeAssistant,
data_type: str,
config_entry: ConfigEntry,
ring: ring_doorbell.Ring,
update_method: Callable[[ring_doorbell.Ring], Any],
update_interval: timedelta,
) -> None:
"""Initialize device data updater."""
self.data_type = data_type
self.hass = hass
self.config_entry = config_entry
self.ring = ring
self.update_method = update_method
self.update_interval = update_interval
self.devices: dict = {}
self._unsub_interval = None

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, self.update_interval
)

if device.device_id not in self.devices:
self.devices[device.device_id] = {
"device": device,
"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[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"]

update_callback(self.devices[device.device_id]["data"])

@callback
def async_untrack_device(self, device, update_callback):
"""Untrack a device."""
self.devices[device.device_id]["update_callbacks"].remove(update_callback)

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, _=None):
"""Refresh all registered devices."""
for device_id, info in self.devices.items():
try:
data = info["data"] = self.update_method(info["device"])
except ring_doorbell.AuthenticationError:
_LOGGER.warning(
"Ring access token is no longer valid, need to re-authenticate"
)
self.hass.loop.call_soon_threadsafe(
self.config_entry.async_start_reauth, self.hass
)
return
except ring_doorbell.RingTimeout:
_LOGGER.warning(
"Time out fetching Ring %s data for device %s",
self.data_type,
device_id,
)
continue
except ring_doorbell.RingError as err:
_LOGGER.warning(
"Error fetching Ring %s data for device %s: %s",
self.data_type,
device_id,
err,
)
continue

for update_callback in info["update_callbacks"]:
self.hass.loop.call_soon_threadsafe(update_callback, data)
36 changes: 14 additions & 22 deletions homeassistant/components/ring/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN, RING_API, RING_DEVICES, RING_NOTIFICATIONS_COORDINATOR
from .entity import RingEntityMixin
from .coordinator import RingNotificationsCoordinator
from .entity import RingEntity


@dataclass(frozen=True)
Expand Down Expand Up @@ -55,9 +56,12 @@ async def async_setup_entry(
"""Set up the Ring binary sensors from a config entry."""
ring = hass.data[DOMAIN][config_entry.entry_id][RING_API]
devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES]
notifications_coordinator: RingNotificationsCoordinator = hass.data[DOMAIN][
config_entry.entry_id
][RING_NOTIFICATIONS_COORDINATOR]

entities = [
RingBinarySensor(config_entry.entry_id, ring, device, description)
RingBinarySensor(ring, device, notifications_coordinator, description)
for device_type in ("doorbots", "authorized_doorbots", "stickup_cams")
for description in BINARY_SENSOR_TYPES
if device_type in description.category
Expand All @@ -67,46 +71,34 @@ async def async_setup_entry(
async_add_entities(entities)


class RingBinarySensor(RingEntityMixin, BinarySensorEntity):
class RingBinarySensor(RingEntity, BinarySensorEntity):
"""A binary sensor implementation for Ring device."""

_active_alert: dict[str, Any] | None = None
entity_description: RingBinarySensorEntityDescription

def __init__(
self,
config_entry_id,
ring,
device,
coordinator,
description: RingBinarySensorEntityDescription,
) -> None:
"""Initialize a sensor for Ring device."""
super().__init__(config_entry_id, device)
super().__init__(
device,
coordinator,
)
self.entity_description = description
self._ring = ring
self._attr_unique_id = f"{device.id}-{description.key}"
self._update_alert()

async def async_added_to_hass(self) -> None:
"""Register callbacks."""
await super().async_added_to_hass()
self.ring_objects[RING_NOTIFICATIONS_COORDINATOR].async_add_listener(
self._dings_update_callback
)
self._dings_update_callback()

async def async_will_remove_from_hass(self) -> None:
"""Disconnect callbacks."""
await super().async_will_remove_from_hass()
self.ring_objects[RING_NOTIFICATIONS_COORDINATOR].async_remove_listener(
self._dings_update_callback
)

@callback
def _dings_update_callback(self):
def _handle_coordinator_update(self, _=None):
"""Call update method."""
self._update_alert()
self.async_write_ha_state()
super()._handle_coordinator_update()

@callback
def _update_alert(self):
Expand Down
Loading

0 comments on commit f725258

Please sign in to comment.