core/homeassistant/components/camera/__init__.py

219 lines
6.3 KiB
Python
Raw Normal View History

2015-06-05 12:51:29 +00:00
# pylint: disable=too-many-lines
"""
homeassistant.components.camera
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various cameras.
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 requests
import logging
import time
import re
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
HTTP_NOT_FOUND,
ATTR_ENTITY_ID,
)
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'camera'
DEPENDENCIES = ['http']
GROUP_NAME_ALL_CAMERAS = 'all_cameras'
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SWITCH_ACTION_RECORD = 'record'
SWITCH_ACTION_SNAPSHOT = 'snapshot'
SERVICE_CAMERA = 'camera_service'
STATE_RECORDING = 'recording'
DEFAULT_RECORDING_SECONDS = 30
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {}
FILE_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S-%f'
DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
REC_DIR_PREFIX = 'recording-'
REC_IMG_PREFIX = 'recording_image-'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
2015-07-10 08:03:46 +00:00
CAMERA_PROXY_URL = '/api/camera_proxy_stream/{0}'
CAMERA_STILL_URL = '/api/camera_proxy/{0}'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?time={1}'
MULTIPART_BOUNDARY = '--jpegboundary'
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):
2015-11-18 07:42:49 +00:00
""" Track states and offer events for cameras. """
2015-06-05 12:51:29 +00:00
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
component.setup(config)
# -------------------------------------------------------------------------
# CAMERA COMPONENT ENDPOINTS
# -------------------------------------------------------------------------
# The following defines the endpoints for serving images from the camera
# via the HA http server. This is means that you can access images from
# your camera outside of your LAN without the need for port forwards etc.
# Because the authentication header can't be added in image requests these
# endpoints are secured with session based security.
# pylint: disable=unused-argument
def _proxy_camera_image(handler, path_match, data):
""" Proxies the camera image via the HA server. """
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if camera:
2015-07-11 06:17:12 +00:00
response = camera.camera_image()
if response is not None:
handler.wfile.write(response)
else:
handler.send_response(HTTP_NOT_FOUND)
2015-06-05 12:51:29 +00:00
else:
handler.send_response(HTTP_NOT_FOUND)
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
# pylint: disable=unused-argument
def _proxy_camera_mjpeg_stream(handler, path_match, data):
2015-11-18 07:42:49 +00:00
"""
Proxies the camera image as an mjpeg stream via the HA server.
2015-06-05 12:51:29 +00:00
This function takes still images from the IP camera and turns them
into an MJPEG stream. This means that HA can return a live video
stream even with only a still image URL available.
"""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
2015-07-10 08:03:46 +00:00
if not camera:
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()
2015-06-05 12:51:29 +00:00
2015-07-10 08:03:46 +00:00
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
handler.request.sendall(bytes(
'Content-type: multipart/x-mixed-replace; \
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
2015-06-05 12:51:29 +00:00
2015-07-10 08:03:46 +00:00
# MJPEG_START_HEADER.format()
2015-06-05 12:51:29 +00:00
2015-07-10 08:03:46 +00:00
while True:
2015-06-05 12:51:29 +00:00
2015-07-11 06:17:12 +00:00
img_bytes = camera.camera_image()
2015-11-14 15:51:07 +00:00
if img_bytes is None:
continue
2015-11-14 20:49:39 +00:00
headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg',
)) + '\r\n\r\n'
handler.request.sendall(
bytes(headers_str, 'utf-8') +
img_bytes +
bytes('\r\n', 'utf-8'))
handler.request.sendall(
bytes('--jpgboundary\r\n', 'utf-8'))
2015-11-14 15:51:07 +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
camera.is_streaming = False
hass.http.register_path(
'GET',
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):
2015-11-18 07:42:49 +00:00
""" The base class for camera components. """
2015-06-05 12:51:29 +00:00
2015-07-11 06:17:12 +00:00
def __init__(self):
self.is_streaming = False
2015-06-05 12:51:29 +00:00
@property
2015-06-05 13:04:52 +00:00
# pylint: disable=no-self-use
2015-06-05 12:51:29 +00:00
def is_recording(self):
2015-11-18 07:42:49 +00:00
""" Returns true if the device is recording. """
2015-06-05 12:51:29 +00:00
return False
@property
2015-06-05 13:04:52 +00:00
# pylint: disable=no-self-use
2015-06-05 12:51:29 +00:00
def brand(self):
2015-11-18 07:42:49 +00:00
""" Should return a string of the camera brand. """
2015-06-05 12:51:29 +00:00
return None
@property
2015-06-05 13:04:52 +00:00
# pylint: disable=no-self-use
2015-06-05 12:51:29 +00:00
def model(self):
2015-11-18 07:42:49 +00:00
""" Returns string of camera model. """
2015-06-05 12:51:29 +00:00
return None
2015-07-11 06:17:12 +00:00
def camera_image(self):
2015-11-18 07:42:49 +00:00
""" Return bytes of camera image. """
2015-06-05 12:51:29 +00:00
raise NotImplementedError()
@property
def state(self):
""" Returns the state of the entity. """
if self.is_recording:
return STATE_RECORDING
elif self.is_streaming:
return STATE_STREAMING
else:
return STATE_IDLE
@property
def state_attributes(self):
""" Returns optional state attributes. """
2015-07-11 18:55:25 +00:00
attr = {
2015-07-11 06:17:12 +00:00
ATTR_ENTITY_PICTURE: ENTITY_IMAGE_URL.format(
2015-07-11 18:55:25 +00:00
self.entity_id, time.time()),
2015-07-11 06:17:12 +00:00
}
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