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