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 Sep 3, 2024
1 parent e3896d1 commit cd1b355
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 76 deletions.
38 changes: 27 additions & 11 deletions homeassistant/components/ring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@
from ring_doorbell import Auth, Ring, RingDevices

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__
from homeassistant.const import APPLICATION_NAME, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
instance_id,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue

from .const import DOMAIN, PLATFORMS
from .coordinator import RingDataCoordinator, RingNotificationsCoordinator
from .const import CONF_LISTEN_CREDENTIALS, DOMAIN, PLATFORMS
from .coordinator import RingDataCoordinator, RingListenCoordinator

_LOGGER = logging.getLogger(__name__)

Expand All @@ -28,7 +32,7 @@ class RingData:
api: Ring
devices: RingDevices
devices_coordinator: RingDataCoordinator
notifications_coordinator: RingNotificationsCoordinator
listen_coordinator: RingListenCoordinator


type RingConfigEntry = ConfigEntry[RingData]
Expand All @@ -44,26 +48,39 @@ def token_updater(token: dict[str, Any]) -> None:
data={**entry.data, CONF_TOKEN: token},
)

def listen_credentials_updater(token: dict[str, Any]) -> None:
"""Handle from async context when token is updated."""
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_LISTEN_CREDENTIALS: token},
)

hardware_id = await instance_id.async_get(hass)
client_session = async_get_clientsession(hass)
auth = Auth(
f"{APPLICATION_NAME}/{__version__}",
f"{APPLICATION_NAME}/{DOMAIN}-integration",
entry.data[CONF_TOKEN],
token_updater,
http_client_session=async_get_clientsession(hass),
hardware_id=hardware_id,
http_client_session=client_session,
)
ring = Ring(auth)

await _migrate_old_unique_ids(hass, entry.entry_id)

devices_coordinator = RingDataCoordinator(hass, ring)
notifications_coordinator = RingNotificationsCoordinator(hass, ring)
listen_credentials = entry.data.get(CONF_LISTEN_CREDENTIALS)
listen_coordinator = RingListenCoordinator(
hass, ring, listen_credentials, listen_credentials_updater
)

await devices_coordinator.async_config_entry_first_refresh()
await notifications_coordinator.async_config_entry_first_refresh()

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 Down Expand Up @@ -91,7 +108,6 @@ async def async_refresh_all(_: ServiceCall) -> None:
)
for loaded_entry in hass.config_entries.async_loaded_entries(DOMAIN):
await loaded_entry.runtime_data.devices_coordinator.async_refresh()
await loaded_entry.runtime_data.notifications_coordinator.async_refresh()

# register service
hass.services.async_register(DOMAIN, "update", async_refresh_all)
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/ring/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
DEFAULT_ENTITY_NAMESPACE = "ring"

PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CAMERA,
Platform.EVENT,
Platform.LIGHT,
Platform.SENSOR,
Platform.SIREN,
Expand All @@ -26,6 +26,7 @@


SCAN_INTERVAL = timedelta(minutes=1)
NOTIFICATIONS_SCAN_INTERVAL = timedelta(seconds=5)
SESSION_REFRESH_INTERVAL = timedelta(minutes=10)

CONF_2FA = "2fa"
CONF_LISTEN_CREDENTIALS = "listen_token"
137 changes: 118 additions & 19 deletions homeassistant/components/ring/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,28 @@
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

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
from .const import SCAN_INTERVAL

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -91,19 +104,105 @@ async def _async_update_data(self) -> RingDevices:
return devices


class RingNotificationsCoordinator(DataUpdateCoordinator[None]):
class RingListenCoordinator(BaseDataUpdateCoordinatorProtocol):
"""Global notifications coordinator."""

def __init__(self, hass: HomeAssistant, ring_api: Ring) -> None:
config_entry: config_entries.ConfigEntry

def __init__(
self,
hass: HomeAssistant,
ring_api: Ring,
listen_credentials: dict[str, Any] | None,
listen_credentials_updater: Callable[[dict[str, Any]], None],
) -> None:
"""Initialize my coordinator."""
super().__init__(
hass,
logger=_LOGGER,
name="active dings",
update_interval=NOTIFICATIONS_SCAN_INTERVAL,
)
self.hass = hass
self.logger = _LOGGER
self.ring_api: Ring = ring_api

async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await _call_api(self.hass, self.ring_api.async_update_dings)
self.event_listener = RingEventListener(
ring_api, listen_credentials, listen_credentials_updater
)
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)
self.alerts = ring_api.active_alerts()

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")
await 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.")
await self.event_listener.start(
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
)
self.alerts = self.ring_api.active_alerts()
# Update the listeners so they switch from Unavailable to Unknown
self._async_update_listeners()

def _on_event(self, event: RingEvent) -> None:
self.logger.debug("Ring event received: %s", event)
self.alerts = self.ring_api.active_alerts()
self._async_update_listeners(event.doorbot_id)

@callback
def _async_update_listeners(self, doorbot_id: int | None = None) -> None:
"""Update all registered listeners."""
for update_callback, device_api_id in list(self._listeners.values()):
if not doorbot_id or device_api_id == doorbot_id:
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
13 changes: 8 additions & 5 deletions homeassistant/components/ring/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@
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

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

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


Expand Down Expand Up @@ -52,7 +55,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 +80,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 cd1b355

Please sign in to comment.