2017-10-21 14:08:40 +00:00
|
|
|
"""
|
|
|
|
This component provides support to the Ring Door Bell camera.
|
|
|
|
|
|
|
|
For more details about this platform, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/camera.ring/
|
|
|
|
"""
|
|
|
|
import asyncio
|
2017-11-09 00:01:20 +00:00
|
|
|
from datetime import timedelta
|
2019-03-21 05:56:46 +00:00
|
|
|
import logging
|
2017-10-21 14:08:40 +00:00
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-03-21 05:56:46 +00:00
|
|
|
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
2017-10-21 14:08:40 +00:00
|
|
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
|
|
|
from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL
|
2019-03-21 05:56:46 +00:00
|
|
|
from homeassistant.helpers import config_validation as cv
|
2017-10-21 14:08:40 +00:00
|
|
|
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
|
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
|
2019-03-21 05:56:46 +00:00
|
|
|
from . import ATTRIBUTION, DATA_RING, NOTIFICATION_ID
|
|
|
|
|
2017-10-21 14:08:40 +00:00
|
|
|
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
|
|
|
|
|
|
|
DEPENDENCIES = ['ring', 'ffmpeg']
|
|
|
|
|
2017-11-09 00:01:20 +00:00
|
|
|
FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
|
|
|
|
|
2017-10-21 14:08:40 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2017-11-25 11:15:12 +00:00
|
|
|
NOTIFICATION_TITLE = 'Ring Camera Setup'
|
|
|
|
|
2017-10-21 14:08:40 +00:00
|
|
|
SCAN_INTERVAL = timedelta(seconds=90)
|
|
|
|
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|
|
|
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
2019-02-14 21:09:22 +00:00
|
|
|
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
|
2017-10-21 14:08:40 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
2018-09-26 06:52:22 +00:00
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
2017-10-21 14:08:40 +00:00
|
|
|
"""Set up a Ring Door Bell and StickUp Camera."""
|
|
|
|
ring = hass.data[DATA_RING]
|
|
|
|
|
|
|
|
cams = []
|
2017-11-25 11:15:12 +00:00
|
|
|
cams_no_plan = []
|
2017-10-21 14:08:40 +00:00
|
|
|
for camera in ring.doorbells:
|
2017-11-25 11:15:12 +00:00
|
|
|
if camera.has_subscription:
|
|
|
|
cams.append(RingCam(hass, camera, config))
|
|
|
|
else:
|
|
|
|
cams_no_plan.append(camera)
|
2017-10-21 14:08:40 +00:00
|
|
|
|
|
|
|
for camera in ring.stickup_cams:
|
2017-11-25 11:15:12 +00:00
|
|
|
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:
|
|
|
|
cameras = str(', '.join([camera.name for camera in cams_no_plan]))
|
|
|
|
|
|
|
|
err_msg = '''A Ring Protect Plan is required for the''' \
|
|
|
|
''' following cameras: {}.'''.format(cameras)
|
|
|
|
|
|
|
|
_LOGGER.error(err_msg)
|
2018-09-26 06:52:22 +00:00
|
|
|
hass.components.persistent_notification.create(
|
2017-11-25 11:15:12 +00:00
|
|
|
'Error: {}<br />'
|
|
|
|
'You will need to restart hass after fixing.'
|
|
|
|
''.format(err_msg),
|
|
|
|
title=NOTIFICATION_TITLE,
|
|
|
|
notification_id=NOTIFICATION_ID)
|
2017-10-21 14:08:40 +00:00
|
|
|
|
2018-09-26 06:52:22 +00:00
|
|
|
add_entities(cams, True)
|
2017-10-21 14:08:40 +00:00
|
|
|
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(RingCam, self).__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)
|
2017-11-09 00:01:20 +00:00
|
|
|
self._utcnow = dt_util.utcnow()
|
|
|
|
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
2017-10-21 14:08:40 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of this camera."""
|
|
|
|
return self._name
|
|
|
|
|
2018-10-16 08:06:00 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return a unique ID."""
|
|
|
|
return self._camera.id
|
|
|
|
|
2017-10-21 14:08:40 +00:00
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the state attributes."""
|
|
|
|
return {
|
2019-02-14 21:09:22 +00:00
|
|
|
ATTR_ATTRIBUTION: ATTRIBUTION,
|
2017-10-21 14:08:40 +00: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,
|
|
|
|
}
|
|
|
|
|
2018-10-01 06:50:05 +00:00
|
|
|
async def async_camera_image(self):
|
2017-10-21 14:08:40 +00:00
|
|
|
"""Return a still image response from the camera."""
|
|
|
|
from haffmpeg import ImageFrame, IMAGE_JPEG
|
|
|
|
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
|
|
|
|
|
|
|
|
if self._video_url is None:
|
|
|
|
return
|
|
|
|
|
2018-10-01 06:50:05 +00:00
|
|
|
image = await asyncio.shield(ffmpeg.get_image(
|
2017-10-21 14:08:40 +00:00
|
|
|
self._video_url, output_format=IMAGE_JPEG,
|
|
|
|
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
|
|
|
return image
|
|
|
|
|
2018-10-01 06:50:05 +00:00
|
|
|
async def handle_async_mjpeg_stream(self, request):
|
2017-10-21 14:08:40 +00:00
|
|
|
"""Generate an HTTP MJPEG stream from the camera."""
|
|
|
|
from haffmpeg import CameraMjpeg
|
|
|
|
|
|
|
|
if self._video_url is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
|
2018-10-01 06:50:05 +00:00
|
|
|
await stream.open_camera(
|
2017-10-21 14:08:40 +00:00
|
|
|
self._video_url, extra_cmd=self._ffmpeg_arguments)
|
|
|
|
|
2018-11-01 08:28:23 +00:00
|
|
|
try:
|
|
|
|
return await async_aiohttp_proxy_stream(
|
|
|
|
self.hass, request, stream,
|
2019-02-04 17:57:22 +00:00
|
|
|
self._ffmpeg.ffmpeg_stream_content_type)
|
2018-11-01 08:28:23 +00:00
|
|
|
finally:
|
|
|
|
await stream.close()
|
2017-10-21 14:08:40 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
"""Update the image periodically."""
|
|
|
|
return True
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
"""Update camera entity and refresh attributes."""
|
2017-11-09 00:01:20 +00:00
|
|
|
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
|
2017-10-21 14:08:40 +00:00
|
|
|
|
2017-11-09 00:01:20 +00:00
|
|
|
self._camera.update()
|
2017-10-21 14:08:40 +00:00
|
|
|
self._utcnow = dt_util.utcnow()
|
|
|
|
|
2017-11-09 00:01:20 +00:00
|
|
|
last_recording_id = self._camera.last_recording_id
|
2017-10-21 14:08:40 +00:00
|
|
|
|
2017-11-09 00:01:20 +00:00
|
|
|
if self._last_video_id != last_recording_id or \
|
|
|
|
self._utcnow >= self._expires_at:
|
|
|
|
|
|
|
|
_LOGGER.info("Ring DoorBell properties refreshed")
|
|
|
|
|
|
|
|
# update attributes if new video or if URL has expired
|
|
|
|
self._last_video_id = self._camera.last_recording_id
|
2017-10-21 14:08:40 +00:00
|
|
|
self._video_url = self._camera.recording_url(self._last_video_id)
|
2017-11-09 00:01:20 +00:00
|
|
|
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|