2015-06-05 12:51:29 +00:00
|
|
|
"""
|
2016-02-17 07:52:05 +00:00
|
|
|
Component to interface with cameras.
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2015-11-09 12:12:18 +00:00
|
|
|
For more details about this component, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/camera/
|
2015-06-05 12:51:29 +00:00
|
|
|
"""
|
2016-10-24 06:48:01 +00:00
|
|
|
import asyncio
|
2018-05-03 20:02:59 +00:00
|
|
|
import base64
|
2017-01-28 19:51:35 +00:00
|
|
|
import collections
|
2017-03-31 09:55:22 +00:00
|
|
|
from contextlib import suppress
|
2017-01-05 23:16:12 +00:00
|
|
|
from datetime import timedelta
|
2015-06-05 12:51:29 +00:00
|
|
|
import logging
|
2017-01-10 15:01:04 +00:00
|
|
|
import hashlib
|
2017-01-28 19:51:35 +00:00
|
|
|
from random import SystemRandom
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2018-05-03 20:02:59 +00:00
|
|
|
import attr
|
2016-10-24 06:48:01 +00:00
|
|
|
from aiohttp import web
|
2017-01-14 07:18:03 +00:00
|
|
|
import async_timeout
|
2017-07-01 04:06:56 +00:00
|
|
|
import voluptuous as vol
|
2015-11-29 21:49:05 +00:00
|
|
|
|
2017-01-28 19:51:35 +00:00
|
|
|
from homeassistant.core import callback
|
2018-07-24 17:13:26 +00:00
|
|
|
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \
|
|
|
|
SERVICE_TURN_ON
|
2017-01-14 07:18:03 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2017-07-16 17:14:46 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2015-06-05 12:51:29 +00:00
|
|
|
from homeassistant.helpers.entity import Entity
|
2015-11-29 21:49:05 +00:00
|
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
2016-03-28 01:48:51 +00:00
|
|
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
2016-11-25 21:04:06 +00:00
|
|
|
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
2018-05-03 20:02:59 +00:00
|
|
|
from homeassistant.components import websocket_api
|
2017-07-01 04:06:56 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2015-06-05 12:51:29 +00:00
|
|
|
|
|
|
|
DOMAIN = 'camera'
|
|
|
|
DEPENDENCIES = ['http']
|
2017-10-29 22:14:26 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
SERVICE_ENABLE_MOTION = 'enable_motion_detection'
|
|
|
|
SERVICE_DISABLE_MOTION = 'disable_motion_detection'
|
|
|
|
SERVICE_SNAPSHOT = 'snapshot'
|
|
|
|
|
2017-01-05 23:16:12 +00:00
|
|
|
SCAN_INTERVAL = timedelta(seconds=30)
|
2015-06-05 12:51:29 +00:00
|
|
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|
|
|
|
2017-10-29 22:14:26 +00:00
|
|
|
ATTR_FILENAME = 'filename'
|
|
|
|
|
2016-02-07 09:08:55 +00:00
|
|
|
STATE_RECORDING = 'recording'
|
2015-06-05 12:51:29 +00:00
|
|
|
STATE_STREAMING = 'streaming'
|
|
|
|
STATE_IDLE = 'idle'
|
|
|
|
|
2018-07-24 17:13:26 +00:00
|
|
|
# Bitfield of features supported by the camera entity
|
|
|
|
SUPPORT_ON_OFF = 1
|
|
|
|
|
2017-06-25 19:25:14 +00:00
|
|
|
DEFAULT_CONTENT_TYPE = 'image/jpeg'
|
2016-05-27 08:29:48 +00:00
|
|
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
2015-07-10 08:03:46 +00:00
|
|
|
|
2017-01-28 19:51:35 +00:00
|
|
|
TOKEN_CHANGE_INTERVAL = timedelta(minutes=5)
|
|
|
|
_RND = SystemRandom()
|
|
|
|
|
2018-05-01 18:49:33 +00:00
|
|
|
FALLBACK_STREAM_INTERVAL = 1 # seconds
|
|
|
|
MIN_STREAM_INTERVAL = 0.5 # seconds
|
|
|
|
|
2017-07-01 04:06:56 +00:00
|
|
|
CAMERA_SERVICE_SCHEMA = vol.Schema({
|
|
|
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
|
|
|
})
|
|
|
|
|
2017-10-29 22:14:26 +00:00
|
|
|
CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
|
|
|
|
vol.Required(ATTR_FILENAME): cv.template
|
|
|
|
})
|
|
|
|
|
2018-05-03 20:02:59 +00:00
|
|
|
WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail'
|
|
|
|
SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
2018-07-13 13:31:20 +00:00
|
|
|
vol.Required('type'): WS_TYPE_CAMERA_THUMBNAIL,
|
|
|
|
vol.Required('entity_id'): cv.entity_id
|
2018-05-03 20:02:59 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s
|
|
|
|
class Image:
|
|
|
|
"""Represent an image."""
|
|
|
|
|
|
|
|
content_type = attr.ib(type=str)
|
|
|
|
content = attr.ib(type=bytes)
|
|
|
|
|
2017-07-01 04:06:56 +00:00
|
|
|
|
2017-10-29 22:14:26 +00:00
|
|
|
@bind_hass
|
2018-05-03 20:02:59 +00:00
|
|
|
async def async_get_image(hass, entity_id, timeout=10):
|
2018-01-27 19:58:27 +00:00
|
|
|
"""Fetch an image from a camera entity."""
|
2018-09-06 08:51:16 +00:00
|
|
|
camera = _get_camera_from_entity_id(hass, entity_id)
|
|
|
|
|
|
|
|
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
|
|
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
|
|
|
image = await camera.async_camera_image()
|
|
|
|
|
|
|
|
if image:
|
|
|
|
return Image(camera.content_type, image)
|
|
|
|
|
|
|
|
raise HomeAssistantError('Unable to get image')
|
|
|
|
|
|
|
|
|
|
|
|
@bind_hass
|
|
|
|
async def async_get_mjpeg_stream(hass, request, entity_id):
|
|
|
|
"""Fetch an mjpeg stream from a camera entity."""
|
|
|
|
camera = _get_camera_from_entity_id(hass, entity_id)
|
|
|
|
|
|
|
|
return await camera.handle_async_mjpeg_stream(request)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_get_still_stream(request, image_cb, content_type, interval):
|
|
|
|
"""Generate an HTTP MJPEG stream from camera images.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
response = web.StreamResponse()
|
|
|
|
response.content_type = ('multipart/x-mixed-replace; '
|
|
|
|
'boundary=--frameboundary')
|
|
|
|
await response.prepare(request)
|
|
|
|
|
|
|
|
async def write_to_mjpeg_stream(img_bytes):
|
|
|
|
"""Write image to stream."""
|
|
|
|
await response.write(bytes(
|
|
|
|
'--frameboundary\r\n'
|
|
|
|
'Content-Type: {}\r\n'
|
|
|
|
'Content-Length: {}\r\n\r\n'.format(
|
|
|
|
content_type, len(img_bytes)),
|
|
|
|
'utf-8') + img_bytes + b'\r\n')
|
|
|
|
|
|
|
|
last_image = None
|
|
|
|
|
|
|
|
while True:
|
|
|
|
img_bytes = await image_cb()
|
|
|
|
if not img_bytes:
|
|
|
|
break
|
|
|
|
|
|
|
|
if img_bytes != last_image:
|
|
|
|
await write_to_mjpeg_stream(img_bytes)
|
|
|
|
|
|
|
|
# Chrome seems to always ignore first picture,
|
|
|
|
# print it twice.
|
|
|
|
if last_image is None:
|
|
|
|
await write_to_mjpeg_stream(img_bytes)
|
|
|
|
last_image = img_bytes
|
|
|
|
|
|
|
|
await asyncio.sleep(interval)
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
def _get_camera_from_entity_id(hass, entity_id):
|
|
|
|
"""Get camera component from entity_id."""
|
2018-05-03 20:02:59 +00:00
|
|
|
component = hass.data.get(DOMAIN)
|
2017-01-14 07:18:03 +00:00
|
|
|
|
2018-05-03 20:02:59 +00:00
|
|
|
if component is None:
|
2018-08-19 20:29:08 +00:00
|
|
|
raise HomeAssistantError('Camera component not set up')
|
2017-01-14 07:18:03 +00:00
|
|
|
|
2018-05-03 20:02:59 +00:00
|
|
|
camera = component.get_entity(entity_id)
|
2017-01-14 07:18:03 +00:00
|
|
|
|
2018-05-03 20:02:59 +00:00
|
|
|
if camera is None:
|
|
|
|
raise HomeAssistantError('Camera not found')
|
2017-01-14 07:18:03 +00:00
|
|
|
|
2018-07-24 17:13:26 +00:00
|
|
|
if not camera.is_on:
|
|
|
|
raise HomeAssistantError('Camera is off')
|
|
|
|
|
2018-09-06 08:51:16 +00:00
|
|
|
return camera
|
2017-01-14 07:18:03 +00:00
|
|
|
|
|
|
|
|
2018-06-16 14:49:11 +00:00
|
|
|
async def async_setup(hass, config):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Set up the camera component."""
|
2018-05-03 20:02:59 +00:00
|
|
|
component = hass.data[DOMAIN] = \
|
|
|
|
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2018-01-23 06:54:41 +00:00
|
|
|
hass.http.register_view(CameraImageView(component))
|
|
|
|
hass.http.register_view(CameraMjpegStream(component))
|
2018-05-03 20:02:59 +00:00
|
|
|
hass.components.websocket_api.async_register_command(
|
|
|
|
WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail,
|
|
|
|
SCHEMA_WS_CAMERA_THUMBNAIL
|
|
|
|
)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2018-06-16 14:49:11 +00:00
|
|
|
await component.async_setup(config)
|
2017-01-28 19:51:35 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def update_tokens(time):
|
|
|
|
"""Update tokens of the entities."""
|
2018-01-23 06:54:41 +00:00
|
|
|
for entity in component.entities:
|
2017-01-28 19:51:35 +00:00
|
|
|
entity.async_update_token()
|
|
|
|
hass.async_add_job(entity.async_update_ha_state())
|
|
|
|
|
2017-10-29 22:14:26 +00:00
|
|
|
hass.helpers.event.async_track_time_interval(
|
|
|
|
update_tokens, TOKEN_CHANGE_INTERVAL)
|
2017-07-01 04:06:56 +00:00
|
|
|
|
2018-08-16 12:28:59 +00:00
|
|
|
component.async_register_entity_service(
|
|
|
|
SERVICE_ENABLE_MOTION, CAMERA_SERVICE_SCHEMA,
|
|
|
|
'async_enable_motion_detection'
|
|
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
|
|
SERVICE_DISABLE_MOTION, CAMERA_SERVICE_SCHEMA,
|
|
|
|
'async_disable_motion_detection'
|
|
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
|
|
SERVICE_TURN_OFF, CAMERA_SERVICE_SCHEMA,
|
|
|
|
'async_turn_off'
|
|
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
|
|
SERVICE_TURN_ON, CAMERA_SERVICE_SCHEMA,
|
|
|
|
'async_turn_on'
|
|
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
|
|
SERVICE_SNAPSHOT, CAMERA_SERVICE_SNAPSHOT,
|
|
|
|
async_handle_snapshot_service
|
|
|
|
)
|
2017-07-01 04:06:56 +00:00
|
|
|
|
2015-07-10 09:42:22 +00:00
|
|
|
return True
|
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2018-06-13 15:14:52 +00:00
|
|
|
async def async_setup_entry(hass, entry):
|
2018-08-19 20:29:08 +00:00
|
|
|
"""Set up a config entry."""
|
2018-06-13 15:14:52 +00:00
|
|
|
return await hass.data[DOMAIN].async_setup_entry(entry)
|
|
|
|
|
|
|
|
|
|
|
|
async def async_unload_entry(hass, entry):
|
|
|
|
"""Unload a config entry."""
|
|
|
|
return await hass.data[DOMAIN].async_unload_entry(entry)
|
|
|
|
|
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
class Camera(Entity):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""The base class for camera entities."""
|
2016-03-07 19:29:54 +00:00
|
|
|
|
2015-07-11 06:17:12 +00:00
|
|
|
def __init__(self):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Initialize a camera."""
|
2015-07-11 06:17:12 +00:00
|
|
|
self.is_streaming = False
|
2017-06-25 19:25:14 +00:00
|
|
|
self.content_type = DEFAULT_CONTENT_TYPE
|
2017-01-28 19:51:35 +00:00
|
|
|
self.access_tokens = collections.deque([], 2)
|
|
|
|
self.async_update_token()
|
2016-05-15 23:59:27 +00:00
|
|
|
|
2016-02-17 07:52:05 +00:00
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
"""No need to poll cameras."""
|
|
|
|
return False
|
|
|
|
|
2016-02-24 06:41:24 +00:00
|
|
|
@property
|
|
|
|
def entity_picture(self):
|
|
|
|
"""Return a link to the camera feed as entity picture."""
|
2017-01-28 19:51:35 +00:00
|
|
|
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
|
2016-02-24 06:41:24 +00:00
|
|
|
|
2018-07-24 17:13:26 +00:00
|
|
|
@property
|
|
|
|
def supported_features(self):
|
|
|
|
"""Flag supported features."""
|
|
|
|
return 0
|
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
@property
|
|
|
|
def is_recording(self):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Return true if the device is recording."""
|
2015-06-05 12:51:29 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def brand(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the camera brand."""
|
2015-06-05 12:51:29 +00:00
|
|
|
return None
|
|
|
|
|
2017-07-01 04:06:56 +00:00
|
|
|
@property
|
|
|
|
def motion_detection_enabled(self):
|
|
|
|
"""Return the camera motion detection status."""
|
|
|
|
return None
|
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
@property
|
|
|
|
def model(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the camera model."""
|
2015-06-05 12:51:29 +00:00
|
|
|
return None
|
|
|
|
|
2018-05-10 22:22:02 +00:00
|
|
|
@property
|
|
|
|
def frame_interval(self):
|
|
|
|
"""Return the interval between frames of the mjpeg stream."""
|
|
|
|
return 0.5
|
|
|
|
|
2015-07-11 06:17:12 +00:00
|
|
|
def camera_image(self):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Return bytes of camera image."""
|
2015-06-05 12:51:29 +00:00
|
|
|
raise NotImplementedError()
|
|
|
|
|
2018-06-16 14:49:11 +00:00
|
|
|
@callback
|
2016-10-24 06:48:01 +00:00
|
|
|
def async_camera_image(self):
|
|
|
|
"""Return bytes of camera image.
|
|
|
|
|
2016-12-26 13:10:23 +00:00
|
|
|
This method must be run in the event loop and returns a coroutine.
|
2016-10-24 06:48:01 +00:00
|
|
|
"""
|
2017-05-26 15:28:07 +00:00
|
|
|
return self.hass.async_add_job(self.camera_image)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
2018-05-01 18:49:33 +00:00
|
|
|
async def handle_async_still_stream(self, request, interval):
|
2016-10-24 06:48:01 +00:00
|
|
|
"""Generate an HTTP MJPEG stream from camera images.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2018-09-06 08:51:16 +00:00
|
|
|
return await async_get_still_stream(request, self.async_camera_image,
|
|
|
|
self.content_type, interval)
|
2018-05-01 18:49:33 +00:00
|
|
|
|
|
|
|
async def handle_async_mjpeg_stream(self, request):
|
|
|
|
"""Serve an HTTP MJPEG stream from the camera.
|
|
|
|
|
|
|
|
This method can be overridden by camera plaforms to proxy
|
|
|
|
a direct stream from the camera.
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2018-05-10 22:22:02 +00:00
|
|
|
await self.handle_async_still_stream(request, self.frame_interval)
|
2016-02-07 09:08:55 +00:00
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
@property
|
|
|
|
def state(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the camera state."""
|
2015-06-05 12:51:29 +00:00
|
|
|
if self.is_recording:
|
|
|
|
return STATE_RECORDING
|
2018-07-23 08:16:05 +00:00
|
|
|
if self.is_streaming:
|
2015-06-05 12:51:29 +00:00
|
|
|
return STATE_STREAMING
|
2017-07-06 06:30:01 +00:00
|
|
|
return STATE_IDLE
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2018-07-24 17:13:26 +00:00
|
|
|
@property
|
|
|
|
def is_on(self):
|
|
|
|
"""Return true if on."""
|
|
|
|
return True
|
|
|
|
|
|
|
|
def turn_off(self):
|
|
|
|
"""Turn off camera."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_turn_off(self):
|
|
|
|
"""Turn off camera."""
|
|
|
|
return self.hass.async_add_job(self.turn_off)
|
|
|
|
|
|
|
|
def turn_on(self):
|
|
|
|
"""Turn off camera."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_turn_on(self):
|
|
|
|
"""Turn off camera."""
|
|
|
|
return self.hass.async_add_job(self.turn_on)
|
|
|
|
|
2017-07-01 04:06:56 +00:00
|
|
|
def enable_motion_detection(self):
|
|
|
|
"""Enable motion detection in the camera."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
2018-07-24 17:13:26 +00:00
|
|
|
@callback
|
2017-07-01 04:06:56 +00:00
|
|
|
def async_enable_motion_detection(self):
|
|
|
|
"""Call the job and enable motion detection."""
|
|
|
|
return self.hass.async_add_job(self.enable_motion_detection)
|
|
|
|
|
|
|
|
def disable_motion_detection(self):
|
|
|
|
"""Disable motion detection in camera."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
2018-07-24 17:13:26 +00:00
|
|
|
@callback
|
2017-07-01 04:06:56 +00:00
|
|
|
def async_disable_motion_detection(self):
|
|
|
|
"""Call the job and disable motion detection."""
|
|
|
|
return self.hass.async_add_job(self.disable_motion_detection)
|
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
@property
|
|
|
|
def state_attributes(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Return the camera state attributes."""
|
2018-05-03 20:02:59 +00:00
|
|
|
attrs = {
|
2017-01-28 19:51:35 +00:00
|
|
|
'access_token': self.access_tokens[-1],
|
2016-05-15 23:59:27 +00:00
|
|
|
}
|
2015-07-11 18:55:25 +00:00
|
|
|
|
|
|
|
if self.model:
|
2018-05-03 20:02:59 +00:00
|
|
|
attrs['model_name'] = self.model
|
2015-07-11 18:55:25 +00:00
|
|
|
|
|
|
|
if self.brand:
|
2018-05-03 20:02:59 +00:00
|
|
|
attrs['brand'] = self.brand
|
2015-07-11 18:55:25 +00:00
|
|
|
|
2017-07-01 04:06:56 +00:00
|
|
|
if self.motion_detection_enabled:
|
2018-05-03 20:02:59 +00:00
|
|
|
attrs['motion_detection'] = self.motion_detection_enabled
|
2017-07-01 04:06:56 +00:00
|
|
|
|
2018-05-03 20:02:59 +00:00
|
|
|
return attrs
|
2016-05-14 07:58:36 +00:00
|
|
|
|
2017-01-28 19:51:35 +00:00
|
|
|
@callback
|
|
|
|
def async_update_token(self):
|
|
|
|
"""Update the used token."""
|
|
|
|
self.access_tokens.append(
|
|
|
|
hashlib.sha256(
|
|
|
|
_RND.getrandbits(256).to_bytes(32, 'little')).hexdigest())
|
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
|
|
|
|
class CameraView(HomeAssistantView):
|
|
|
|
"""Base CameraView."""
|
|
|
|
|
2016-05-15 23:59:27 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
2018-01-23 06:54:41 +00:00
|
|
|
def __init__(self, component):
|
2016-05-14 07:58:36 +00:00
|
|
|
"""Initialize a basic camera view."""
|
2018-01-23 06:54:41 +00:00
|
|
|
self.component = component
|
2016-05-14 07:58:36 +00:00
|
|
|
|
2018-06-16 14:49:11 +00:00
|
|
|
async def get(self, request, entity_id):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Start a GET request."""
|
2018-01-23 06:54:41 +00:00
|
|
|
camera = self.component.get_entity(entity_id)
|
2016-05-15 23:59:27 +00:00
|
|
|
|
|
|
|
if camera is None:
|
2018-07-24 17:13:26 +00:00
|
|
|
raise web.HTTPNotFound()
|
2016-05-15 23:59:27 +00:00
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
authenticated = (request[KEY_AUTHENTICATED] or
|
2017-05-26 20:12:17 +00:00
|
|
|
request.query.get('token') in camera.access_tokens)
|
2016-05-15 23:59:27 +00:00
|
|
|
|
|
|
|
if not authenticated:
|
2018-07-23 12:36:36 +00:00
|
|
|
raise web.HTTPUnauthorized()
|
2016-05-15 23:59:27 +00:00
|
|
|
|
2018-07-24 17:13:26 +00:00
|
|
|
if not camera.is_on:
|
|
|
|
_LOGGER.debug('Camera is off.')
|
|
|
|
raise web.HTTPServiceUnavailable()
|
|
|
|
|
2018-07-23 12:36:36 +00:00
|
|
|
return await self.handle(request, camera)
|
2016-05-15 23:59:27 +00:00
|
|
|
|
2018-06-16 14:49:11 +00:00
|
|
|
async def handle(self, request, camera):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Handle the camera request."""
|
2016-05-15 23:59:27 +00:00
|
|
|
raise NotImplementedError()
|
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
|
|
|
|
class CameraImageView(CameraView):
|
|
|
|
"""Camera view to serve an image."""
|
|
|
|
|
2017-04-30 05:04:49 +00:00
|
|
|
url = '/api/camera_proxy/{entity_id}'
|
|
|
|
name = 'api:camera:image'
|
2016-05-14 07:58:36 +00:00
|
|
|
|
2018-06-16 14:49:11 +00:00
|
|
|
async def handle(self, request, camera):
|
2016-05-14 07:58:36 +00:00
|
|
|
"""Serve camera image."""
|
2017-03-31 09:55:22 +00:00
|
|
|
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
|
|
|
with async_timeout.timeout(10, loop=request.app['hass'].loop):
|
2018-06-16 14:49:11 +00:00
|
|
|
image = await camera.async_camera_image()
|
2017-01-15 16:35:58 +00:00
|
|
|
|
2017-03-31 09:55:22 +00:00
|
|
|
if image:
|
2017-06-25 19:25:14 +00:00
|
|
|
return web.Response(body=image,
|
|
|
|
content_type=camera.content_type)
|
2016-05-14 07:58:36 +00:00
|
|
|
|
2018-07-23 12:36:36 +00:00
|
|
|
raise web.HTTPInternalServerError()
|
2016-05-14 07:58:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CameraMjpegStream(CameraView):
|
|
|
|
"""Camera View to serve an MJPEG stream."""
|
|
|
|
|
2017-04-30 05:04:49 +00:00
|
|
|
url = '/api/camera_proxy_stream/{entity_id}'
|
|
|
|
name = 'api:camera:stream'
|
2016-05-14 07:58:36 +00:00
|
|
|
|
2018-05-01 18:49:33 +00:00
|
|
|
async def handle(self, request, camera):
|
|
|
|
"""Serve camera stream, possibly with interval."""
|
|
|
|
interval = request.query.get('interval')
|
|
|
|
if interval is None:
|
2018-07-23 12:36:36 +00:00
|
|
|
return await camera.handle_async_mjpeg_stream(request)
|
2018-05-01 18:49:33 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Compose camera stream from stills
|
|
|
|
interval = float(request.query.get('interval'))
|
2018-05-10 22:22:02 +00:00
|
|
|
if interval < MIN_STREAM_INTERVAL:
|
|
|
|
raise ValueError("Stream interval must be be > {}"
|
|
|
|
.format(MIN_STREAM_INTERVAL))
|
2018-07-23 12:36:36 +00:00
|
|
|
return await camera.handle_async_still_stream(request, interval)
|
2018-05-01 18:49:33 +00:00
|
|
|
except ValueError:
|
2018-07-23 12:36:36 +00:00
|
|
|
raise web.HTTPBadRequest()
|
2018-05-03 20:02:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def websocket_camera_thumbnail(hass, connection, msg):
|
|
|
|
"""Handle get camera thumbnail websocket command.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
|
|
|
async def send_camera_still():
|
|
|
|
"""Send a camera still."""
|
|
|
|
try:
|
|
|
|
image = await async_get_image(hass, msg['entity_id'])
|
|
|
|
connection.send_message_outside(websocket_api.result_message(
|
|
|
|
msg['id'], {
|
|
|
|
'content_type': image.content_type,
|
|
|
|
'content': base64.b64encode(image.content).decode('utf-8')
|
|
|
|
}
|
|
|
|
))
|
|
|
|
except HomeAssistantError:
|
|
|
|
connection.send_message_outside(websocket_api.error_message(
|
|
|
|
msg['id'], 'image_fetch_failed', 'Unable to fetch image'))
|
|
|
|
|
|
|
|
hass.async_add_job(send_camera_still())
|
2018-08-16 12:28:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def async_handle_snapshot_service(camera, service):
|
|
|
|
"""Handle snapshot services calls."""
|
|
|
|
hass = camera.hass
|
|
|
|
filename = service.data[ATTR_FILENAME]
|
|
|
|
filename.hass = hass
|
|
|
|
|
|
|
|
snapshot_file = filename.async_render(
|
|
|
|
variables={ATTR_ENTITY_ID: camera})
|
|
|
|
|
|
|
|
# check if we allow to access to that file
|
|
|
|
if not hass.config.is_allowed_path(snapshot_file):
|
|
|
|
_LOGGER.error(
|
|
|
|
"Can't write %s, no access to path!", snapshot_file)
|
|
|
|
return
|
|
|
|
|
|
|
|
image = await camera.async_camera_image()
|
|
|
|
|
|
|
|
def _write_image(to_file, image_data):
|
|
|
|
"""Executor helper to write image."""
|
|
|
|
with open(to_file, 'wb') as img_file:
|
|
|
|
img_file.write(image_data)
|
|
|
|
|
|
|
|
try:
|
|
|
|
await hass.async_add_executor_job(
|
|
|
|
_write_image, snapshot_file, image)
|
|
|
|
except OSError as err:
|
|
|
|
_LOGGER.error("Can't write image to file: %s", err)
|