2015-06-05 12:51:29 +00:00
|
|
|
# pylint: disable=too-many-lines
|
|
|
|
"""
|
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
|
2015-06-05 12:51:29 +00:00
|
|
|
import logging
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
from aiohttp import web
|
2015-11-29 21:49:05 +00:00
|
|
|
|
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
|
2015-06-05 12:51:29 +00:00
|
|
|
|
|
|
|
DOMAIN = 'camera'
|
|
|
|
DEPENDENCIES = ['http']
|
|
|
|
SCAN_INTERVAL = 30
|
|
|
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|
|
|
|
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'
|
|
|
|
|
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
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def async_setup(hass, config):
|
2016-03-07 19:29:54 +00:00
|
|
|
"""Setup the camera component."""
|
2015-06-05 12:51:29 +00:00
|
|
|
component = EntityComponent(
|
2016-06-12 00:43:13 +00:00
|
|
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
hass.http.register_view(CameraImageView(component.entities))
|
|
|
|
hass.http.register_view(CameraMjpegStream(component.entities))
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2016-10-28 04:40:10 +00:00
|
|
|
yield from component.async_setup(config)
|
2015-07-10 09:42:22 +00:00
|
|
|
return True
|
|
|
|
|
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
|
|
|
|
|
2016-05-15 23:59:27 +00:00
|
|
|
@property
|
|
|
|
def access_token(self):
|
|
|
|
"""Access token for this camera."""
|
|
|
|
return str(id(self))
|
|
|
|
|
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."""
|
2016-05-27 08:29:48 +00:00
|
|
|
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
|
2016-02-24 06:41:24 +00:00
|
|
|
|
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):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Camera brand."""
|
2015-06-05 12:51:29 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def model(self):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Camera model."""
|
2015-06-05 12:51:29 +00:00
|
|
|
return None
|
|
|
|
|
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()
|
|
|
|
|
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
|
|
|
"""
|
2016-12-26 13:10:23 +00:00
|
|
|
return self.hass.loop.run_in_executor(None, self.camera_image)
|
2016-10-24 06:48:01 +00:00
|
|
|
|
|
|
|
@asyncio.coroutine
|
|
|
|
def handle_async_mjpeg_stream(self, request):
|
|
|
|
"""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=--jpegboundary')
|
|
|
|
yield from response.prepare(request)
|
|
|
|
|
|
|
|
def write(img_bytes):
|
|
|
|
"""Write image to stream."""
|
|
|
|
response.write(bytes(
|
|
|
|
'--jpegboundary\r\n'
|
|
|
|
'Content-Type: image/jpeg\r\n'
|
|
|
|
'Content-Length: {}\r\n\r\n'.format(
|
|
|
|
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n')
|
|
|
|
|
|
|
|
last_image = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
while True:
|
|
|
|
img_bytes = yield from self.async_camera_image()
|
|
|
|
if not img_bytes:
|
|
|
|
break
|
|
|
|
|
|
|
|
if img_bytes is not None and img_bytes != last_image:
|
|
|
|
write(img_bytes)
|
|
|
|
|
|
|
|
# Chrome seems to always ignore first picture,
|
|
|
|
# print it twice.
|
|
|
|
if last_image is None:
|
|
|
|
write(img_bytes)
|
|
|
|
|
|
|
|
last_image = img_bytes
|
|
|
|
yield from response.drain()
|
|
|
|
|
|
|
|
yield from asyncio.sleep(.5)
|
|
|
|
finally:
|
2016-11-04 01:32:14 +00:00
|
|
|
yield from response.write_eof()
|
2016-02-07 09:08:55 +00:00
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
@property
|
|
|
|
def state(self):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Camera state."""
|
2015-06-05 12:51:29 +00:00
|
|
|
if self.is_recording:
|
|
|
|
return STATE_RECORDING
|
|
|
|
elif self.is_streaming:
|
|
|
|
return STATE_STREAMING
|
|
|
|
else:
|
|
|
|
return STATE_IDLE
|
|
|
|
|
|
|
|
@property
|
|
|
|
def state_attributes(self):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Camera state attributes."""
|
2016-05-15 23:59:27 +00:00
|
|
|
attr = {
|
|
|
|
'access_token': self.access_token,
|
|
|
|
}
|
2015-07-11 18:55:25 +00:00
|
|
|
|
|
|
|
if self.model:
|
|
|
|
attr['model_name'] = self.model
|
|
|
|
|
|
|
|
if self.brand:
|
|
|
|
attr['brand'] = self.brand
|
|
|
|
|
|
|
|
return attr
|
2016-05-14 07:58:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CameraView(HomeAssistantView):
|
|
|
|
"""Base CameraView."""
|
|
|
|
|
2016-05-15 23:59:27 +00:00
|
|
|
requires_auth = False
|
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
def __init__(self, entities):
|
2016-05-14 07:58:36 +00:00
|
|
|
"""Initialize a basic camera view."""
|
|
|
|
self.entities = entities
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
2016-05-15 23:59:27 +00:00
|
|
|
def get(self, request, entity_id):
|
|
|
|
"""Start a get request."""
|
|
|
|
camera = self.entities.get(entity_id)
|
|
|
|
|
|
|
|
if camera is None:
|
2016-10-24 06:48:01 +00:00
|
|
|
return web.Response(status=404)
|
2016-05-15 23:59:27 +00:00
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
authenticated = (request[KEY_AUTHENTICATED] or
|
2016-10-24 06:48:01 +00:00
|
|
|
request.GET.get('token') == camera.access_token)
|
2016-05-15 23:59:27 +00:00
|
|
|
|
|
|
|
if not authenticated:
|
2016-10-24 06:48:01 +00:00
|
|
|
return web.Response(status=401)
|
2016-05-15 23:59:27 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
response = yield from self.handle(request, camera)
|
|
|
|
return response
|
2016-05-15 23:59:27 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def handle(self, request, camera):
|
2016-05-15 23:59:27 +00:00
|
|
|
"""Hanlde the camera request."""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
2016-05-14 07:58:36 +00:00
|
|
|
|
|
|
|
class CameraImageView(CameraView):
|
|
|
|
"""Camera view to serve an image."""
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
url = "/api/camera_proxy/{entity_id}"
|
2016-05-14 07:58:36 +00:00
|
|
|
name = "api:camera:image"
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def handle(self, request, camera):
|
2016-05-14 07:58:36 +00:00
|
|
|
"""Serve camera image."""
|
2016-10-24 06:48:01 +00:00
|
|
|
image = yield from camera.async_camera_image()
|
2016-05-14 07:58:36 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
if image is None:
|
|
|
|
return web.Response(status=500)
|
2016-05-14 07:58:36 +00:00
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
return web.Response(body=image)
|
2016-05-14 07:58:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CameraMjpegStream(CameraView):
|
|
|
|
"""Camera View to serve an MJPEG stream."""
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
url = "/api/camera_proxy_stream/{entity_id}"
|
2016-05-14 07:58:36 +00:00
|
|
|
name = "api:camera:stream"
|
|
|
|
|
2016-10-24 06:48:01 +00:00
|
|
|
@asyncio.coroutine
|
|
|
|
def handle(self, request, camera):
|
2016-05-14 07:58:36 +00:00
|
|
|
"""Serve camera image."""
|
2016-10-24 06:48:01 +00:00
|
|
|
yield from camera.handle_async_mjpeg_stream(request)
|