Skip to content

Commit

Permalink
Enable ring event listener to fix missing notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
sdb9696 committed Aug 24, 2024
1 parent 5ef12c6 commit 7c7615f
Show file tree
Hide file tree
Showing 15 changed files with 413 additions and 52 deletions.
24 changes: 17 additions & 7 deletions homeassistant/components/ring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

from .const import DOMAIN, PLATFORMS
from .coordinator import RingDataCoordinator, RingNotificationsCoordinator
from .coordinator import (
RingDataCoordinator,
RingListenCoordinator,
RingNotificationsCoordinator,
)

_LOGGER = logging.getLogger(__name__)

Expand All @@ -29,9 +33,13 @@ class RingData:
devices: RingDevices
devices_coordinator: RingDataCoordinator
notifications_coordinator: RingNotificationsCoordinator
listen_coordinator: RingListenCoordinator


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
type RingConfigEntry = ConfigEntry[RingData]


async def async_setup_entry(hass: HomeAssistant, entry: RingConfigEntry) -> bool:
"""Set up a config entry."""

def token_updater(token: dict[str, Any]) -> None:
Expand All @@ -53,14 +61,17 @@ def token_updater(token: dict[str, Any]) -> None:

devices_coordinator = RingDataCoordinator(hass, ring)
notifications_coordinator = RingNotificationsCoordinator(hass, ring)
listen_coordinator = RingListenCoordinator(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] = RingData(
entry.runtime_data = RingData(
api=ring,
devices=ring.devices(),
devices_coordinator=devices_coordinator,
notifications_coordinator=notifications_coordinator,
listen_coordinator=listen_coordinator,
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
Expand All @@ -87,10 +98,9 @@ async def async_refresh_all(_: ServiceCall) -> None:
translation_key="deprecated_service_ring_update",
)

for info in hass.data[DOMAIN].values():
ring_data = cast(RingData, info)
await ring_data.devices_coordinator.async_refresh()
await ring_data.notifications_coordinator.async_refresh()
ring_data = entry.runtime_data
await ring_data.devices_coordinator.async_refresh()
await ring_data.notifications_coordinator.async_refresh()

# register service
hass.services.async_register(DOMAIN, "update", async_refresh_all)
Expand Down
11 changes: 6 additions & 5 deletions homeassistant/components/ring/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import RingData
from .const import DOMAIN
from . import RingConfigEntry
from .coordinator import RingNotificationsCoordinator
from .entity import RingBaseEntity

Expand Down Expand Up @@ -50,11 +48,11 @@ class RingBinarySensorEntityDescription(BinarySensorEntityDescription):

async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RingConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Ring binary sensors from a config entry."""
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
ring_data = config_entry.runtime_data

entities = [
RingBinarySensor(
Expand Down Expand Up @@ -136,3 +134,6 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
attrs["expires_at"] = datetime.fromtimestamp(now + expires_in).isoformat()

return attrs

async def async_update(self) -> None:
"""All updates are passive."""
8 changes: 3 additions & 5 deletions homeassistant/components/ring/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
from ring_doorbell import RingOther

from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import RingData
from .const import DOMAIN
from . import RingConfigEntry
from .coordinator import RingDataCoordinator
from .entity import RingEntity, exception_wrap

Expand All @@ -21,11 +19,11 @@

async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RingConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Create the buttons for the Ring devices."""
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
ring_data = config_entry.runtime_data
devices_coordinator = ring_data.devices_coordinator

async_add_entities(
Expand Down
8 changes: 3 additions & 5 deletions homeassistant/components/ring/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@

from homeassistant.components import ffmpeg
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util

from . import RingData
from .const import DOMAIN
from . import RingConfigEntry
from .coordinator import RingDataCoordinator
from .entity import RingEntity, exception_wrap

Expand All @@ -31,11 +29,11 @@

async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RingConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a Ring Door Bell and StickUp Camera."""
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
ring_data = config_entry.runtime_data
devices_coordinator = ring_data.devices_coordinator
ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass)

Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/ring/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CAMERA,
Platform.EVENT,
Platform.LIGHT,
Platform.SENSOR,
Platform.SIREN,
Expand Down
121 changes: 115 additions & 6 deletions homeassistant/components/ring/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@
from asyncio import TaskGroup
from collections.abc import Callable, Coroutine
import logging
from typing import Any

from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout

from homeassistant.core import HomeAssistant
from typing import TYPE_CHECKING, Any

from ring_doorbell import (
AuthenticationError,
Ring,
RingDevices,
RingError,
RingEvent,
RingTimeout,
)
from ring_doorbell.listen import RingEventListener, RingEventListenerConfig

from homeassistant import config_entries
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.update_coordinator import (
BaseDataUpdateCoordinatorProtocol,
DataUpdateCoordinator,
UpdateFailed,
)

from .const import NOTIFICATIONS_SCAN_INTERVAL, SCAN_INTERVAL

Expand Down Expand Up @@ -107,3 +120,99 @@ def __init__(self, hass: HomeAssistant, ring_api: Ring) -> None:
async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await _call_api(self.hass, self.ring_api.async_update_dings)


class RingListenCoordinator(BaseDataUpdateCoordinatorProtocol):
"""Global notifications coordinator."""

config_entry: config_entries.ConfigEntry

def __init__(self, hass: HomeAssistant, ring_api: Ring) -> None:
"""Initialize my coordinator."""
self.hass = hass
self.logger = _LOGGER
self.ring_api: Ring = ring_api
self.config = RingEventListenerConfig.default_config()
self.event_listener = RingEventListener(ring_api, config=self.config)
self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}
self._listen_callback_id: int | None = None

config_entry = config_entries.current_entry.get()
if TYPE_CHECKING:
assert config_entry
self.config_entry = config_entry
self.start_timeout = 10
self.config_entry.async_on_unload(self.async_shutdown)

async def async_shutdown(self) -> None:
"""Cancel any scheduled call, and ignore new runs."""
if self.event_listener.started:
await self._async_stop_listen()

async def _async_stop_listen(self) -> None:
self.logger.debug("Stopped ring listener")
self.event_listener.stop()
self.logger.debug("Stopped ring listener")

async def _async_start_listen(self) -> None:
"""Start listening for realtime events."""
self.logger.debug("Starting ring listener with config: %s", self.config)
await self.event_listener.async_start(
listen_loop=self.hass.loop,
callback_loop=self.hass.loop,
timeout=self.start_timeout,
)
if self.event_listener.started is True:
self.logger.debug("Started ring listener")
else:
self.logger.warning(
"Ring event listener failed to start after %s seconds",
self.start_timeout,
)
self._listen_callback_id = self.event_listener.add_notification_callback(
self._on_event
)
await _call_api(self.hass, self.ring_api.update_dings)
self._async_update_listeners()

def _on_event(self, event: RingEvent) -> None:
self.logger.debug("Ring event received: %s", event)
self._async_update_listeners()
self.hass.loop.call_later(event.expires_in, self._async_update_listeners)

@callback
def _async_update_listeners(self) -> None:
"""Update all registered listeners."""
for update_callback, _ in list(self._listeners.values()):
update_callback()

@callback
def async_add_listener(
self, update_callback: CALLBACK_TYPE, context: Any = None
) -> Callable[[], None]:
"""Listen for data updates."""
start_listen = not self._listeners

@callback
def remove_listener() -> None:
"""Remove update listener."""
self._listeners.pop(remove_listener)
if not self._listeners:
self.config_entry.async_create_task(
self.hass,
self._async_stop_listen(),
"Ring event listener stop",
eager_start=True,
)

self._listeners[remove_listener] = (update_callback, context)

# This is the first listener, start the event listener.
if start_listen:
self.config_entry.async_create_task(
self.hass,
self._async_start_listen(),
"Ring event listener start",
eager_start=True,
)
return remove_listener
17 changes: 12 additions & 5 deletions homeassistant/components/ring/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.update_coordinator import (
BaseCoordinatorEntity,
CoordinatorEntity,
)

from .const import ATTRIBUTION, DOMAIN
from .coordinator import RingDataCoordinator, RingNotificationsCoordinator
from .coordinator import (
RingDataCoordinator,
RingListenCoordinator,
RingNotificationsCoordinator,
)

RingDeviceT = TypeVar("RingDeviceT", bound=RingGeneric, default=RingGeneric)

_RingCoordinatorT = TypeVar(
"_RingCoordinatorT",
bound=(RingDataCoordinator | RingNotificationsCoordinator),
bound=(RingDataCoordinator | RingNotificationsCoordinator | RingListenCoordinator),
)


Expand Down Expand Up @@ -52,7 +59,7 @@ async def _wrap(self: _RingBaseEntityT, *args: _P.args, **kwargs: _P.kwargs) ->


class RingBaseEntity(
CoordinatorEntity[_RingCoordinatorT], Generic[_RingCoordinatorT, RingDeviceT]
BaseCoordinatorEntity[_RingCoordinatorT], Generic[_RingCoordinatorT, RingDeviceT]
):
"""Base implementation for Ring device."""

Expand All @@ -77,7 +84,7 @@ def __init__(
)


class RingEntity(RingBaseEntity[RingDataCoordinator, RingDeviceT]):
class RingEntity(RingBaseEntity[RingDataCoordinator, RingDeviceT], CoordinatorEntity):
"""Implementation for Ring devices."""

def _get_coordinator_data(self) -> RingDevices:
Expand Down
Loading

0 comments on commit 7c7615f

Please sign in to comment.