diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3eedac3ffca..6b35c268652 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -147,6 +147,7 @@ def enable_logging(hass): _LOGGER.error( "Unable to setup error log %s (access denied)", err_log_path) + def _ensure_loader_prepared(hass): """ Ensure Home Assistant loader is prepared. """ if not loader.PREPARED: diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index d658c1523de..4c0e9c315f1 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -1,11 +1,18 @@ +""" +homeassistant.components.api +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides a Rest API for Home Assistant. +""" import re +import logging import homeassistant as ha from homeassistant.helpers import TrackStates import homeassistant.remote as rem from homeassistant.const import ( - SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, - URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER) + URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, + URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY) HTTP_OK = 200 HTTP_CREATED = 201 @@ -20,14 +27,16 @@ HTTP_UNPROCESSABLE_ENTITY = 422 DOMAIN = 'api' DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) + def setup(hass, config): - """ """ + """ Register the API with the HTTP interface. """ if 'http' not in hass.components: + _LOGGER.error('Dependency http is not loaded') return False - # TODO register with hass.http # /api - for validation purposes hass.http.register_path('GET', URL_API, _handle_get_api) @@ -66,14 +75,15 @@ def setup(hass, config): return True + def _handle_get_api(handler, path_match, data): """ Renders the debug interface. """ - handler._json_message("API running.") + handler.write_json_message("API running.") def _handle_get_api_states(handler, path_match, data): """ Returns a dict containing all entity ids and their state. """ - handler._write_json(handler.server.hass.states.all()) + handler.write_json(handler.server.hass.states.all()) def _handle_get_api_states_entity(handler, path_match, data): @@ -83,9 +93,9 @@ def _handle_get_api_states_entity(handler, path_match, data): state = handler.server.hass.states.get(entity_id) if state: - handler._write_json(state) + handler.write_json(state) else: - handler._json_message("State does not exist.", HTTP_NOT_FOUND) + handler.write_json_message("State does not exist.", HTTP_NOT_FOUND) def _handle_post_state_entity(handler, path_match, data): @@ -99,7 +109,7 @@ def _handle_post_state_entity(handler, path_match, data): try: new_state = data['state'] except KeyError: - handler._json_message("state not specified", HTTP_BAD_REQUEST) + handler.write_json_message("state not specified", HTTP_BAD_REQUEST) return attributes = data['attributes'] if 'attributes' in data else None @@ -113,7 +123,7 @@ def _handle_post_state_entity(handler, path_match, data): status_code = HTTP_CREATED if is_new_state else HTTP_OK - handler._write_json( + handler.write_json( state.as_dict(), status_code=status_code, location=URL_API_STATES_ENTITY.format(entity_id)) @@ -121,9 +131,9 @@ def _handle_post_state_entity(handler, path_match, data): def _handle_get_api_events(handler, path_match, data): """ Handles getting overview of event listeners. """ - handler._write_json([{"event": key, "listener_count": value} - for key, value - in handler.server.hass.bus.listeners.items()]) + handler.write_json([{"event": key, "listener_count": value} + for key, value + in handler.server.hass.bus.listeners.items()]) def _handle_api_post_events_event(handler, path_match, event_data): @@ -137,8 +147,8 @@ def _handle_api_post_events_event(handler, path_match, event_data): event_type = path_match.group('event_type') if event_data is not None and not isinstance(event_data, dict): - handler._json_message("event_data should be an object", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_json_message( + "event_data should be an object", HTTP_UNPROCESSABLE_ENTITY) event_origin = ha.EventOrigin.remote @@ -153,12 +163,12 @@ def _handle_api_post_events_event(handler, path_match, event_data): handler.server.hass.bus.fire(event_type, event_data, event_origin) - handler._json_message("Event {} fired.".format(event_type)) + handler.write_json_message("Event {} fired.".format(event_type)) def _handle_get_api_services(handler, path_match, data): """ Handles getting overview of services. """ - handler._write_json( + handler.write_json( [{"domain": key, "services": value} for key, value in handler.server.hass.services.services.items()]) @@ -177,7 +187,7 @@ def _handle_post_api_services_domain_service(handler, path_match, data): with TrackStates(handler.server.hass) as changed_states: handler.server.hass.services.call(domain, service, data, True) - handler._write_json(changed_states) + handler.write_json(changed_states) # pylint: disable=invalid-name @@ -188,21 +198,21 @@ def _handle_post_api_event_forward(handler, path_match, data): host = data['host'] api_password = data['api_password'] except KeyError: - handler._json_message("No host or api_password received.", - HTTP_BAD_REQUEST) + handler.write_json_message( + "No host or api_password received.", HTTP_BAD_REQUEST) return try: port = int(data['port']) if 'port' in data else None except ValueError: - handler._json_message( + handler.write_json_message( "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) return api = rem.API(host, api_password, port) if not api.validate_api(): - handler._json_message( + handler.write_json_message( "Unable to validate API", HTTP_UNPROCESSABLE_ENTITY) return @@ -212,7 +222,7 @@ def _handle_post_api_event_forward(handler, path_match, data): handler.server.event_forwarder.connect(api) - handler._json_message("Event forwarding setup.") + handler.write_json_message("Event forwarding setup.") def _handle_delete_api_event_forward(handler, path_match, data): @@ -221,13 +231,13 @@ def _handle_delete_api_event_forward(handler, path_match, data): try: host = data['host'] except KeyError: - handler._json_message("No host received.", HTTP_BAD_REQUEST) + handler.write_json_message("No host received.", HTTP_BAD_REQUEST) return try: port = int(data['port']) if 'port' in data else None except ValueError: - handler._json_message( + handler.write_json_message( "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) return @@ -236,4 +246,4 @@ def _handle_delete_api_event_forward(handler, path_match, data): handler.server.event_forwarder.disconnect(api) - handler._json_message("Event forwarding cancelled.") + handler.write_json_message("Event forwarding cancelled.") diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b615b6ff8dd..4829efeaf09 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,9 +1,14 @@ +""" +homeassistant.components.frontend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides a frontend for Home Assistant. +""" import re import os -import time -import gzip +import logging -from . import frontend +from . import version import homeassistant.util as util DOMAIN = 'frontend' @@ -21,10 +26,13 @@ HTTP_UNPROCESSABLE_ENTITY = 422 URL_ROOT = "/" +_LOGGER = logging.getLogger(__name__) + def setup(hass, config): """ Setup serving the frontend. """ if 'http' not in hass.components: + _LOGGER.error('Dependency http is not loaded') return False hass.http.register_path('GET', URL_ROOT, _handle_get_root, False) @@ -52,7 +60,7 @@ def _handle_get_root(handler, path_match, data): if handler.server.development: app_url = "polymer/splash-login.html" else: - app_url = "frontend-{}.html".format(frontend.VERSION) + app_url = "frontend-{}.html".format(version.VERSION) # auto login if no password was set, else check api_password param auth = (handler.server.api_password if handler.server.no_password_set @@ -80,7 +88,7 @@ def _handle_get_root(handler, path_match, data): def _handle_get_static(handler, path_match, data): - """ Returns a static file. """ + """ Returns a static file for the frontend. """ req_file = util.sanitize_path(path_match.group('file')) # Strip md5 hash out of frontend filename @@ -89,54 +97,4 @@ def _handle_get_static(handler, path_match, data): path = os.path.join(os.path.dirname(__file__), 'www_static', req_file) - inp = None - - try: - inp = open(path, 'rb') - - do_gzip = 'gzip' in handler.headers.get('accept-encoding', '') - - handler.send_response(HTTP_OK) - - ctype = handler.guess_type(path) - handler.send_header("Content-Type", ctype) - - # Add cache if not development - if not handler.server.development: - # 1 year in seconds - cache_time = 365 * 86400 - - handler.send_header( - "Cache-Control", "public, max-age={}".format(cache_time)) - handler.send_header( - "Expires", handler.date_time_string(time.time()+cache_time)) - - if do_gzip: - gzip_data = gzip.compress(inp.read()) - - handler.send_header("Content-Encoding", "gzip") - handler.send_header("Vary", "Accept-Encoding") - handler.send_header("Content-Length", str(len(gzip_data))) - - else: - fs = os.fstat(inp.fileno()) - handler.send_header("Content-Length", str(fs[6])) - - handler.end_headers() - - if handler.command == 'HEAD': - return - - elif do_gzip: - handler.wfile.write(gzip_data) - - else: - handler.copyfile(inp, handler.wfile) - - except IOError: - handler.send_response(HTTP_NOT_FOUND) - handler.end_headers() - - finally: - if inp: - inp.close() + handler.write_file(path) diff --git a/homeassistant/components/frontend/frontend.py b/homeassistant/components/frontend/version.py similarity index 100% rename from homeassistant/components/frontend/frontend.py rename to homeassistant/components/frontend/version.py diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c32c775015e..2691e693eb5 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -74,18 +74,17 @@ Example result: import json import threading import logging -import re +import time +import gzip +import os from http.server import SimpleHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn from urllib.parse import urlparse, parse_qs import homeassistant as ha -from homeassistant.const import ( - SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, - URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER) +from homeassistant.const import SERVER_PORT, AUTH_HEADER import homeassistant.remote as rem import homeassistant.util as util -from . import frontend DOMAIN = "http" DEPENDENCIES = [] @@ -220,12 +219,11 @@ class RequestHandler(SimpleHTTPRequestHandler): try: data.update(json.loads(body_content)) except (TypeError, ValueError): - # TypeError is JSON object is not a dict + # TypeError if JSON object is not a dict # ValueError if we could not parse JSON - _LOGGER.exception("Exception parsing JSON: %s", - body_content) - - self._json_message( + _LOGGER.exception( + "Exception parsing JSON: %s", body_content) + self.write_json_message( "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) return @@ -271,7 +269,7 @@ class RequestHandler(SimpleHTTPRequestHandler): # For some calls we need a valid password if require_auth and api_password != self.server.api_password: - self._json_message( + self.write_json_message( "API password missing or incorrect.", HTTP_UNAUTHORIZED) else: @@ -305,11 +303,11 @@ class RequestHandler(SimpleHTTPRequestHandler): """ DELETE request handler. """ self._handle_request('DELETE') - def _json_message(self, message, status_code=HTTP_OK): + def write_json_message(self, message, status_code=HTTP_OK): """ Helper method to return a message to the caller. """ - self._write_json({'message': message}, status_code=status_code) + self.write_json({'message': message}, status_code=status_code) - def _write_json(self, data=None, status_code=HTTP_OK, location=None): + def write_json(self, data=None, status_code=HTTP_OK, location=None): """ Helper method to return JSON to the caller. """ self.send_response(status_code) self.send_header('Content-type', 'application/json') @@ -323,3 +321,56 @@ class RequestHandler(SimpleHTTPRequestHandler): self.wfile.write( json.dumps(data, indent=4, sort_keys=True, cls=rem.JSONEncoder).encode("UTF-8")) + + def write_file(self, path): + """ Returns a file to the user. """ + try: + with open(path, 'rb') as inp: + self.write_file_pointer(self.guess_type(path), inp) + + except IOError: + self.send_response(HTTP_NOT_FOUND) + self.end_headers() + _LOGGER.exception("Unable to serve %s", path) + + def write_file_pointer(self, content_type, inp): + """ + Helper function to write a file pointer to the user. + Does not do error handling. + """ + do_gzip = 'gzip' in self.headers.get('accept-encoding', '') + + self.send_response(HTTP_OK) + self.send_header("Content-Type", content_type) + + # Add cache if not development + if not self.server.development: + # 1 year in seconds + cache_time = 365 * 86400 + + self.send_header( + "Cache-Control", "public, max-age={}".format(cache_time)) + self.send_header( + "Expires", self.date_time_string(time.time()+cache_time)) + + if do_gzip: + gzip_data = gzip.compress(inp.read()) + + self.send_header("Content-Encoding", "gzip") + self.send_header("Vary", "Accept-Encoding") + self.send_header("Content-Length", str(len(gzip_data))) + + else: + fst = os.fstat(inp.fileno()) + self.send_header("Content-Length", str(fst[6])) + + self.end_headers() + + if self.command == 'HEAD': + return + + elif do_gzip: + self.wfile.write(gzip_data) + + else: + self.copyfile(inp, self.wfile) diff --git a/scripts/build_frontend b/scripts/build_frontend index 4df1ba47129..6476fd1deb3 100755 --- a/scripts/build_frontend +++ b/scripts/build_frontend @@ -23,11 +23,11 @@ mv polymer/bower_components/polymer/polymer.html.bak polymer/bower_components/po # Generate the MD5 hash of the new frontend cd .. -echo '""" DO NOT MODIFY. Auto-generated by build_frontend script """' > frontend.py +echo '""" DO NOT MODIFY. Auto-generated by build_frontend script """' > version.py if [ $(command -v md5) ]; then - echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> frontend.py + echo 'VERSION = "'`md5 -q www_static/frontend.html`'"' >> version.py elif [ $(command -v md5sum) ]; then - echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> frontend.py + echo 'VERSION = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> version.py else echo 'Could not find a MD5 utility' fi