"""This component provides support to the Ring Door Bell camera.""" from __future__ import annotations from datetime import timedelta from itertools import chain import logging from haffmpeg.camera import CameraMjpeg import requests from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, callback 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 . import ATTRIBUTION, DOMAIN from .entity import RingEntityMixin FORCE_REFRESH_INTERVAL = timedelta(minutes=3) _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Ring Door Bell and StickUp Camera.""" devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] cams = [] for camera in chain( devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] ): if not camera.has_subscription: continue cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) async_add_entities(cams) class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" def __init__(self, config_entry_id, ffmpeg_manager, device): """Initialize a Ring Door Bell camera.""" super().__init__(config_entry_id, device) self._name = self._device.name self._ffmpeg_manager = ffmpeg_manager self._last_event = None self._last_video_id = None self._video_url = None self._image = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL async def async_added_to_hass(self): """Register callbacks.""" await super().async_added_to_hass() await self.ring_objects["history_data"].async_track_device( self._device, self._history_update_callback ) async def async_will_remove_from_hass(self): """Disconnect callbacks.""" await super().async_will_remove_from_hass() self.ring_objects["history_data"].async_untrack_device( self._device, self._history_update_callback ) @callback def _history_update_callback(self, history_data): """Call update method.""" if history_data: self._last_event = history_data[0] self.async_schedule_update_ha_state(True) else: self._last_event = None self._last_video_id = None self._video_url = None self._image = None self._expires_at = dt_util.utcnow() self.async_write_ha_state() @property def name(self): """Return the name of this camera.""" return self._name @property def unique_id(self): """Return a unique ID.""" return self._device.id @property def extra_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, "video_url": self._video_url, "last_video_id": self._last_video_id, } async def async_camera_image( 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: image = await ffmpeg.async_get_image( self.hass, self._video_url, width=width, height=height, ) if image: self._image = image return self._image async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" if self._video_url is None: return stream = CameraMjpeg(self._ffmpeg_manager.binary) await stream.open_camera(self._video_url) try: stream_reader = await stream.get_reader() return await async_aiohttp_proxy_stream( self.hass, request, stream_reader, self._ffmpeg_manager.ffmpeg_stream_content_type, ) finally: await stream.close() async def async_update(self): """Update camera entity and refresh attributes.""" if self._last_event is None: return if self._last_event["recording"]["status"] != "ready": return utcnow = dt_util.utcnow() if self._last_video_id == self._last_event["id"] and utcnow <= self._expires_at: return if self._last_video_id != self._last_event["id"]: self._image = None try: video_url = await self.hass.async_add_executor_job( self._device.recording_url, self._last_event["id"] ) except requests.Timeout: _LOGGER.warning( "Time out fetching recording url for camera %s", self.entity_id ) video_url = None if video_url: self._last_video_id = self._last_event["id"] self._video_url = video_url self._expires_at = FORCE_REFRESH_INTERVAL + utcnow