diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 41860a62cb7..d48563d471f 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -14,10 +14,7 @@ import requests from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.components import bloomsky -from homeassistant.const import ( - HTTP_NOT_FOUND, - ATTR_ENTITY_ID, - ) +from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID DOMAIN = 'camera' @@ -36,7 +33,7 @@ STATE_IDLE = 'idle' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}' -MULTIPART_BOUNDARY = '--jpegboundary' +MULTIPART_BOUNDARY = '--jpgboundary' MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n' @@ -49,17 +46,6 @@ def setup(hass, 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): """Serve the camera image via the HA server.""" entity_id = path_match.group(ATTR_ENTITY_ID) @@ -77,22 +63,16 @@ def setup(hass, config): handler.end_headers() return - handler.wfile.write(response) + handler.send_response(HTTP_OK) + handler.write_content(response) hass.http.register_path( 'GET', re.compile(r'/api/camera_proxy/(?P[a-zA-Z\._0-9]+)'), _proxy_camera_image) - # pylint: disable=unused-argument def _proxy_camera_mjpeg_stream(handler, path_match, data): - """ - 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. - """ + """Proxy the camera image as an mjpeg stream via the HA server.""" entity_id = path_match.group(ATTR_ENTITY_ID) camera = component.entities.get(entity_id) @@ -112,8 +92,7 @@ def setup(hass, config): hass.http.register_path( 'GET', - re.compile( - r'/api/camera_proxy_stream/(?P[a-zA-Z\._0-9]+)'), + re.compile(r'/api/camera_proxy_stream/(?P[a-zA-Z\._0-9]+)'), _proxy_camera_mjpeg_stream) return True @@ -137,19 +116,16 @@ class Camera(Entity): return ENTITY_IMAGE_URL.format(self.entity_id) @property - # pylint: disable=no-self-use def is_recording(self): """Return true if the device is recording.""" return False @property - # pylint: disable=no-self-use def brand(self): """Camera brand.""" return None @property - # pylint: disable=no-self-use def model(self): """Camera model.""" return None @@ -160,29 +136,28 @@ class Camera(Entity): def mjpeg_stream(self, handler): """Generate an HTTP MJPEG stream from camera images.""" - 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')) + 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) - # MJPEG_START_HEADER.format() while True: img_bytes = self.camera_image() + if img_bytes is None: continue - 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')) + 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) time.sleep(0.5) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ba2a06130b..c1025fd1657 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -66,10 +66,6 @@ def _handle_get_api_bootstrap(handler, path_match, data): def _handle_get_root(handler, path_match, data): """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: app_url = "home-assistant-polymer/src/home-assistant.html" 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('{{ 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): diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 7c252385d5d..7da4bc320f3 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -7,7 +7,6 @@ https://home-assistant.io/developers/api/ import gzip import json import logging -import os import ssl import threading import time @@ -164,6 +163,7 @@ class RequestHandler(SimpleHTTPRequestHandler): # Track if this was an authenticated request self.authenticated = False SimpleHTTPRequestHandler.__init__(self, req, client_addr, server) + self.protocol_version = 'HTTP/1.1' def log_message(self, fmt, *arguments): """Redirect built-in log to HA logging.""" @@ -282,31 +282,21 @@ class RequestHandler(SimpleHTTPRequestHandler): json_data = json.dumps(data, indent=4, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') 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: self.send_header('Location', location) self.set_session_cookie_header() - self.end_headers() - - if data is not None: - self.wfile.write(json_data) + self.write_content(json_data, CONTENT_TYPE_JSON) def write_text(self, message, status_code=HTTP_OK): """Helper method to return a text message to the caller.""" msg_data = message.encode('UTF-8') 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.end_headers() - - self.wfile.write(msg_data) + self.write_content(msg_data, CONTENT_TYPE_TEXT_PLAIN) def write_file(self, path, cache_headers=True): """Return a file to the user.""" @@ -322,36 +312,32 @@ class RequestHandler(SimpleHTTPRequestHandler): def write_file_pointer(self, content_type, inp, cache_headers=True): """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_header(HTTP_HEADER_CONTENT_TYPE, content_type) if cache_headers: self.set_cache_header() self.set_session_cookie_header() - if do_gzip: - gzip_data = gzip.compress(inp.read()) + self.write_content(inp.read(), content_type) + + 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_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() if self.command == 'HEAD': return - elif do_gzip: - self.wfile.write(gzip_data) - - else: - self.copyfile(inp, self.wfile) + self.wfile.write(content) def set_cache_header(self): """Add cache headers if not in development.""" diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 6acb1c2a569..fb571fe5811 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -104,8 +104,6 @@ class TestAPI(unittest.TestCase): _url(const.URL_API_STATES_ENTITY.format("test.test")), headers=HA_HEADERS) - self.assertEqual(req.headers['content-length'], str(len(req.content))) - data = ha.State.from_dict(req.json()) state = hass.states.get("test.test")