# vim:sw=4:ts=4:et: """Python Ring Doorbell module.""" from __future__ import annotations import logging import time from itertools import chain from typing import TYPE_CHECKING, Any, ClassVar from ring_doorbell import RingEvent from ring_doorbell.chime import RingChime from ring_doorbell.doorbot import RingDoorBell from ring_doorbell.exceptions import RingError from ring_doorbell.group import RingLightGroup from ring_doorbell.other import RingOther from ring_doorbell.stickup_cam import RingStickUpCam from .const import ( API_URI, API_VERSION, DEVICES_ENDPOINT, DINGS_ENDPOINT, GROUPS_ENDPOINT, INTERCOM_KINDS, NEW_SESSION_ENDPOINT, ) if TYPE_CHECKING: from collections.abc import Iterator, Mapping, Sequence from ring_doorbell.auth import Auth from ring_doorbell.generic import RingGeneric _logger = logging.getLogger(__name__) class Ring: """A Python Abstraction object to Ring Door Bell.""" def __init__(self, auth: Auth) -> None: """Initialize the Ring object.""" self.auth: Auth = auth self.session = None self.subscription = None self.devices_data: dict[str, dict[int, dict[str, Any]]] = {} self._devices: RingDevices | None = None self.chime_health_data = None self.doorbell_health_data = None self.dings_data: dict[Any, Any] = {} self.push_dings_data: list[RingEvent] = [] self.groups_data: dict[str, dict[str, Any]] = {} self.init_loop = None self.session_refresh_time: float | None = None async def async_update_data(self) -> None: """Update all data.""" await self._async_update_data() async def _async_update_data(self) -> None: if self.session is None: await self.async_create_session() await self.async_update_devices() await self.async_update_dings() await self.async_update_groups() def _add_event_to_dings_data(self, ring_event: RingEvent) -> None: # Purge expired push_dings now = time.time() self.push_dings_data = [ re for re in self.push_dings_data if now < re.now + re.expires_in ] self.push_dings_data.append(ring_event) async def async_create_session(self) -> None: """Create a new Ring session.""" session_post_data = { "device": { "hardware_id": self.auth.get_hardware_id(), "metadata": { "api_version": API_VERSION, "device_model": self.auth.get_device_model(), }, "os": "android", } } resp = await self._async_query( NEW_SESSION_ENDPOINT, method="POST", json=session_post_data, ) self.session = resp.json() self.session_refresh_time = time.monotonic() async def async_update_devices(self) -> None: """Update device data.""" if self.session is None: await self.async_create_session() resp = await self._async_query(DEVICES_ENDPOINT) data: dict[Any, Any] = resp.json() # Index data by device ID. self.devices_data = { device_type: {obj["id"]: obj for obj in devices} for device_type, devices in data.items() } async def async_update_dings(self) -> None: """Update dings data.""" if self.session is None: await self.async_create_session() resp = await self._async_query(DINGS_ENDPOINT) self.dings_data = resp.json() async def async_update_groups(self) -> None: """Update groups data.""" if self.session is None: await self.async_create_session() # Get all locations locations = set() devices = self.devices() for device_type in devices: for dev in devices[device_type]: if dev.location_id is not None: locations.add(dev.location_id) # Query for groups self.groups_data = {} for location in locations: resp = await self._async_query(GROUPS_ENDPOINT.format(location)) data = resp.json() if data["device_groups"]: for group in data["device_groups"]: self.groups_data[group["device_group_id"]] = group async def async_query( # noqa: PLR0913 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, base_uri: str = API_URI, ) -> Auth.Response: """Query data from Ring API.""" if self.session is None: await self.async_create_session() return await self._async_query( url, method, extra_params, data, json, timeout, base_uri ) async def _async_query( # noqa: PLR0913 self, url: str, method: str = "GET", extra_params: dict[str, Any] | None = None, data: bytes | None = None, json: dict[Any, Any] | None = None, timeout: float | None = None, base_uri: str = API_URI, ) -> Auth.Response: _logger.debug( "url: %s\nmethod: %s\njson: %s\ndata: %s\n extra_params: %s", url, method, json, data, extra_params, ) return await self.auth.async_query( base_uri + url, method=method, extra_params=extra_params, data=data, json=json, timeout=timeout, ) def devices(self) -> RingDevices: """Get all devices.""" if not self._devices: self._devices = RingDevices(self, self.devices_data) return self._devices def get_device_list(self) -> Sequence[RingGeneric]: """Get a combined list of all devices.""" devices = self.devices() return list( chain( devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"], devices["chimes"], devices["other"], ) ) def get_device_by_name(self, device_name: str) -> RingGeneric | None: """Return a device using it's name.""" all_devices = self.get_device_list() names_to_idx = {device.name: idx for (idx, device) in enumerate(all_devices)} return ( None if device_name not in names_to_idx else all_devices[names_to_idx[device_name]] ) def get_video_device_by_name(self, device_name: str) -> RingDoorBell | None: """Return a device using it's name.""" video_devices = self.video_devices() names_to_idx = {device.name: idx for (idx, device) in enumerate(video_devices)} return ( None if device_name not in names_to_idx else video_devices[names_to_idx[device_name]] ) def get_device_by_api_id(self, device_api_id: int) -> RingGeneric | None: """Return a device using it's id.""" all_devices = self.get_device_list() api_id_to_idx = { device.device_api_id: idx for (idx, device) in enumerate(all_devices) } return ( None if device_api_id not in api_id_to_idx else all_devices[api_id_to_idx[device_api_id]] ) def video_devices(self) -> Sequence[RingDoorBell]: """Get all devices.""" devices = self.devices() return list( chain(devices.doorbots, devices.authorized_doorbots, devices.stickup_cams) ) def groups(self) -> Mapping[str, RingLightGroup]: """Get all groups.""" groups = {} for group_id in self.groups_data: groups[group_id] = RingLightGroup(self, group_id) return groups def active_alerts(self) -> Sequence[RingEvent]: """Get active alerts.""" now = time.time() # Purge expired push_dings self.push_dings_data = [ re for re in self.push_dings_data if now < re.now + re.expires_in ] # Get unique id dictionary alerts: dict[tuple[int, int, str], RingEvent] = {} for re in self.push_dings_data: key = (re.doorbot_id, re.id, re.kind) if key not in alerts or re.now > alerts[key].now: alerts[key] = re for ding_data in self.dings_data: expires_at = ding_data.get("now") + ding_data.get("expires_in") if now < expires_at: re = RingEvent( id=ding_data["id"], doorbot_id=ding_data["doorbot_id"], device_name=ding_data["doorbot_description"], device_kind=ding_data["device_kind"], now=ding_data["now"], expires_in=ding_data["expires_in"], kind=ding_data["kind"], state=ding_data["state"], ) key = (re.doorbot_id, re.id, re.kind) if key not in alerts or re.now > alerts[key].now: alerts[key] = re return list(alerts.values()) DEPRECATED_API_QUERIES: ClassVar = { "update_devices", "update_data", "update_dings", "update_groups", "create_session", "query", } if not TYPE_CHECKING: def __getattr__(self, name: str) -> Any: """Get a deprecated attribute or raise an error.""" if name in self.DEPRECATED_API_QUERIES: return self.auth._dep_handler.get_api_query(self, name) # noqa: SLF001 msg = f"{self.__class__.__name__} has no attribute {name!r}" raise AttributeError(msg) class RingDevices: """Class to represent collection of devices.""" def __init__( self, ring: Ring, devices_data: dict[str, dict[int, dict[str, Any]]] ) -> None: """Initialise the devices from the api response.""" self._stickup_cams: list[RingStickUpCam] = [] self._chimes: list[RingChime] = [] self._doorbots: list[RingDoorBell] = [] self._authorized_doorbots: list[RingDoorBell] = [] self._other: list[RingOther] = [] for device_type, devices in devices_data.items(): if device_type == "stickup_cams": self._stickup_cams = [ RingStickUpCam(ring, device_id) for device_id in devices ] if device_type == "chimes": self._chimes = [RingChime(ring, device_id) for device_id in devices] if device_type == "doorbots": self._doorbots = [ RingDoorBell(ring, device_id) for device_id in devices ] if device_type == "authorized_doorbots": self._authorized_doorbots = [ RingDoorBell(ring, device_id, shared=True) for device_id in devices ] if device_type == "other": self._other = [ RingOther(ring, device_id, shared=True) for device_id, device in devices.items() if (device_kind := device.get("kind")) and device_kind in INTERCOM_KINDS ] self._all_devices = { device.id: device for device in chain( self._stickup_cams, self._chimes, self._doorbots, self._authorized_doorbots, self._other, ) } def __getitem__(self, device_type: str) -> Sequence[RingGeneric]: """Get a generic device by type.""" if device_type == "stickup_cams": return self._stickup_cams if device_type == "chimes": return self._chimes if device_type == "doorbots": return self._doorbots if device_type == "authorized_doorbots": return self._authorized_doorbots if device_type == "other": return self._other if device_type == "intercoms": return self._other msg = f"Invalid device_type {device_type}" raise RingError(msg) def __iter__(self) -> Iterator[str]: """Device type iterator.""" return iter( ["stickup_cams", "chimes", "doorbots", "authorized_doorbots", "other"] ) @property def stickup_cams(self) -> Sequence[RingStickUpCam]: """The stickup cams.""" return self._stickup_cams @property def chimes(self) -> Sequence[RingChime]: """The chimes.""" return self._chimes @property def doorbots(self) -> Sequence[RingDoorBell]: """The doorbots.""" return self._doorbots @property def authorized_doorbots(self) -> Sequence[RingDoorBell]: """The authorized_doorbots.""" return self._authorized_doorbots @property def doorbells(self) -> Sequence[RingDoorBell]: """The doorbells, i.e. doorbots and authorized_doorbots combined.""" return self._doorbots + self._authorized_doorbots @property def other(self) -> Sequence[RingOther]: """The other devices, i.e. intercoms.""" return self._other @property def all_devices(self) -> Sequence[RingGeneric]: """All devices combined.""" return list(self._all_devices.values()) @property def video_devices(self) -> Sequence[RingDoorBell]: """The video devices, i.e. doorbells and stickup_cams.""" return [*self._doorbots, *self._authorized_doorbots, *self._stickup_cams] def get_device(self, device_api_id: int) -> RingGeneric: """Get device by api id.""" if device := self._all_devices.get(device_api_id): return device msg = f"device with id {device_api_id} not found" raise RingError(msg) def get_doorbell(self, device_api_id: int) -> RingDoorBell: """Get doorbell by api id.""" if ( (device := self._all_devices.get(device_api_id)) and isinstance(device, RingDoorBell) and not issubclass(device.__class__, RingDoorBell) ): return device msg = f"doorbell with id {device_api_id} not found" raise RingError(msg) def get_stickup_cam(self, device_api_id: int) -> RingStickUpCam: """Get stickup_cam by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingStickUpCam ): return device msg = f"stickup_cam with id {device_api_id} not found" raise RingError(msg) def get_chime(self, device_api_id: int) -> RingChime: """Get chime by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingChime ): return device msg = f"chime with id {device_api_id} not found" raise RingError(msg) def get_other(self, device_api_id: int) -> RingOther: """Get other device by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingOther ): return device msg = f"other device with id {device_api_id} not found" raise RingError(msg) def get_video_device(self, device_api_id: int) -> RingDoorBell: """Get video capable device by api id.""" if (device := self._all_devices.get(device_api_id)) and isinstance( device, RingDoorBell ): return device msg = f"video capable device with id {device_api_id} not found" raise RingError(msg) def __str__(self) -> str: """Get string representation of devices.""" d = {dev_type: self.__getitem__(dev_type) for dev_type in self.__iter__()} return "{" + "\n".join(f"{k!r}: {v!r}," for k, v in d.items()) + "}" def __repr__(self) -> str: """Return repr of devices.""" d = {dev_type: self.__getitem__(dev_type) for dev_type in self.__iter__()} return repr(d)