Add coordinator to ring integration (#107088)
parent
7fe4a343f9
commit
f725258ea9
|
@ -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__)
|
||||
|
||||
|
@ -53,42 +42,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
)
|
||||
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)
|
||||
|
@ -99,10 +62,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -15,7 +15,8 @@ from homeassistant.core import HomeAssistant, callback
|
|||
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)
|
||||
|
@ -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
|
||||
|
@ -67,7 +71,7 @@ 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
|
||||
|
@ -75,38 +79,26 @@ class RingBinarySensor(RingEntityMixin, BinarySensorEntity):
|
|||
|
||||
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):
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
from datetime import timedelta
|
||||
from itertools import chain
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
import requests
|
||||
|
@ -16,8 +17,9 @@ 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 .const import DOMAIN, RING_DEVICES, RING_HISTORY_COORDINATOR
|
||||
from .entity import RingEntityMixin
|
||||
from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity
|
||||
|
||||
FORCE_REFRESH_INTERVAL = timedelta(minutes=3)
|
||||
|
||||
|
@ -31,6 +33,9 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up a Ring Door Bell and StickUp Camera."""
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES]
|
||||
devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
RING_DEVICES_COORDINATOR
|
||||
]
|
||||
ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass)
|
||||
|
||||
cams = []
|
||||
|
@ -40,19 +45,20 @@ async def async_setup_entry(
|
|||
if not camera.has_subscription:
|
||||
continue
|
||||
|
||||
cams.append(RingCam(config_entry.entry_id, ffmpeg_manager, camera))
|
||||
cams.append(RingCam(camera, devices_coordinator, ffmpeg_manager))
|
||||
|
||||
async_add_entities(cams)
|
||||
|
||||
|
||||
class RingCam(RingEntityMixin, Camera):
|
||||
class RingCam(RingEntity, Camera):
|
||||
"""An implementation of a Ring Door Bell camera."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, config_entry_id, ffmpeg_manager, device):
|
||||
def __init__(self, device, coordinator, ffmpeg_manager):
|
||||
"""Initialize a Ring Door Bell camera."""
|
||||
super().__init__(config_entry_id, device)
|
||||
super().__init__(device, coordinator)
|
||||
Camera.__init__(self)
|
||||
|
||||
self._ffmpeg_manager = ffmpeg_manager
|
||||
self._last_event = None
|
||||
|
@ -62,25 +68,12 @@ class RingCam(RingEntityMixin, Camera):
|
|||
self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL
|
||||
self._attr_unique_id = device.id
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self.ring_objects[RING_HISTORY_COORDINATOR].async_track_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects[RING_HISTORY_COORDINATOR].async_untrack_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _history_update_callback(self, history_data):
|
||||
def _handle_coordinator_update(self):
|
||||
"""Call update method."""
|
||||
history_data: Optional[list]
|
||||
if not (history_data := self._get_coordinator_history()):
|
||||
return
|
||||
if history_data:
|
||||
self._last_event = history_data[0]
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
|
|
@ -23,17 +23,13 @@ PLATFORMS = [
|
|||
]
|
||||
|
||||
|
||||
DEVICES_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
NOTIFICATIONS_SCAN_INTERVAL = timedelta(seconds=5)
|
||||
HISTORY_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
HEALTH_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
RING_API = "api"
|
||||
RING_DEVICES = "devices"
|
||||
|
||||
RING_DEVICES_COORDINATOR = "device_data"
|
||||
RING_NOTIFICATIONS_COORDINATOR = "dings_data"
|
||||
RING_HISTORY_COORDINATOR = "history_data"
|
||||
RING_HEALTH_COORDINATOR = "health_data"
|
||||
|
||||
CONF_2FA = "2fa"
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
"""Data coordinators for the ring integration."""
|
||||
from asyncio import TaskGroup
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
import ring_doorbell
|
||||
from ring_doorbell.generic import RingGeneric
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import NOTIFICATIONS_SCAN_INTERVAL, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _call_api(
|
||||
hass: HomeAssistant, target: Callable[..., Any], *args, msg_suffix: str = ""
|
||||
):
|
||||
try:
|
||||
return await hass.async_add_executor_job(target, *args)
|
||||
except ring_doorbell.AuthenticationError as err:
|
||||
# Raising ConfigEntryAuthFailed will cancel future updates
|
||||
# and start a config flow with SOURCE_REAUTH (async_step_reauth)
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except ring_doorbell.RingTimeout as err:
|
||||
raise UpdateFailed(
|
||||
f"Timeout communicating with API{msg_suffix}: {err}"
|
||||
) from err
|
||||
except ring_doorbell.RingError as err:
|
||||
raise UpdateFailed(f"Error communicating with API{msg_suffix}: {err}") from err
|
||||
|
||||
|
||||
@dataclass
|
||||
class RingDeviceData:
|
||||
"""RingDeviceData."""
|
||||
|
||||
device: RingGeneric
|
||||
history: Optional[list] = None
|
||||
|
||||
|
||||
class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]):
|
||||
"""Base class for device coordinators."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
ring_api: ring_doorbell.Ring,
|
||||
) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
name="devices",
|
||||
logger=_LOGGER,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.ring_api: ring_doorbell.Ring = ring_api
|
||||
self.first_call: bool = True
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Fetch data from API endpoint."""
|
||||
update_method: str = "update_data" if self.first_call else "update_devices"
|
||||
await _call_api(self.hass, getattr(self.ring_api, update_method))
|
||||
self.first_call = False
|
||||
data: dict[str, RingDeviceData] = {}
|
||||
devices: dict[str : list[RingGeneric]] = self.ring_api.devices()
|
||||
subscribed_device_ids = set(self.async_contexts())
|
||||
for device_type in devices:
|
||||
for device in devices[device_type]:
|
||||
# Don't update all devices in the ring api, only those that set
|
||||
# their device id as context when they subscribed.
|
||||
if device.id in subscribed_device_ids:
|
||||
data[device.id] = RingDeviceData(device=device)
|
||||
try:
|
||||
async with TaskGroup() as tg:
|
||||
if hasattr(device, "history"):
|
||||
history_task = tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
lambda device: device.history(limit=10),
|
||||
device,
|
||||
msg_suffix=f" for device {device.name}", # device_id is the mac
|
||||
)
|
||||
)
|
||||
tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
device.update_health_data,
|
||||
msg_suffix=f" for device {device.name}",
|
||||
)
|
||||
)
|
||||
if history_task:
|
||||
data[device.id].history = history_task.result()
|
||||
except ExceptionGroup as eg:
|
||||
raise eg.exceptions[0]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class RingNotificationsCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Global notifications coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, ring_api: ring_doorbell.Ring) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
name="active dings",
|
||||
update_interval=NOTIFICATIONS_SCAN_INTERVAL,
|
||||
)
|
||||
self.ring_api: ring_doorbell.Ring = ring_api
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Fetch data from API endpoint."""
|
||||
await _call_api(self.hass, self.ring_api.update_dings)
|
|
@ -1,49 +1,71 @@
|
|||
"""Base class for Ring entity."""
|
||||
from typing import TypeVar
|
||||
|
||||
from ring_doorbell.generic import RingGeneric
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN, RING_DEVICES_COORDINATOR
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from .coordinator import (
|
||||
RingDataCoordinator,
|
||||
RingDeviceData,
|
||||
RingNotificationsCoordinator,
|
||||
)
|
||||
|
||||
_RingCoordinatorT = TypeVar(
|
||||
"_RingCoordinatorT",
|
||||
bound=(RingDataCoordinator | RingNotificationsCoordinator),
|
||||
)
|
||||
|
||||
|
||||
class RingEntityMixin(Entity):
|
||||
class RingEntity(CoordinatorEntity[_RingCoordinatorT]):
|
||||
"""Base implementation for Ring device."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, config_entry_id, device):
|
||||
def __init__(
|
||||
self,
|
||||
device: RingGeneric,
|
||||
coordinator: _RingCoordinatorT,
|
||||
) -> None:
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super().__init__()
|
||||
self._config_entry_id = config_entry_id
|
||||
super().__init__(coordinator, context=device.id)
|
||||
self._device = device
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.device_id)},
|
||||
identifiers={(DOMAIN, device.device_id)}, # device_id is the mac
|
||||
manufacturer="Ring",
|
||||
model=device.model,
|
||||
name=device.name,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self.ring_objects[RING_DEVICES_COORDINATOR].async_add_listener(
|
||||
self._update_callback
|
||||
)
|
||||
def _get_coordinator_device_data(self) -> RingDeviceData | None:
|
||||
if (data := self.coordinator.data) and (
|
||||
device_data := data.get(self._device.id)
|
||||
):
|
||||
return device_data
|
||||
return None
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect callbacks."""
|
||||
self.ring_objects[RING_DEVICES_COORDINATOR].async_remove_listener(
|
||||
self._update_callback
|
||||
)
|
||||
def _get_coordinator_device(self) -> RingGeneric | None:
|
||||
if (device_data := self._get_coordinator_device_data()) and (
|
||||
device := device_data.device
|
||||
):
|
||||
return device
|
||||
return None
|
||||
|
||||
def _get_coordinator_history(self) -> list | None:
|
||||
if (device_data := self._get_coordinator_device_data()) and (
|
||||
history := device_data.history
|
||||
):
|
||||
return history
|
||||
return None
|
||||
|
||||
@callback
|
||||
def _update_callback(self) -> None:
|
||||
"""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]
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
if device := self._get_coordinator_device():
|
||||
self._device = device
|
||||
super()._handle_coordinator_update()
|
||||
|
|
|
@ -4,6 +4,8 @@ import logging
|
|||
from typing import Any
|
||||
|
||||
import requests
|
||||
from ring_doorbell import RingStickUpCam
|
||||
from ring_doorbell.generic import RingGeneric
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -11,8 +13,9 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DOMAIN, RING_DEVICES
|
||||
from .entity import RingEntityMixin
|
||||
from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -35,38 +38,42 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Create the lights for the Ring devices."""
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES]
|
||||
|
||||
devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
RING_DEVICES_COORDINATOR
|
||||
]
|
||||
lights = []
|
||||
|
||||
for device in devices["stickup_cams"]:
|
||||
if device.has_capability("light"):
|
||||
lights.append(RingLight(config_entry.entry_id, device))
|
||||
lights.append(RingLight(device, devices_coordinator))
|
||||
|
||||
async_add_entities(lights)
|
||||
|
||||
|
||||
class RingLight(RingEntityMixin, LightEntity):
|
||||
class RingLight(RingEntity, LightEntity):
|
||||
"""Creates a switch to turn the ring cameras light on and off."""
|
||||
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
_attr_translation_key = "light"
|
||||
|
||||
def __init__(self, config_entry_id, device):
|
||||
def __init__(self, device: RingGeneric, coordinator) -> None:
|
||||
"""Initialize the light."""
|
||||
super().__init__(config_entry_id, device)
|
||||
super().__init__(device, coordinator)
|
||||
self._attr_unique_id = device.id
|
||||
self._attr_is_on = device.lights == ON_STATE
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
def _handle_coordinator_update(self):
|
||||
"""Call update method."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
|
||||
self._attr_is_on = self._device.lights == ON_STATE
|
||||
self.async_write_ha_state()
|
||||
if (device := self._get_coordinator_device()) and isinstance(
|
||||
device, RingStickUpCam
|
||||
):
|
||||
self._attr_is_on = device.lights == ON_STATE
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _set_light(self, new_state):
|
||||
"""Update light state, and causes Home Assistant to correctly update."""
|
||||
|
@ -78,7 +85,7 @@ class RingLight(RingEntityMixin, LightEntity):
|
|||
|
||||
self._attr_is_on = new_state == ON_STATE
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.async_write_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on for 30 seconds."""
|
||||
|
|
|
@ -4,6 +4,8 @@ from __future__ import annotations
|
|||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from ring_doorbell.generic import RingGeneric
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
|
@ -18,13 +20,9 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
RING_DEVICES,
|
||||
RING_HEALTH_COORDINATOR,
|
||||
RING_HISTORY_COORDINATOR,
|
||||
)
|
||||
from .entity import RingEntityMixin
|
||||
from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -34,9 +32,12 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up a sensor for a Ring device."""
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES]
|
||||
devices_coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
RING_DEVICES_COORDINATOR
|
||||
]
|
||||
|
||||
entities = [
|
||||
description.cls(config_entry.entry_id, device, description)
|
||||
description.cls(device, devices_coordinator, description)
|
||||
for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams")
|
||||
for description in SENSOR_TYPES
|
||||
if device_type in description.category
|
||||
|
@ -47,19 +48,19 @@ async def async_setup_entry(
|
|||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RingSensor(RingEntityMixin, SensorEntity):
|
||||
class RingSensor(RingEntity, SensorEntity):
|
||||
"""A sensor implementation for Ring device."""
|
||||
|
||||
entity_description: RingSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry_id,
|
||||
device,
|
||||
device: RingGeneric,
|
||||
coordinator: RingDataCoordinator,
|
||||
description: RingSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super().__init__(config_entry_id, device)
|
||||
super().__init__(device, coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.id}-{description.key}"
|
||||
|
||||
|
@ -80,27 +81,6 @@ class HealthDataRingSensor(RingSensor):
|
|||
# These sensors are data hungry and not useful. Disable by default.
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self.ring_objects[RING_HEALTH_COORDINATOR].async_track_device(
|
||||
self._device, self._health_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects[RING_HEALTH_COORDINATOR].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 native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
@ -117,26 +97,10 @@ class HistoryRingSensor(RingSensor):
|
|||
|
||||
_latest_event: dict[str, Any] | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self.ring_objects[RING_HISTORY_COORDINATOR].async_track_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects[RING_HISTORY_COORDINATOR].async_untrack_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _history_update_callback(self, history_data):
|
||||
def _handle_coordinator_update(self):
|
||||
"""Call update method."""
|
||||
if not history_data:
|
||||
if not (history_data := self._get_coordinator_history()):
|
||||
return
|
||||
|
||||
kind = self.entity_description.kind
|
||||
|
@ -153,7 +117,7 @@ class HistoryRingSensor(RingSensor):
|
|||
return
|
||||
|
||||
self._latest_event = found
|
||||
self.async_write_ha_state()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
|
|
|
@ -3,14 +3,16 @@ import logging
|
|||
from typing import Any
|
||||
|
||||
from ring_doorbell.const import CHIME_TEST_SOUND_KINDS, KIND_DING
|
||||
from ring_doorbell.generic import RingGeneric
|
||||
|
||||
from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, RING_DEVICES
|
||||
from .entity import RingEntityMixin
|
||||
from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,24 +24,27 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Create the sirens for the Ring devices."""
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES]
|
||||
coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
RING_DEVICES_COORDINATOR
|
||||
]
|
||||
sirens = []
|
||||
|
||||
for device in devices["chimes"]:
|
||||
sirens.append(RingChimeSiren(config_entry, device))
|
||||
sirens.append(RingChimeSiren(device, coordinator))
|
||||
|
||||
async_add_entities(sirens)
|
||||
|
||||
|
||||
class RingChimeSiren(RingEntityMixin, SirenEntity):
|
||||
class RingChimeSiren(RingEntity, SirenEntity):
|
||||
"""Creates a siren to play the test chimes of a Chime device."""
|
||||
|
||||
_attr_available_tones = CHIME_TEST_SOUND_KINDS
|
||||
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES
|
||||
_attr_translation_key = "siren"
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry, device) -> None:
|
||||
def __init__(self, device: RingGeneric, coordinator: RingDataCoordinator) -> None:
|
||||
"""Initialize a Ring Chime siren."""
|
||||
super().__init__(config_entry.entry_id, device)
|
||||
super().__init__(device, coordinator)
|
||||
# Entity class attributes
|
||||
self._attr_unique_id = f"{self._device.id}-siren"
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import logging
|
|||
from typing import Any
|
||||
|
||||
import requests
|
||||
from ring_doorbell import RingStickUpCam
|
||||
from ring_doorbell.generic import RingGeneric
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -11,8 +13,9 @@ from homeassistant.core import HomeAssistant, callback
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DOMAIN, RING_DEVICES
|
||||
from .entity import RingEntityMixin
|
||||
from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -34,21 +37,26 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Create the switches for the Ring devices."""
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id][RING_DEVICES]
|
||||
coordinator: RingDataCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
RING_DEVICES_COORDINATOR
|
||||
]
|
||||
switches = []
|
||||
|
||||
for device in devices["stickup_cams"]:
|
||||
if device.has_capability("siren"):
|
||||
switches.append(SirenSwitch(config_entry.entry_id, device))
|
||||
switches.append(SirenSwitch(device, coordinator))
|
||||
|
||||
async_add_entities(switches)
|
||||
|
||||
|
||||
class BaseRingSwitch(RingEntityMixin, SwitchEntity):
|
||||
class BaseRingSwitch(RingEntity, SwitchEntity):
|
||||
"""Represents a switch for controlling an aspect of a ring device."""
|
||||
|
||||
def __init__(self, config_entry_id, device, device_type):
|
||||
def __init__(
|
||||
self, device: RingGeneric, coordinator: RingDataCoordinator, device_type: str
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(config_entry_id, device)
|
||||
super().__init__(device, coordinator)
|
||||
self._device_type = device_type
|
||||
self._attr_unique_id = f"{self._device.id}-{self._device_type}"
|
||||
|
||||
|
@ -59,20 +67,23 @@ class SirenSwitch(BaseRingSwitch):
|
|||
_attr_translation_key = "siren"
|
||||
_attr_icon = SIREN_ICON
|
||||
|
||||
def __init__(self, config_entry_id, device):
|
||||
def __init__(self, device: RingGeneric, coordinator: RingDataCoordinator) -> None:
|
||||
"""Initialize the switch for a device with a siren."""
|
||||
super().__init__(config_entry_id, device, "siren")
|
||||
super().__init__(device, coordinator, "siren")
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
self._attr_is_on = device.siren > 0
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
def _handle_coordinator_update(self):
|
||||
"""Call update method."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
|
||||
self._attr_is_on = self._device.siren > 0
|
||||
self.async_write_ha_state()
|
||||
if (device := self._get_coordinator_device()) and isinstance(
|
||||
device, RingStickUpCam
|
||||
):
|
||||
self._attr_is_on = device.siren > 0
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _set_switch(self, new_state):
|
||||
"""Update switch state, and causes Home Assistant to correctly update."""
|
||||
|
|
|
@ -60,6 +60,49 @@ async def test_auth_failed_on_setup(
|
|||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error_type", "log_msg"),
|
||||
[
|
||||
(
|
||||
RingTimeout,
|
||||
"Timeout communicating with API: ",
|
||||
),
|
||||
(
|
||||
RingError,
|
||||
"Error communicating with API: ",
|
||||
),
|
||||
],
|
||||
ids=["timeout-error", "other-error"],
|
||||
)
|
||||
async def test_error_on_setup(
|
||||
hass: HomeAssistant,
|
||||
requests_mock: requests_mock.Mocker,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog,
|
||||
error_type,
|
||||
log_msg,
|
||||
) -> None:
|
||||
"""Test auth failure on setup entry."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"ring_doorbell.Ring.update_data",
|
||||
side_effect=error_type,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
assert [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.levelname == "DEBUG"
|
||||
and record.name == "homeassistant.config_entries"
|
||||
and log_msg in record.message
|
||||
and DOMAIN in record.message
|
||||
]
|
||||
|
||||
|
||||
async def test_auth_failure_on_global_update(
|
||||
hass: HomeAssistant,
|
||||
requests_mock: requests_mock.Mocker,
|
||||
|
@ -78,8 +121,11 @@ async def test_auth_failure_on_global_update(
|
|||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Ring access token is no longer valid, need to re-authenticate" in [
|
||||
record.message for record in caplog.records if record.levelname == "WARNING"
|
||||
assert "Authentication failed while fetching devices data: " in [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.levelname == "ERROR"
|
||||
and record.name == "homeassistant.components.ring.coordinator"
|
||||
]
|
||||
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
@ -91,7 +137,7 @@ async def test_auth_failure_on_device_update(
|
|||
mock_config_entry: MockConfigEntry,
|
||||
caplog,
|
||||
) -> None:
|
||||
"""Test authentication failure on global data update."""
|
||||
"""Test authentication failure on device data update."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -103,8 +149,11 @@ async def test_auth_failure_on_device_update(
|
|||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "Ring access token is no longer valid, need to re-authenticate" in [
|
||||
record.message for record in caplog.records if record.levelname == "WARNING"
|
||||
assert "Authentication failed while fetching devices data: " in [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.levelname == "ERROR"
|
||||
and record.name == "homeassistant.components.ring.coordinator"
|
||||
]
|
||||
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH}))
|
||||
|
@ -115,11 +164,11 @@ async def test_auth_failure_on_device_update(
|
|||
[
|
||||
(
|
||||
RingTimeout,
|
||||
"Time out fetching Ring device data",
|
||||
"Error fetching devices data: Timeout communicating with API: ",
|
||||
),
|
||||
(
|
||||
RingError,
|
||||
"Error fetching Ring device data: ",
|
||||
"Error fetching devices data: Error communicating with API: ",
|
||||
),
|
||||
],
|
||||
ids=["timeout-error", "other-error"],
|
||||
|
@ -145,7 +194,7 @@ async def test_error_on_global_update(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert log_msg in [
|
||||
record.message for record in caplog.records if record.levelname == "WARNING"
|
||||
record.message for record in caplog.records if record.levelname == "ERROR"
|
||||
]
|
||||
|
||||
assert mock_config_entry.entry_id in hass.data[DOMAIN]
|
||||
|
@ -156,11 +205,11 @@ async def test_error_on_global_update(
|
|||
[
|
||||
(
|
||||
RingTimeout,
|
||||
"Time out fetching Ring history data for device aacdef123",
|
||||
"Error fetching devices data: Timeout communicating with API for device Front: ",
|
||||
),
|
||||
(
|
||||
RingError,
|
||||
"Error fetching Ring history data for device aacdef123: ",
|
||||
"Error fetching devices data: Error communicating with API for device Front: ",
|
||||
),
|
||||
],
|
||||
ids=["timeout-error", "other-error"],
|
||||
|
@ -186,6 +235,6 @@ async def test_error_on_device_update(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert log_msg in [
|
||||
record.message for record in caplog.records if record.levelname == "WARNING"
|
||||
record.message for record in caplog.records if record.levelname == "ERROR"
|
||||
]
|
||||
assert mock_config_entry.entry_id in hass.data[DOMAIN]
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
"""The tests for the Ring switch platform."""
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import setup_platform
|
||||
|
||||
|
@ -84,7 +85,13 @@ async def test_updates_work(
|
|||
text=load_fixture("devices_updated.json", "ring"),
|
||||
)
|
||||
|
||||
await hass.services.async_call("ring", "update", {}, blocking=True)
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
await hass.services.async_call(
|
||||
"homeassistant",
|
||||
"update_entity",
|
||||
{ATTR_ENTITY_ID: ["switch.front_siren"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
|
Loading…
Reference in New Issue