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
|
|
|
"""
|
|
|
|
import logging
|
|
|
|
import re
|
2015-11-29 21:49:05 +00:00
|
|
|
import time
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
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-02-20 07:22:23 +00:00
|
|
|
from homeassistant.components import bloomsky
|
2016-03-27 18:57:15 +00:00
|
|
|
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
|
2016-03-28 01:48:51 +00:00
|
|
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
2015-06-05 12:51:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
DOMAIN = 'camera'
|
|
|
|
DEPENDENCIES = ['http']
|
|
|
|
SCAN_INTERVAL = 30
|
|
|
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|
|
|
|
|
|
|
# Maps discovered services to their platforms
|
2016-02-20 07:22:23 +00:00
|
|
|
DISCOVERY_PLATFORMS = {
|
|
|
|
bloomsky.DISCOVER_CAMERAS: 'bloomsky',
|
|
|
|
}
|
2015-06-05 12:51:29 +00:00
|
|
|
|
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-02-17 07:52:05 +00:00
|
|
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
2015-07-10 08:03:46 +00:00
|
|
|
|
2016-03-27 18:57:15 +00:00
|
|
|
MULTIPART_BOUNDARY = '--jpgboundary'
|
2015-07-10 08:03:46 +00:00
|
|
|
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
|
|
|
|
2015-06-05 12:51:29 +00:00
|
|
|
|
|
|
|
# pylint: disable=too-many-branches
|
|
|
|
def 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(
|
|
|
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
|
|
|
DISCOVERY_PLATFORMS)
|
|
|
|
|
|
|
|
component.setup(config)
|
|
|
|
|
|
|
|
def _proxy_camera_image(handler, path_match, data):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Serve the camera image via the HA server."""
|
2015-06-05 12:51:29 +00:00
|
|
|
entity_id = path_match.group(ATTR_ENTITY_ID)
|
2015-11-29 02:59:59 +00:00
|
|
|
camera = component.entities.get(entity_id)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2015-11-29 02:59:59 +00:00
|
|
|
if camera is None:
|
2015-06-05 12:51:29 +00:00
|
|
|
handler.send_response(HTTP_NOT_FOUND)
|
2015-11-29 02:59:59 +00:00
|
|
|
handler.end_headers()
|
|
|
|
return
|
|
|
|
|
|
|
|
response = camera.camera_image()
|
|
|
|
|
|
|
|
if response is None:
|
|
|
|
handler.send_response(HTTP_NOT_FOUND)
|
|
|
|
handler.end_headers()
|
|
|
|
return
|
|
|
|
|
2016-03-27 18:57:15 +00:00
|
|
|
handler.send_response(HTTP_OK)
|
|
|
|
handler.write_content(response)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
|
|
|
hass.http.register_path(
|
|
|
|
'GET',
|
|
|
|
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
2015-07-11 18:55:25 +00:00
|
|
|
_proxy_camera_image)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
|
|
|
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
2016-03-27 18:57:15 +00:00
|
|
|
"""Proxy the camera image as an mjpeg stream via the HA server."""
|
2015-06-05 12:51:29 +00:00
|
|
|
entity_id = path_match.group(ATTR_ENTITY_ID)
|
2015-11-29 02:59:59 +00:00
|
|
|
camera = component.entities.get(entity_id)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2015-11-29 02:59:59 +00:00
|
|
|
if camera is None:
|
2015-07-10 10:10:23 +00:00
|
|
|
handler.send_response(HTTP_NOT_FOUND)
|
|
|
|
handler.end_headers()
|
2015-07-10 08:03:46 +00:00
|
|
|
return
|
2015-06-05 12:51:29 +00:00
|
|
|
|
2015-07-10 08:03:46 +00:00
|
|
|
try:
|
|
|
|
camera.is_streaming = True
|
|
|
|
camera.update_ha_state()
|
2016-02-07 09:08:55 +00:00
|
|
|
camera.mjpeg_stream(handler)
|
2015-11-29 02:59:59 +00:00
|
|
|
|
2015-07-10 08:03:46 +00:00
|
|
|
except (requests.RequestException, IOError):
|
|
|
|
camera.is_streaming = False
|
|
|
|
camera.update_ha_state()
|
2015-06-05 12:51:29 +00:00
|
|
|
|
|
|
|
hass.http.register_path(
|
|
|
|
'GET',
|
2016-03-27 18:57:15 +00:00
|
|
|
re.compile(r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
2015-07-11 18:55:25 +00:00
|
|
|
_proxy_camera_mjpeg_stream)
|
2015-06-05 12:51:29 +00:00
|
|
|
|
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-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."""
|
|
|
|
return ENTITY_IMAGE_URL.format(self.entity_id)
|
|
|
|
|
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-02-07 09:08:55 +00:00
|
|
|
def mjpeg_stream(self, handler):
|
2016-02-17 07:52:05 +00:00
|
|
|
"""Generate an HTTP MJPEG stream from camera images."""
|
2016-03-27 18:57:15 +00:00
|
|
|
def write_string(text):
|
|
|
|
"""Helper method to write a string to the stream."""
|
|
|
|
handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
|
|
|
|
|
|
|
|
write_string('HTTP/1.1 200 OK')
|
|
|
|
write_string('Content-type: multipart/x-mixed-replace; '
|
|
|
|
'boundary={}'.format(MULTIPART_BOUNDARY))
|
|
|
|
write_string('')
|
|
|
|
write_string(MULTIPART_BOUNDARY)
|
2016-02-07 09:08:55 +00:00
|
|
|
|
|
|
|
while True:
|
|
|
|
img_bytes = self.camera_image()
|
2016-03-27 18:57:15 +00:00
|
|
|
|
2016-02-07 09:08:55 +00:00
|
|
|
if img_bytes is None:
|
|
|
|
continue
|
2016-03-27 18:57:15 +00:00
|
|
|
|
|
|
|
write_string('Content-length: {}'.format(len(img_bytes)))
|
|
|
|
write_string('Content-type: image/jpeg')
|
|
|
|
write_string('')
|
|
|
|
handler.request.sendall(img_bytes)
|
|
|
|
write_string('')
|
|
|
|
write_string(MULTIPART_BOUNDARY)
|
2016-02-07 09:08:55 +00:00
|
|
|
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
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-02-24 06:41:24 +00:00
|
|
|
attr = {}
|
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
|