mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add strict typing to ring integration (#115276)
This commit is contained in:
parent
3546ca386f
commit
6954fcc8ad
@ -363,6 +363,7 @@ homeassistant.components.rest_command.*
|
||||
homeassistant.components.rfxtrx.*
|
||||
homeassistant.components.rhasspy.*
|
||||
homeassistant.components.ridwell.*
|
||||
homeassistant.components.ring.*
|
||||
homeassistant.components.rituals_perfume_genie.*
|
||||
homeassistant.components.roku.*
|
||||
homeassistant.components.romy.*
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from ring_doorbell import Auth, Ring
|
||||
from ring_doorbell import Auth, Ring, RingDevices
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import APPLICATION_NAME, CONF_TOKEN, __version__
|
||||
@ -13,23 +15,26 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
RING_API,
|
||||
RING_DEVICES,
|
||||
RING_DEVICES_COORDINATOR,
|
||||
RING_NOTIFICATIONS_COORDINATOR,
|
||||
)
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import RingDataCoordinator, RingNotificationsCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RingData:
|
||||
"""Class to support type hinting of ring data collection."""
|
||||
|
||||
api: Ring
|
||||
devices: RingDevices
|
||||
devices_coordinator: RingDataCoordinator
|
||||
notifications_coordinator: RingNotificationsCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
|
||||
def token_updater(token):
|
||||
def token_updater(token: dict[str, Any]) -> None:
|
||||
"""Handle from sync context when token is updated."""
|
||||
hass.loop.call_soon_threadsafe(
|
||||
partial(
|
||||
@ -51,12 +56,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
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: devices_coordinator,
|
||||
RING_NOTIFICATIONS_COORDINATOR: notifications_coordinator,
|
||||
}
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RingData(
|
||||
api=ring,
|
||||
devices=ring.devices(),
|
||||
devices_coordinator=devices_coordinator,
|
||||
notifications_coordinator=notifications_coordinator,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@ -83,8 +88,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
for info in hass.data[DOMAIN].values():
|
||||
await info[RING_DEVICES_COORDINATOR].async_refresh()
|
||||
await info[RING_NOTIFICATIONS_COORDINATOR].async_refresh()
|
||||
ring_data = cast(RingData, info)
|
||||
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)
|
||||
@ -121,8 +127,9 @@ async def _migrate_old_unique_ids(hass: HomeAssistant, entry_id: str) -> None:
|
||||
@callback
|
||||
def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
|
||||
# Old format for camera and light was int
|
||||
if isinstance(entity_entry.unique_id, int):
|
||||
new_unique_id = str(entity_entry.unique_id)
|
||||
unique_id = cast(str | int, entity_entry.unique_id)
|
||||
if isinstance(unique_id, int):
|
||||
new_unique_id = str(unique_id)
|
||||
if existing_entity_id := entity_registry.async_get_entity_id(
|
||||
entity_entry.domain, entity_entry.platform, new_unique_id
|
||||
):
|
||||
|
@ -2,10 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from ring_doorbell import Ring, RingEvent, RingGeneric
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
@ -15,29 +18,32 @@ from homeassistant.config_entries import ConfigEntry
|
||||
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 . import RingData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingNotificationsCoordinator
|
||||
from .entity import RingEntity
|
||||
from .entity import RingBaseEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RingBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes Ring binary sensor entity."""
|
||||
|
||||
category: list[str]
|
||||
exists_fn: Callable[[RingGeneric], bool]
|
||||
|
||||
|
||||
BINARY_SENSOR_TYPES: tuple[RingBinarySensorEntityDescription, ...] = (
|
||||
RingBinarySensorEntityDescription(
|
||||
key="ding",
|
||||
translation_key="ding",
|
||||
category=["doorbots", "authorized_doorbots", "other"],
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
exists_fn=lambda device: device.family
|
||||
in {"doorbots", "authorized_doorbots", "other"},
|
||||
),
|
||||
RingBinarySensorEntityDescription(
|
||||
key="motion",
|
||||
category=["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
exists_fn=lambda device: device.family
|
||||
in {"doorbots", "authorized_doorbots", "stickup_cams"},
|
||||
),
|
||||
)
|
||||
|
||||
@ -48,34 +54,36 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""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]
|
||||
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
entities = [
|
||||
RingBinarySensor(ring, device, notifications_coordinator, description)
|
||||
for device_type in ("doorbots", "authorized_doorbots", "stickup_cams", "other")
|
||||
RingBinarySensor(
|
||||
ring_data.api,
|
||||
device,
|
||||
ring_data.notifications_coordinator,
|
||||
description,
|
||||
)
|
||||
for description in BINARY_SENSOR_TYPES
|
||||
if device_type in description.category
|
||||
for device in devices[device_type]
|
||||
for device in ring_data.devices.all_devices
|
||||
if description.exists_fn(device)
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RingBinarySensor(RingEntity, BinarySensorEntity):
|
||||
class RingBinarySensor(
|
||||
RingBaseEntity[RingNotificationsCoordinator], BinarySensorEntity
|
||||
):
|
||||
"""A binary sensor implementation for Ring device."""
|
||||
|
||||
_active_alert: dict[str, Any] | None = None
|
||||
_active_alert: RingEvent | None = None
|
||||
entity_description: RingBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ring,
|
||||
device,
|
||||
coordinator,
|
||||
ring: Ring,
|
||||
device: RingGeneric,
|
||||
coordinator: RingNotificationsCoordinator,
|
||||
description: RingBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a sensor for Ring device."""
|
||||
@ -89,13 +97,13 @@ class RingBinarySensor(RingEntity, BinarySensorEntity):
|
||||
self._update_alert()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self, _=None):
|
||||
def _handle_coordinator_update(self, _: Any = None) -> None:
|
||||
"""Call update method."""
|
||||
self._update_alert()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _update_alert(self):
|
||||
def _update_alert(self) -> None:
|
||||
"""Update active alert."""
|
||||
self._active_alert = next(
|
||||
(
|
||||
@ -108,21 +116,23 @@ class RingBinarySensor(RingEntity, BinarySensorEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._active_alert is not None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return the state attributes."""
|
||||
attrs = super().extra_state_attributes
|
||||
|
||||
if self._active_alert is None:
|
||||
return attrs
|
||||
|
||||
assert isinstance(attrs, dict)
|
||||
attrs["state"] = self._active_alert["state"]
|
||||
attrs["expires_at"] = datetime.fromtimestamp(
|
||||
self._active_alert.get("now") + self._active_alert.get("expires_in")
|
||||
).isoformat()
|
||||
now = self._active_alert.get("now")
|
||||
expires_in = self._active_alert.get("expires_in")
|
||||
assert now and expires_in
|
||||
attrs["expires_at"] = datetime.fromtimestamp(now + expires_in).isoformat()
|
||||
|
||||
return attrs
|
||||
|
@ -2,12 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR
|
||||
from . import RingData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity, exception_wrap
|
||||
|
||||
@ -22,14 +25,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create the buttons 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
|
||||
]
|
||||
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices_coordinator = ring_data.devices_coordinator
|
||||
|
||||
async_add_entities(
|
||||
RingDoorButton(device, devices_coordinator, BUTTON_DESCRIPTION)
|
||||
for device in devices["other"]
|
||||
for device in ring_data.devices.other
|
||||
if device.has_capability("open")
|
||||
)
|
||||
|
||||
@ -37,10 +38,12 @@ async def async_setup_entry(
|
||||
class RingDoorButton(RingEntity, ButtonEntity):
|
||||
"""Creates a button to open the ring intercom door."""
|
||||
|
||||
_device: RingOther
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device,
|
||||
coordinator,
|
||||
device: RingOther,
|
||||
coordinator: RingDataCoordinator,
|
||||
description: ButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the button."""
|
||||
@ -52,6 +55,6 @@ class RingDoorButton(RingEntity, ButtonEntity):
|
||||
self._attr_unique_id = f"{device.id}-{description.key}"
|
||||
|
||||
@exception_wrap
|
||||
def press(self):
|
||||
def press(self) -> None:
|
||||
"""Open the door."""
|
||||
self._device.open_door()
|
||||
|
@ -3,11 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from itertools import chain
|
||||
import logging
|
||||
from typing import Optional
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
from ring_doorbell import RingDoorBell
|
||||
|
||||
from homeassistant.components import ffmpeg
|
||||
from homeassistant.components.camera import Camera
|
||||
@ -17,7 +18,8 @@ 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_DEVICES_COORDINATOR
|
||||
from . import RingData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity, exception_wrap
|
||||
|
||||
@ -33,20 +35,15 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> 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
|
||||
]
|
||||
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices_coordinator = ring_data.devices_coordinator
|
||||
ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass)
|
||||
|
||||
cams = []
|
||||
for camera in chain(
|
||||
devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"]
|
||||
):
|
||||
if not camera.has_subscription:
|
||||
continue
|
||||
|
||||
cams.append(RingCam(camera, devices_coordinator, ffmpeg_manager))
|
||||
cams = [
|
||||
RingCam(camera, devices_coordinator, ffmpeg_manager)
|
||||
for camera in ring_data.devices.video_devices
|
||||
if camera.has_subscription
|
||||
]
|
||||
|
||||
async_add_entities(cams)
|
||||
|
||||
@ -55,28 +52,34 @@ class RingCam(RingEntity, Camera):
|
||||
"""An implementation of a Ring Door Bell camera."""
|
||||
|
||||
_attr_name = None
|
||||
_device: RingDoorBell
|
||||
|
||||
def __init__(self, device, coordinator, ffmpeg_manager):
|
||||
def __init__(
|
||||
self,
|
||||
device: RingDoorBell,
|
||||
coordinator: RingDataCoordinator,
|
||||
ffmpeg_manager: ffmpeg.FFmpegManager,
|
||||
) -> None:
|
||||
"""Initialize a Ring Door Bell camera."""
|
||||
super().__init__(device, coordinator)
|
||||
Camera.__init__(self)
|
||||
|
||||
self._ffmpeg_manager = ffmpeg_manager
|
||||
self._last_event = None
|
||||
self._last_video_id = None
|
||||
self._video_url = None
|
||||
self._image = None
|
||||
self._last_event: dict[str, Any] | None = None
|
||||
self._last_video_id: int | None = None
|
||||
self._video_url: str | None = None
|
||||
self._image: bytes | None = None
|
||||
self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL
|
||||
self._attr_unique_id = str(device.id)
|
||||
if device.has_capability(MOTION_DETECTION_CAPABILITY):
|
||||
self._attr_motion_detection_enabled = device.motion_detection
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Call update method."""
|
||||
history_data: Optional[list]
|
||||
if not (history_data := self._get_coordinator_history()):
|
||||
return
|
||||
self._device = self._get_coordinator_data().get_video_device(
|
||||
self._device.device_api_id
|
||||
)
|
||||
history_data = self._device.last_history
|
||||
if history_data:
|
||||
self._last_event = history_data[0]
|
||||
self.async_schedule_update_ha_state(True)
|
||||
@ -89,7 +92,7 @@ class RingCam(RingEntity, Camera):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
"video_url": self._video_url,
|
||||
@ -100,7 +103,7 @@ class RingCam(RingEntity, Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image response from the camera."""
|
||||
if self._image is None and self._video_url:
|
||||
if self._image is None and self._video_url is not None:
|
||||
image = await ffmpeg.async_get_image(
|
||||
self.hass,
|
||||
self._video_url,
|
||||
@ -113,10 +116,12 @@ class RingCam(RingEntity, Camera):
|
||||
|
||||
return self._image
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(
|
||||
self, request: web.Request
|
||||
) -> web.StreamResponse | None:
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
if self._video_url is None:
|
||||
return
|
||||
return None
|
||||
|
||||
stream = CameraMjpeg(self._ffmpeg_manager.binary)
|
||||
await stream.open_camera(self._video_url)
|
||||
@ -132,7 +137,7 @@ class RingCam(RingEntity, Camera):
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
async def async_update(self):
|
||||
async def async_update(self) -> None:
|
||||
"""Update camera entity and refresh attributes."""
|
||||
if (
|
||||
self._device.has_capability(MOTION_DETECTION_CAPABILITY)
|
||||
@ -160,11 +165,14 @@ class RingCam(RingEntity, Camera):
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + utcnow
|
||||
|
||||
@exception_wrap
|
||||
def _get_video(self):
|
||||
return self._device.recording_url(self._last_event["id"])
|
||||
def _get_video(self) -> str | None:
|
||||
if self._last_event is None:
|
||||
return None
|
||||
assert (event_id := self._last_event.get("id")) and isinstance(event_id, int)
|
||||
return self._device.recording_url(event_id)
|
||||
|
||||
@exception_wrap
|
||||
def _set_motion_detection_enabled(self, new_state):
|
||||
def _set_motion_detection_enabled(self, new_state: bool) -> None:
|
||||
if not self._device.has_capability(MOTION_DETECTION_CAPABILITY):
|
||||
_LOGGER.error(
|
||||
"Entity %s does not have motion detection capability", self.entity_id
|
||||
|
@ -28,7 +28,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data):
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
auth = Auth(f"{APPLICATION_NAME}/{ha_version}")
|
||||
@ -56,9 +56,11 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
user_pass: dict[str, Any] = {}
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
token = await validate_input(self.hass, user_input)
|
||||
@ -82,7 +84,9 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_2fa(self, user_input=None):
|
||||
async def async_step_2fa(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle 2fa step."""
|
||||
if user_input:
|
||||
if self.reauth_entry:
|
||||
@ -110,7 +114,7 @@ class RingConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
assert self.reauth_entry is not None
|
||||
|
||||
if user_input:
|
||||
|
@ -28,10 +28,4 @@ PLATFORMS = [
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
NOTIFICATIONS_SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
RING_API = "api"
|
||||
RING_DEVICES = "devices"
|
||||
|
||||
RING_DEVICES_COORDINATOR = "device_data"
|
||||
RING_NOTIFICATIONS_COORDINATOR = "dings_data"
|
||||
|
||||
CONF_2FA = "2fa"
|
||||
|
@ -2,11 +2,10 @@
|
||||
|
||||
from asyncio import TaskGroup
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from typing import TypeVar, TypeVarTuple
|
||||
|
||||
from ring_doorbell import AuthenticationError, Ring, RingError, RingGeneric, RingTimeout
|
||||
from ring_doorbell import AuthenticationError, Ring, RingDevices, RingError, RingTimeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
@ -16,10 +15,13 @@ from .const import NOTIFICATIONS_SCAN_INTERVAL, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_R = TypeVar("_R")
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
|
||||
|
||||
async def _call_api(
|
||||
hass: HomeAssistant, target: Callable[..., Any], *args, msg_suffix: str = ""
|
||||
):
|
||||
hass: HomeAssistant, target: Callable[[*_Ts], _R], *args: *_Ts, msg_suffix: str = ""
|
||||
) -> _R:
|
||||
try:
|
||||
return await hass.async_add_executor_job(target, *args)
|
||||
except AuthenticationError as err:
|
||||
@ -34,15 +36,7 @@ async def _call_api(
|
||||
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]]):
|
||||
class RingDataCoordinator(DataUpdateCoordinator[RingDevices]):
|
||||
"""Base class for device coordinators."""
|
||||
|
||||
def __init__(
|
||||
@ -60,45 +54,39 @@ class RingDataCoordinator(DataUpdateCoordinator[dict[int, RingDeviceData]]):
|
||||
self.ring_api: Ring = ring_api
|
||||
self.first_call: bool = True
|
||||
|
||||
async def _async_update_data(self):
|
||||
async def _async_update_data(self) -> RingDevices:
|
||||
"""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()
|
||||
devices: RingDevices = 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:
|
||||
history_task = None
|
||||
async with TaskGroup() as tg:
|
||||
if device.has_capability("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
|
||||
)
|
||||
)
|
||||
for device in devices.all_devices:
|
||||
# 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:
|
||||
try:
|
||||
async with TaskGroup() as tg:
|
||||
if device.has_capability("history"):
|
||||
tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
device.update_health_data,
|
||||
msg_suffix=f" for device {device.name}",
|
||||
lambda device: device.history(limit=10),
|
||||
device,
|
||||
msg_suffix=f" for device {device.name}", # device_id is the mac
|
||||
)
|
||||
)
|
||||
if history_task:
|
||||
data[device.id].history = history_task.result()
|
||||
except ExceptionGroup as eg:
|
||||
raise eg.exceptions[0] # noqa: B904
|
||||
tg.create_task(
|
||||
_call_api(
|
||||
self.hass,
|
||||
device.update_health_data,
|
||||
msg_suffix=f" for device {device.name}",
|
||||
)
|
||||
)
|
||||
except ExceptionGroup as eg:
|
||||
raise eg.exceptions[0] # noqa: B904
|
||||
|
||||
return data
|
||||
return devices
|
||||
|
||||
|
||||
class RingNotificationsCoordinator(DataUpdateCoordinator[None]):
|
||||
@ -114,6 +102,6 @@ class RingNotificationsCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
self.ring_api: Ring = ring_api
|
||||
|
||||
async def _async_update_data(self):
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
await _call_api(self.hass, self.ring_api.update_dings)
|
||||
|
@ -4,12 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ring_doorbell import Ring
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import RingData
|
||||
from .const import DOMAIN
|
||||
|
||||
TO_REDACT = {
|
||||
@ -33,11 +32,12 @@ async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
ring: Ring = hass.data[DOMAIN][entry.entry_id]["api"]
|
||||
ring_data: RingData = hass.data[DOMAIN][entry.entry_id]
|
||||
devices_data = ring_data.api.devices_data
|
||||
devices_raw = [
|
||||
ring.devices_data[device_type][device_id]
|
||||
for device_type in ring.devices_data
|
||||
for device_id in ring.devices_data[device_type]
|
||||
devices_data[device_type][device_id]
|
||||
for device_type in devices_data
|
||||
for device_id in devices_data[device_type]
|
||||
]
|
||||
return async_redact_data(
|
||||
{"device_data": devices_raw},
|
||||
|
@ -3,7 +3,13 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
from ring_doorbell import AuthenticationError, RingError, RingGeneric, RingTimeout
|
||||
from ring_doorbell import (
|
||||
AuthenticationError,
|
||||
RingDevices,
|
||||
RingError,
|
||||
RingGeneric,
|
||||
RingTimeout,
|
||||
)
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@ -11,26 +17,23 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN
|
||||
from .coordinator import (
|
||||
RingDataCoordinator,
|
||||
RingDeviceData,
|
||||
RingNotificationsCoordinator,
|
||||
)
|
||||
from .coordinator import RingDataCoordinator, RingNotificationsCoordinator
|
||||
|
||||
_RingCoordinatorT = TypeVar(
|
||||
"_RingCoordinatorT",
|
||||
bound=(RingDataCoordinator | RingNotificationsCoordinator),
|
||||
)
|
||||
_T = TypeVar("_T", bound="RingEntity")
|
||||
_RingBaseEntityT = TypeVar("_RingBaseEntityT", bound="RingBaseEntity[Any]")
|
||||
_R = TypeVar("_R")
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
def exception_wrap(
|
||||
func: Callable[Concatenate[_T, _P], Any],
|
||||
) -> Callable[Concatenate[_T, _P], Any]:
|
||||
func: Callable[Concatenate[_RingBaseEntityT, _P], _R],
|
||||
) -> Callable[Concatenate[_RingBaseEntityT, _P], _R]:
|
||||
"""Define a wrapper to catch exceptions and raise HomeAssistant errors."""
|
||||
|
||||
def _wrap(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
def _wrap(self: _RingBaseEntityT, *args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
except AuthenticationError as err:
|
||||
@ -50,7 +53,7 @@ def exception_wrap(
|
||||
return _wrap
|
||||
|
||||
|
||||
class RingEntity(CoordinatorEntity[_RingCoordinatorT]):
|
||||
class RingBaseEntity(CoordinatorEntity[_RingCoordinatorT]):
|
||||
"""Base implementation for Ring device."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
@ -73,29 +76,16 @@ class RingEntity(CoordinatorEntity[_RingCoordinatorT]):
|
||||
name=device.name,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def _get_coordinator_device(self) -> RingGeneric | None:
|
||||
if (device_data := self._get_coordinator_device_data()) and (
|
||||
device := device_data.device
|
||||
):
|
||||
return device
|
||||
return None
|
||||
class RingEntity(RingBaseEntity[RingDataCoordinator]):
|
||||
"""Implementation for Ring devices."""
|
||||
|
||||
def _get_coordinator_history(self) -> list | None:
|
||||
if (device_data := self._get_coordinator_device_data()) and (
|
||||
history := device_data.history
|
||||
):
|
||||
return history
|
||||
return None
|
||||
def _get_coordinator_data(self) -> RingDevices:
|
||||
return self.coordinator.data
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
if device := self._get_coordinator_device():
|
||||
self._device = device
|
||||
self._device = self._get_coordinator_data().get_device(
|
||||
self._device.device_api_id
|
||||
)
|
||||
super()._handle_coordinator_update()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Component providing HA switch support for Ring Door Bell/Chimes."""
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum, auto
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@ -12,7 +13,8 @@ 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, RING_DEVICES_COORDINATOR
|
||||
from . import RingData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity, exception_wrap
|
||||
|
||||
@ -26,8 +28,12 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SKIP_UPDATES_DELAY = timedelta(seconds=5)
|
||||
|
||||
ON_STATE = "on"
|
||||
OFF_STATE = "off"
|
||||
|
||||
class OnOffState(StrEnum):
|
||||
"""Enum for allowed on off states."""
|
||||
|
||||
ON = auto()
|
||||
OFF = auto()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@ -36,14 +42,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> 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
|
||||
]
|
||||
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices_coordinator = ring_data.devices_coordinator
|
||||
|
||||
async_add_entities(
|
||||
RingLight(device, devices_coordinator)
|
||||
for device in devices["stickup_cams"]
|
||||
for device in ring_data.devices.stickup_cams
|
||||
if device.has_capability("light")
|
||||
)
|
||||
|
||||
@ -55,37 +59,41 @@ class RingLight(RingEntity, LightEntity):
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
_attr_translation_key = "light"
|
||||
|
||||
def __init__(self, device, coordinator):
|
||||
_device: RingStickUpCam
|
||||
|
||||
def __init__(
|
||||
self, device: RingStickUpCam, coordinator: RingDataCoordinator
|
||||
) -> None:
|
||||
"""Initialize the light."""
|
||||
super().__init__(device, coordinator)
|
||||
self._attr_unique_id = str(device.id)
|
||||
self._attr_is_on = device.lights == ON_STATE
|
||||
self._attr_is_on = device.lights == OnOffState.ON
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Call update method."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
if (device := self._get_coordinator_device()) and isinstance(
|
||||
device, RingStickUpCam
|
||||
):
|
||||
self._attr_is_on = device.lights == ON_STATE
|
||||
device = self._get_coordinator_data().get_stickup_cam(
|
||||
self._device.device_api_id
|
||||
)
|
||||
self._attr_is_on = device.lights == OnOffState.ON
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@exception_wrap
|
||||
def _set_light(self, new_state):
|
||||
def _set_light(self, new_state: OnOffState) -> None:
|
||||
"""Update light state, and causes Home Assistant to correctly update."""
|
||||
self._device.lights = new_state
|
||||
|
||||
self._attr_is_on = new_state == ON_STATE
|
||||
self._attr_is_on = new_state == OnOffState.ON
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on for 30 seconds."""
|
||||
self._set_light(ON_STATE)
|
||||
self._set_light(OnOffState.ON)
|
||||
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
self._set_light(OFF_STATE)
|
||||
self._set_light(OnOffState.OFF)
|
||||
|
@ -2,10 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from typing import Any, Generic, cast
|
||||
|
||||
from ring_doorbell import RingGeneric
|
||||
from ring_doorbell import (
|
||||
RingCapability,
|
||||
RingChime,
|
||||
RingDoorBell,
|
||||
RingEventKind,
|
||||
RingGeneric,
|
||||
RingOther,
|
||||
)
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@ -21,11 +30,15 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN, RING_DEVICES, RING_DEVICES_COORDINATOR
|
||||
from . import RingData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity
|
||||
|
||||
_RingDeviceT = TypeVar("_RingDeviceT", bound=RingGeneric, default=RingGeneric)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -33,209 +46,193 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> 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
|
||||
]
|
||||
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices_coordinator = ring_data.devices_coordinator
|
||||
|
||||
entities = [
|
||||
description.cls(device, devices_coordinator, description)
|
||||
for device_type in (
|
||||
"chimes",
|
||||
"doorbots",
|
||||
"authorized_doorbots",
|
||||
"stickup_cams",
|
||||
"other",
|
||||
)
|
||||
RingSensor(device, devices_coordinator, description)
|
||||
for description in SENSOR_TYPES
|
||||
if device_type in description.category
|
||||
for device in devices[device_type]
|
||||
if not (device_type == "battery" and device.battery_life is None)
|
||||
for device in ring_data.devices.all_devices
|
||||
if description.exists_fn(device)
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RingSensor(RingEntity, SensorEntity):
|
||||
class RingSensor(RingEntity, SensorEntity, Generic[_RingDeviceT]):
|
||||
"""A sensor implementation for Ring device."""
|
||||
|
||||
entity_description: RingSensorEntityDescription
|
||||
entity_description: RingSensorEntityDescription[_RingDeviceT]
|
||||
_device: _RingDeviceT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: RingGeneric,
|
||||
coordinator: RingDataCoordinator,
|
||||
description: RingSensorEntityDescription,
|
||||
description: RingSensorEntityDescription[_RingDeviceT],
|
||||
) -> None:
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super().__init__(device, coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.id}-{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
sensor_type = self.entity_description.key
|
||||
if sensor_type == "volume":
|
||||
return self._device.volume
|
||||
if sensor_type == "doorbell_volume":
|
||||
return self._device.doorbell_volume
|
||||
if sensor_type == "mic_volume":
|
||||
return self._device.mic_volume
|
||||
if sensor_type == "voice_volume":
|
||||
return self._device.voice_volume
|
||||
|
||||
if sensor_type == "battery":
|
||||
return self._device.battery_life
|
||||
|
||||
|
||||
class HealthDataRingSensor(RingSensor):
|
||||
"""Ring sensor that relies on health data."""
|
||||
|
||||
# These sensors are data hungry and not useful. Disable by default.
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
sensor_type = self.entity_description.key
|
||||
if sensor_type == "wifi_signal_category":
|
||||
return self._device.wifi_signal_category
|
||||
|
||||
if sensor_type == "wifi_signal_strength":
|
||||
return self._device.wifi_signal_strength
|
||||
|
||||
|
||||
class HistoryRingSensor(RingSensor):
|
||||
"""Ring sensor that relies on history data."""
|
||||
|
||||
_latest_event: dict[str, Any] | None = None
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
description.entity_registry_enabled_default
|
||||
)
|
||||
self._attr_native_value = self.entity_description.value_fn(self._device)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Call update method."""
|
||||
if not (history_data := self._get_coordinator_history()):
|
||||
return
|
||||
|
||||
kind = self.entity_description.kind
|
||||
found = None
|
||||
if kind is None:
|
||||
found = history_data[0]
|
||||
else:
|
||||
for entry in history_data:
|
||||
if entry["kind"] == kind:
|
||||
found = entry
|
||||
break
|
||||
|
||||
if not found:
|
||||
return
|
||||
|
||||
self._latest_event = found
|
||||
self._device = cast(
|
||||
_RingDeviceT,
|
||||
self._get_coordinator_data().get_device(self._device.device_api_id),
|
||||
)
|
||||
# History values can drop off the last 10 events so only update
|
||||
# the value if it's not None
|
||||
if native_value := self.entity_description.value_fn(self._device):
|
||||
self._attr_native_value = native_value
|
||||
if extra_attrs := self.entity_description.extra_state_attributes_fn(
|
||||
self._device
|
||||
):
|
||||
self._attr_extra_state_attributes = extra_attrs
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._latest_event is None:
|
||||
return None
|
||||
|
||||
return self._latest_event["created_at"]
|
||||
def _get_last_event(
|
||||
history_data: list[dict[str, Any]], kind: RingEventKind | None
|
||||
) -> dict[str, Any] | None:
|
||||
if not history_data:
|
||||
return None
|
||||
if kind is None:
|
||||
return history_data[0]
|
||||
for entry in history_data:
|
||||
if entry["kind"] == kind.value:
|
||||
return entry
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = super().extra_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
|
||||
def _get_last_event_attrs(
|
||||
history_data: list[dict[str, Any]], kind: RingEventKind | None
|
||||
) -> dict[str, Any] | None:
|
||||
if last_event := _get_last_event(history_data, kind):
|
||||
return {
|
||||
"created_at": last_event.get("created_at"),
|
||||
"answered": last_event.get("answered"),
|
||||
"recording_status": last_event.get("recording", {}).get("status"),
|
||||
"category": last_event.get("kind"),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RingSensorEntityDescription(SensorEntityDescription):
|
||||
class RingSensorEntityDescription(SensorEntityDescription, Generic[_RingDeviceT]):
|
||||
"""Describes Ring sensor entity."""
|
||||
|
||||
category: list[str]
|
||||
cls: type[RingSensor]
|
||||
|
||||
kind: str | None = None
|
||||
value_fn: Callable[[_RingDeviceT], StateType] = lambda _: True
|
||||
exists_fn: Callable[[RingGeneric], bool] = lambda _: True
|
||||
extra_state_attributes_fn: Callable[[_RingDeviceT], dict[str, Any] | None] = (
|
||||
lambda _: None
|
||||
)
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[RingSensorEntityDescription, ...] = (
|
||||
RingSensorEntityDescription(
|
||||
# For some reason mypy doesn't properly type check the default TypeVar value here
|
||||
# so for now the [RingGeneric] subscript needs to be specified.
|
||||
# Once https://github.com/python/mypy/issues/14851 is closed this should hopefully
|
||||
# be fixed and the [RingGeneric] subscript can be removed.
|
||||
# https://github.com/home-assistant/core/pull/115276#discussion_r1560106576
|
||||
SENSOR_TYPES: tuple[RingSensorEntityDescription[Any], ...] = (
|
||||
RingSensorEntityDescription[RingGeneric](
|
||||
key="battery",
|
||||
category=["doorbots", "authorized_doorbots", "stickup_cams", "other"],
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
cls=RingSensor,
|
||||
value_fn=lambda device: device.battery_life,
|
||||
exists_fn=lambda device: device.family != "chimes",
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingGeneric](
|
||||
key="last_activity",
|
||||
translation_key="last_activity",
|
||||
category=["doorbots", "authorized_doorbots", "stickup_cams", "other"],
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
cls=HistoryRingSensor,
|
||||
value_fn=lambda device: last_event.get("created_at")
|
||||
if (last_event := _get_last_event(device.last_history, None))
|
||||
else None,
|
||||
extra_state_attributes_fn=lambda device: last_event_attrs
|
||||
if (last_event_attrs := _get_last_event_attrs(device.last_history, None))
|
||||
else None,
|
||||
exists_fn=lambda device: device.has_capability(RingCapability.HISTORY),
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingGeneric](
|
||||
key="last_ding",
|
||||
translation_key="last_ding",
|
||||
category=["doorbots", "authorized_doorbots", "other"],
|
||||
kind="ding",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
cls=HistoryRingSensor,
|
||||
value_fn=lambda device: last_event.get("created_at")
|
||||
if (last_event := _get_last_event(device.last_history, RingEventKind.DING))
|
||||
else None,
|
||||
extra_state_attributes_fn=lambda device: last_event_attrs
|
||||
if (
|
||||
last_event_attrs := _get_last_event_attrs(
|
||||
device.last_history, RingEventKind.DING
|
||||
)
|
||||
)
|
||||
else None,
|
||||
exists_fn=lambda device: device.has_capability(RingCapability.HISTORY),
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingGeneric](
|
||||
key="last_motion",
|
||||
translation_key="last_motion",
|
||||
category=["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
kind="motion",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
cls=HistoryRingSensor,
|
||||
value_fn=lambda device: last_event.get("created_at")
|
||||
if (last_event := _get_last_event(device.last_history, RingEventKind.MOTION))
|
||||
else None,
|
||||
extra_state_attributes_fn=lambda device: last_event_attrs
|
||||
if (
|
||||
last_event_attrs := _get_last_event_attrs(
|
||||
device.last_history, RingEventKind.MOTION
|
||||
)
|
||||
)
|
||||
else None,
|
||||
exists_fn=lambda device: device.has_capability(RingCapability.HISTORY),
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingDoorBell | RingChime](
|
||||
key="volume",
|
||||
translation_key="volume",
|
||||
category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
cls=RingSensor,
|
||||
value_fn=lambda device: device.volume,
|
||||
exists_fn=lambda device: isinstance(device, (RingDoorBell, RingChime)),
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingOther](
|
||||
key="doorbell_volume",
|
||||
translation_key="doorbell_volume",
|
||||
category=["other"],
|
||||
cls=RingSensor,
|
||||
value_fn=lambda device: device.doorbell_volume,
|
||||
exists_fn=lambda device: isinstance(device, RingOther),
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingOther](
|
||||
key="mic_volume",
|
||||
translation_key="mic_volume",
|
||||
category=["other"],
|
||||
cls=RingSensor,
|
||||
value_fn=lambda device: device.mic_volume,
|
||||
exists_fn=lambda device: isinstance(device, RingOther),
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingOther](
|
||||
key="voice_volume",
|
||||
translation_key="voice_volume",
|
||||
category=["other"],
|
||||
cls=RingSensor,
|
||||
value_fn=lambda device: device.voice_volume,
|
||||
exists_fn=lambda device: isinstance(device, RingOther),
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingGeneric](
|
||||
key="wifi_signal_category",
|
||||
translation_key="wifi_signal_category",
|
||||
category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"],
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
cls=HealthDataRingSensor,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda device: device.wifi_signal_category,
|
||||
),
|
||||
RingSensorEntityDescription(
|
||||
RingSensorEntityDescription[RingGeneric](
|
||||
key="wifi_signal_strength",
|
||||
translation_key="wifi_signal_strength",
|
||||
category=["chimes", "doorbots", "authorized_doorbots", "stickup_cams", "other"],
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
cls=HealthDataRingSensor,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda device: device.wifi_signal_strength,
|
||||
),
|
||||
)
|
||||
|
@ -1,15 +1,17 @@
|
||||
"""Component providing HA Siren support for Ring Chimes."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ring_doorbell.const import CHIME_TEST_SOUND_KINDS, KIND_DING
|
||||
from ring_doorbell import RingChime, RingEventKind
|
||||
|
||||
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, RING_DEVICES_COORDINATOR
|
||||
from . import RingData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity, exception_wrap
|
||||
|
||||
@ -22,32 +24,33 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> 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
|
||||
]
|
||||
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices_coordinator = ring_data.devices_coordinator
|
||||
|
||||
async_add_entities(
|
||||
RingChimeSiren(device, coordinator) for device in devices["chimes"]
|
||||
RingChimeSiren(device, devices_coordinator)
|
||||
for device in ring_data.devices.chimes
|
||||
)
|
||||
|
||||
|
||||
class RingChimeSiren(RingEntity, SirenEntity):
|
||||
"""Creates a siren to play the test chimes of a Chime device."""
|
||||
|
||||
_attr_available_tones = list(CHIME_TEST_SOUND_KINDS)
|
||||
_attr_available_tones = [RingEventKind.DING.value, RingEventKind.MOTION.value]
|
||||
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES
|
||||
_attr_translation_key = "siren"
|
||||
|
||||
def __init__(self, device, coordinator: RingDataCoordinator) -> None:
|
||||
_device: RingChime
|
||||
|
||||
def __init__(self, device: RingChime, coordinator: RingDataCoordinator) -> None:
|
||||
"""Initialize a Ring Chime siren."""
|
||||
super().__init__(device, coordinator)
|
||||
# Entity class attributes
|
||||
self._attr_unique_id = f"{self._device.id}-siren"
|
||||
|
||||
@exception_wrap
|
||||
def turn_on(self, **kwargs):
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Play the test sound on a Ring Chime device."""
|
||||
tone = kwargs.get(ATTR_TONE) or KIND_DING
|
||||
tone = kwargs.get(ATTR_TONE) or RingEventKind.DING.value
|
||||
|
||||
self._device.test_sound(kind=tone)
|
||||
|
@ -4,7 +4,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ring_doorbell import RingGeneric, RingStickUpCam
|
||||
from ring_doorbell import RingStickUpCam
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -12,7 +12,8 @@ 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, RING_DEVICES_COORDINATOR
|
||||
from . import RingData
|
||||
from .const import DOMAIN
|
||||
from .coordinator import RingDataCoordinator
|
||||
from .entity import RingEntity, exception_wrap
|
||||
|
||||
@ -33,14 +34,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> 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
|
||||
]
|
||||
ring_data: RingData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
devices_coordinator = ring_data.devices_coordinator
|
||||
|
||||
async_add_entities(
|
||||
SirenSwitch(device, coordinator)
|
||||
for device in devices["stickup_cams"]
|
||||
SirenSwitch(device, devices_coordinator)
|
||||
for device in ring_data.devices.stickup_cams
|
||||
if device.has_capability("siren")
|
||||
)
|
||||
|
||||
@ -48,8 +47,10 @@ async def async_setup_entry(
|
||||
class BaseRingSwitch(RingEntity, SwitchEntity):
|
||||
"""Represents a switch for controlling an aspect of a ring device."""
|
||||
|
||||
_device: RingStickUpCam
|
||||
|
||||
def __init__(
|
||||
self, device: RingGeneric, coordinator: RingDataCoordinator, device_type: str
|
||||
self, device: RingStickUpCam, coordinator: RingDataCoordinator, device_type: str
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(device, coordinator)
|
||||
@ -62,26 +63,27 @@ class SirenSwitch(BaseRingSwitch):
|
||||
|
||||
_attr_translation_key = "siren"
|
||||
|
||||
def __init__(self, device, coordinator: RingDataCoordinator) -> None:
|
||||
def __init__(
|
||||
self, device: RingStickUpCam, coordinator: RingDataCoordinator
|
||||
) -> None:
|
||||
"""Initialize the switch for a device with a siren."""
|
||||
super().__init__(device, coordinator, "siren")
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
self._attr_is_on = device.siren > 0
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Call update method."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
|
||||
if (device := self._get_coordinator_device()) and isinstance(
|
||||
device, RingStickUpCam
|
||||
):
|
||||
self._attr_is_on = device.siren > 0
|
||||
device = self._get_coordinator_data().get_stickup_cam(
|
||||
self._device.device_api_id
|
||||
)
|
||||
self._attr_is_on = device.siren > 0
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@exception_wrap
|
||||
def _set_switch(self, new_state):
|
||||
def _set_switch(self, new_state: int) -> None:
|
||||
"""Update switch state, and causes Home Assistant to correctly update."""
|
||||
self._device.siren = new_state
|
||||
|
||||
|
10
mypy.ini
10
mypy.ini
@ -3391,6 +3391,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.ring.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.rituals_perfume_genie.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -15,4 +15,4 @@ async def setup_platform(hass, platform):
|
||||
)
|
||||
with patch("homeassistant.components.ring.PLATFORMS", [platform]):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
Loading…
x
Reference in New Issue
Block a user