hass-core/homeassistant/components/ring/camera.py

193 lines
6 KiB
Python
Raw Normal View History

"""This component provides support to the Ring Door Bell camera."""
import asyncio
from datetime import timedelta
import logging
2019-12-05 06:13:28 +01:00
from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_ATTRIBUTION
2019-12-05 06:13:28 +01:00
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.dispatcher import async_dispatcher_connect
2019-12-05 06:13:28 +01:00
from homeassistant.util import dt as dt_util
2019-07-31 12:25:30 -07:00
from . import (
ATTRIBUTION,
DATA_RING_DOORBELLS,
DATA_RING_STICKUP_CAMS,
NOTIFICATION_ID,
SIGNAL_UPDATE_RING,
)
2019-07-31 12:25:30 -07:00
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
_LOGGER = logging.getLogger(__name__)
2019-07-31 12:25:30 -07:00
NOTIFICATION_TITLE = "Ring Camera Setup"
2019-07-31 12:25:30 -07:00
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string}
)
2018-09-26 08:52:22 +02:00
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Ring Door Bell and StickUp Camera."""
ring_doorbell = hass.data[DATA_RING_DOORBELLS]
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
cams = []
cams_no_plan = []
for camera in ring_doorbell + ring_stickup_cams:
if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
cams_no_plan.append(camera)
# show notification for all cameras without an active subscription
if cams_no_plan:
2019-07-31 12:25:30 -07:00
cameras = str(", ".join([camera.name for camera in cams_no_plan]))
2019-07-31 12:25:30 -07:00
err_msg = (
"""A Ring Protect Plan is required for the"""
""" following cameras: {}.""".format(cameras)
)
_LOGGER.error(err_msg)
2018-09-26 08:52:22 +02:00
hass.components.persistent_notification.create(
2019-07-31 12:25:30 -07:00
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(err_msg),
title=NOTIFICATION_TITLE,
2019-07-31 12:25:30 -07:00
notification_id=NOTIFICATION_ID,
)
2018-09-26 08:52:22 +02:00
add_entities(cams, True)
return True
class RingCam(Camera):
"""An implementation of a Ring Door Bell camera."""
def __init__(self, hass, camera, device_info):
"""Initialize a Ring Door Bell camera."""
super().__init__()
self._camera = camera
self._hass = hass
self._name = self._camera.name
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_video_id = self._camera.last_recording_id
self._video_url = self._camera.recording_url(self._last_video_id)
self._utcnow = dt_util.utcnow()
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
async def async_added_to_hass(self):
"""Register callbacks."""
2019-07-31 12:25:30 -07:00
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
_LOGGER.debug("Updating Ring camera %s (callback)", self.name)
@property
def name(self):
"""Return the name of this camera."""
return self._name
2018-10-16 01:06:00 -07:00
@property
def unique_id(self):
"""Return a unique ID."""
return self._camera.id
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
2019-07-31 12:25:30 -07:00
"device_id": self._camera.id,
"firmware": self._camera.firmware,
"kind": self._camera.kind,
"timezone": self._camera.timezone,
"type": self._camera.family,
"video_url": self._video_url,
"last_video_id": self._last_video_id,
}
async def async_camera_image(self):
"""Return a still image response from the camera."""
2019-07-31 12:25:30 -07:00
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
if self._video_url is None:
return
2019-07-31 12:25:30 -07:00
image = await asyncio.shield(
ffmpeg.get_image(
self._video_url,
output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments,
)
)
return 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.binary, loop=self.hass.loop)
2019-07-31 12:25:30 -07:00
await stream.open_camera(self._video_url, extra_cmd=self._ffmpeg_arguments)
try:
2019-03-27 07:55:05 +01:00
stream_reader = await stream.get_reader()
return await async_aiohttp_proxy_stream(
2019-07-31 12:25:30 -07:00
self.hass,
request,
stream_reader,
self._ffmpeg.ffmpeg_stream_content_type,
)
finally:
await stream.close()
@property
def should_poll(self):
"""Return False, updates are controlled via the hub."""
return False
def update(self):
"""Update camera entity and refresh attributes."""
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
self._utcnow = dt_util.utcnow()
try:
last_event = self._camera.history(limit=1)[0]
except (IndexError, TypeError):
return
2019-07-31 12:25:30 -07:00
last_recording_id = last_event["id"]
video_status = last_event["recording"]["status"]
2019-07-31 12:25:30 -07:00
if video_status == "ready" and (
self._last_video_id != last_recording_id or self._utcnow >= self._expires_at
):
video_url = self._camera.recording_url(last_recording_id)
if video_url:
_LOGGER.info("Ring DoorBell properties refreshed")
# update attributes if new video or if URL has expired
self._last_video_id = last_recording_id
self._video_url = video_url
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow