mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
commit
e989c8a24a
@ -14,10 +14,7 @@ import requests
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.components import bloomsky
|
from homeassistant.components import bloomsky
|
||||||
from homeassistant.const import (
|
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
|
||||||
HTTP_NOT_FOUND,
|
|
||||||
ATTR_ENTITY_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = 'camera'
|
DOMAIN = 'camera'
|
||||||
@ -36,7 +33,7 @@ STATE_IDLE = 'idle'
|
|||||||
|
|
||||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
||||||
|
|
||||||
MULTIPART_BOUNDARY = '--jpegboundary'
|
MULTIPART_BOUNDARY = '--jpgboundary'
|
||||||
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
||||||
|
|
||||||
|
|
||||||
@ -49,17 +46,6 @@ def setup(hass, config):
|
|||||||
|
|
||||||
component.setup(config)
|
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):
|
def _proxy_camera_image(handler, path_match, data):
|
||||||
"""Serve the camera image via the HA server."""
|
"""Serve the camera image via the HA server."""
|
||||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||||
@ -77,22 +63,16 @@ def setup(hass, config):
|
|||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
return
|
return
|
||||||
|
|
||||||
handler.wfile.write(response)
|
handler.send_response(HTTP_OK)
|
||||||
|
handler.write_content(response)
|
||||||
|
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'GET',
|
'GET',
|
||||||
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
_proxy_camera_image)
|
_proxy_camera_image)
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
||||||
"""
|
"""Proxy the camera image as an mjpeg stream via the HA server."""
|
||||||
Proxy the camera image as an mjpeg stream via the HA server.
|
|
||||||
|
|
||||||
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)
|
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||||
camera = component.entities.get(entity_id)
|
camera = component.entities.get(entity_id)
|
||||||
|
|
||||||
@ -112,8 +92,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
'GET',
|
'GET',
|
||||||
re.compile(
|
re.compile(r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
|
||||||
_proxy_camera_mjpeg_stream)
|
_proxy_camera_mjpeg_stream)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -137,19 +116,16 @@ class Camera(Entity):
|
|||||||
return ENTITY_IMAGE_URL.format(self.entity_id)
|
return ENTITY_IMAGE_URL.format(self.entity_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
# pylint: disable=no-self-use
|
|
||||||
def is_recording(self):
|
def is_recording(self):
|
||||||
"""Return true if the device is recording."""
|
"""Return true if the device is recording."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
# pylint: disable=no-self-use
|
|
||||||
def brand(self):
|
def brand(self):
|
||||||
"""Camera brand."""
|
"""Camera brand."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
# pylint: disable=no-self-use
|
|
||||||
def model(self):
|
def model(self):
|
||||||
"""Camera model."""
|
"""Camera model."""
|
||||||
return None
|
return None
|
||||||
@ -160,29 +136,28 @@ class Camera(Entity):
|
|||||||
|
|
||||||
def mjpeg_stream(self, handler):
|
def mjpeg_stream(self, handler):
|
||||||
"""Generate an HTTP MJPEG stream from camera images."""
|
"""Generate an HTTP MJPEG stream from camera images."""
|
||||||
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
def write_string(text):
|
||||||
handler.request.sendall(bytes(
|
"""Helper method to write a string to the stream."""
|
||||||
'Content-type: multipart/x-mixed-replace; \
|
handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
|
||||||
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
|
||||||
handler.request.sendall(bytes('--jpgboundary\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)
|
||||||
|
|
||||||
# MJPEG_START_HEADER.format()
|
|
||||||
while True:
|
while True:
|
||||||
img_bytes = self.camera_image()
|
img_bytes = self.camera_image()
|
||||||
|
|
||||||
if img_bytes is None:
|
if img_bytes is None:
|
||||||
continue
|
continue
|
||||||
headers_str = '\r\n'.join((
|
|
||||||
'Content-length: {}'.format(len(img_bytes)),
|
|
||||||
'Content-type: image/jpeg',
|
|
||||||
)) + '\r\n\r\n'
|
|
||||||
|
|
||||||
handler.request.sendall(
|
write_string('Content-length: {}'.format(len(img_bytes)))
|
||||||
bytes(headers_str, 'utf-8') +
|
write_string('Content-type: image/jpeg')
|
||||||
img_bytes +
|
write_string('')
|
||||||
bytes('\r\n', 'utf-8'))
|
handler.request.sendall(img_bytes)
|
||||||
|
write_string('')
|
||||||
handler.request.sendall(
|
write_string(MULTIPART_BOUNDARY)
|
||||||
bytes('--jpgboundary\r\n', 'utf-8'))
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
@ -66,10 +66,6 @@ def _handle_get_api_bootstrap(handler, path_match, data):
|
|||||||
|
|
||||||
def _handle_get_root(handler, path_match, data):
|
def _handle_get_root(handler, path_match, data):
|
||||||
"""Render the frontend."""
|
"""Render the frontend."""
|
||||||
handler.send_response(HTTP_OK)
|
|
||||||
handler.send_header('Content-type', 'text/html; charset=utf-8')
|
|
||||||
handler.end_headers()
|
|
||||||
|
|
||||||
if handler.server.development:
|
if handler.server.development:
|
||||||
app_url = "home-assistant-polymer/src/home-assistant.html"
|
app_url = "home-assistant-polymer/src/home-assistant.html"
|
||||||
else:
|
else:
|
||||||
@ -86,7 +82,9 @@ def _handle_get_root(handler, path_match, data):
|
|||||||
template_html = template_html.replace('{{ auth }}', auth)
|
template_html = template_html.replace('{{ auth }}', auth)
|
||||||
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
|
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
|
||||||
|
|
||||||
handler.wfile.write(template_html.encode("UTF-8"))
|
handler.send_response(HTTP_OK)
|
||||||
|
handler.write_content(template_html.encode("UTF-8"),
|
||||||
|
'text/html; charset=utf-8')
|
||||||
|
|
||||||
|
|
||||||
def _handle_get_service_worker(handler, path_match, data):
|
def _handle_get_service_worker(handler, path_match, data):
|
||||||
|
@ -7,7 +7,6 @@ https://home-assistant.io/developers/api/
|
|||||||
import gzip
|
import gzip
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import ssl
|
import ssl
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@ -164,6 +163,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||||||
# Track if this was an authenticated request
|
# Track if this was an authenticated request
|
||||||
self.authenticated = False
|
self.authenticated = False
|
||||||
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
|
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
|
||||||
|
self.protocol_version = 'HTTP/1.1'
|
||||||
|
|
||||||
def log_message(self, fmt, *arguments):
|
def log_message(self, fmt, *arguments):
|
||||||
"""Redirect built-in log to HA logging."""
|
"""Redirect built-in log to HA logging."""
|
||||||
@ -282,31 +282,21 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||||||
json_data = json.dumps(data, indent=4, sort_keys=True,
|
json_data = json.dumps(data, indent=4, sort_keys=True,
|
||||||
cls=rem.JSONEncoder).encode('UTF-8')
|
cls=rem.JSONEncoder).encode('UTF-8')
|
||||||
self.send_response(status_code)
|
self.send_response(status_code)
|
||||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
|
|
||||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(json_data)))
|
|
||||||
|
|
||||||
if location:
|
if location:
|
||||||
self.send_header('Location', location)
|
self.send_header('Location', location)
|
||||||
|
|
||||||
self.set_session_cookie_header()
|
self.set_session_cookie_header()
|
||||||
|
|
||||||
self.end_headers()
|
self.write_content(json_data, CONTENT_TYPE_JSON)
|
||||||
|
|
||||||
if data is not None:
|
|
||||||
self.wfile.write(json_data)
|
|
||||||
|
|
||||||
def write_text(self, message, status_code=HTTP_OK):
|
def write_text(self, message, status_code=HTTP_OK):
|
||||||
"""Helper method to return a text message to the caller."""
|
"""Helper method to return a text message to the caller."""
|
||||||
msg_data = message.encode('UTF-8')
|
msg_data = message.encode('UTF-8')
|
||||||
self.send_response(status_code)
|
self.send_response(status_code)
|
||||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
|
||||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(msg_data)))
|
|
||||||
|
|
||||||
self.set_session_cookie_header()
|
self.set_session_cookie_header()
|
||||||
|
|
||||||
self.end_headers()
|
self.write_content(msg_data, CONTENT_TYPE_TEXT_PLAIN)
|
||||||
|
|
||||||
self.wfile.write(msg_data)
|
|
||||||
|
|
||||||
def write_file(self, path, cache_headers=True):
|
def write_file(self, path, cache_headers=True):
|
||||||
"""Return a file to the user."""
|
"""Return a file to the user."""
|
||||||
@ -322,36 +312,32 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||||||
|
|
||||||
def write_file_pointer(self, content_type, inp, cache_headers=True):
|
def write_file_pointer(self, content_type, inp, cache_headers=True):
|
||||||
"""Helper function to write a file pointer to the user."""
|
"""Helper function to write a file pointer to the user."""
|
||||||
do_gzip = 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, '')
|
|
||||||
|
|
||||||
self.send_response(HTTP_OK)
|
self.send_response(HTTP_OK)
|
||||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
|
|
||||||
|
|
||||||
if cache_headers:
|
if cache_headers:
|
||||||
self.set_cache_header()
|
self.set_cache_header()
|
||||||
self.set_session_cookie_header()
|
self.set_session_cookie_header()
|
||||||
|
|
||||||
if do_gzip:
|
self.write_content(inp.read(), content_type)
|
||||||
gzip_data = gzip.compress(inp.read())
|
|
||||||
|
def write_content(self, content, content_type=None):
|
||||||
|
"""Helper method to write content bytes to output stream."""
|
||||||
|
if content_type is not None:
|
||||||
|
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
|
||||||
|
|
||||||
|
if 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, ''):
|
||||||
|
content = gzip.compress(content)
|
||||||
|
|
||||||
self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip")
|
self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip")
|
||||||
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
|
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
|
||||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(gzip_data)))
|
|
||||||
|
|
||||||
else:
|
|
||||||
fst = os.fstat(inp.fileno())
|
|
||||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(fst[6]))
|
|
||||||
|
|
||||||
|
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content)))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
if self.command == 'HEAD':
|
if self.command == 'HEAD':
|
||||||
return
|
return
|
||||||
|
|
||||||
elif do_gzip:
|
self.wfile.write(content)
|
||||||
self.wfile.write(gzip_data)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.copyfile(inp, self.wfile)
|
|
||||||
|
|
||||||
def set_cache_header(self):
|
def set_cache_header(self):
|
||||||
"""Add cache headers if not in development."""
|
"""Add cache headers if not in development."""
|
||||||
|
@ -104,8 +104,6 @@ class TestAPI(unittest.TestCase):
|
|||||||
_url(const.URL_API_STATES_ENTITY.format("test.test")),
|
_url(const.URL_API_STATES_ENTITY.format("test.test")),
|
||||||
headers=HA_HEADERS)
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
self.assertEqual(req.headers['content-length'], str(len(req.content)))
|
|
||||||
|
|
||||||
data = ha.State.from_dict(req.json())
|
data = ha.State.from_dict(req.json())
|
||||||
|
|
||||||
state = hass.states.get("test.test")
|
state = hass.states.get("test.test")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user