Clean up http related components

This commit is contained in:
Paulus Schoutsen 2015-01-30 08:26:06 -08:00
parent 61f6aff056
commit 13ac71bdf0
6 changed files with 119 additions and 99 deletions

View File

@ -147,6 +147,7 @@ def enable_logging(hass):
_LOGGER.error( _LOGGER.error(
"Unable to setup error log %s (access denied)", err_log_path) "Unable to setup error log %s (access denied)", err_log_path)
def _ensure_loader_prepared(hass): def _ensure_loader_prepared(hass):
""" Ensure Home Assistant loader is prepared. """ """ Ensure Home Assistant loader is prepared. """
if not loader.PREPARED: if not loader.PREPARED:

View File

@ -1,11 +1,18 @@
"""
homeassistant.components.api
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a Rest API for Home Assistant.
"""
import re import re
import logging
import homeassistant as ha import homeassistant as ha
from homeassistant.helpers import TrackStates from homeassistant.helpers import TrackStates
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant.const import ( from homeassistant.const import (
SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER) URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY)
HTTP_OK = 200 HTTP_OK = 200
HTTP_CREATED = 201 HTTP_CREATED = 201
@ -20,14 +27,16 @@ HTTP_UNPROCESSABLE_ENTITY = 422
DOMAIN = 'api' DOMAIN = 'api'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
""" """ """ Register the API with the HTTP interface. """
if 'http' not in hass.components: if 'http' not in hass.components:
_LOGGER.error('Dependency http is not loaded')
return False return False
# TODO register with hass.http
# /api - for validation purposes # /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api) hass.http.register_path('GET', URL_API, _handle_get_api)
@ -66,14 +75,15 @@ def setup(hass, config):
return True return True
def _handle_get_api(handler, path_match, data): def _handle_get_api(handler, path_match, data):
""" Renders the debug interface. """ """ Renders the debug interface. """
handler._json_message("API running.") handler.write_json_message("API running.")
def _handle_get_api_states(handler, path_match, data): def _handle_get_api_states(handler, path_match, data):
""" Returns a dict containing all entity ids and their state. """ """ 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): 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) state = handler.server.hass.states.get(entity_id)
if state: if state:
handler._write_json(state) handler.write_json(state)
else: 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): def _handle_post_state_entity(handler, path_match, data):
@ -99,7 +109,7 @@ def _handle_post_state_entity(handler, path_match, data):
try: try:
new_state = data['state'] new_state = data['state']
except KeyError: except KeyError:
handler._json_message("state not specified", HTTP_BAD_REQUEST) handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
return return
attributes = data['attributes'] if 'attributes' in data else None 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 status_code = HTTP_CREATED if is_new_state else HTTP_OK
handler._write_json( handler.write_json(
state.as_dict(), state.as_dict(),
status_code=status_code, status_code=status_code,
location=URL_API_STATES_ENTITY.format(entity_id)) location=URL_API_STATES_ENTITY.format(entity_id))
@ -121,7 +131,7 @@ def _handle_post_state_entity(handler, path_match, data):
def _handle_get_api_events(handler, path_match, data): def _handle_get_api_events(handler, path_match, data):
""" Handles getting overview of event listeners. """ """ Handles getting overview of event listeners. """
handler._write_json([{"event": key, "listener_count": value} handler.write_json([{"event": key, "listener_count": value}
for key, value for key, value
in handler.server.hass.bus.listeners.items()]) in handler.server.hass.bus.listeners.items()])
@ -137,8 +147,8 @@ def _handle_api_post_events_event(handler, path_match, event_data):
event_type = path_match.group('event_type') event_type = path_match.group('event_type')
if event_data is not None and not isinstance(event_data, dict): if event_data is not None and not isinstance(event_data, dict):
handler._json_message("event_data should be an object", handler.write_json_message(
HTTP_UNPROCESSABLE_ENTITY) "event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
event_origin = ha.EventOrigin.remote 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.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): def _handle_get_api_services(handler, path_match, data):
""" Handles getting overview of services. """ """ Handles getting overview of services. """
handler._write_json( handler.write_json(
[{"domain": key, "services": value} [{"domain": key, "services": value}
for key, value for key, value
in handler.server.hass.services.services.items()]) 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: with TrackStates(handler.server.hass) as changed_states:
handler.server.hass.services.call(domain, service, data, True) handler.server.hass.services.call(domain, service, data, True)
handler._write_json(changed_states) handler.write_json(changed_states)
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -188,21 +198,21 @@ def _handle_post_api_event_forward(handler, path_match, data):
host = data['host'] host = data['host']
api_password = data['api_password'] api_password = data['api_password']
except KeyError: except KeyError:
handler._json_message("No host or api_password received.", handler.write_json_message(
HTTP_BAD_REQUEST) "No host or api_password received.", HTTP_BAD_REQUEST)
return return
try: try:
port = int(data['port']) if 'port' in data else None port = int(data['port']) if 'port' in data else None
except ValueError: except ValueError:
handler._json_message( handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return return
api = rem.API(host, api_password, port) api = rem.API(host, api_password, port)
if not api.validate_api(): if not api.validate_api():
handler._json_message( handler.write_json_message(
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY) "Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
return return
@ -212,7 +222,7 @@ def _handle_post_api_event_forward(handler, path_match, data):
handler.server.event_forwarder.connect(api) 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): 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: try:
host = data['host'] host = data['host']
except KeyError: except KeyError:
handler._json_message("No host received.", HTTP_BAD_REQUEST) handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
return return
try: try:
port = int(data['port']) if 'port' in data else None port = int(data['port']) if 'port' in data else None
except ValueError: except ValueError:
handler._json_message( handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return return
@ -236,4 +246,4 @@ def _handle_delete_api_event_forward(handler, path_match, data):
handler.server.event_forwarder.disconnect(api) handler.server.event_forwarder.disconnect(api)
handler._json_message("Event forwarding cancelled.") handler.write_json_message("Event forwarding cancelled.")

View File

@ -1,9 +1,14 @@
"""
homeassistant.components.frontend
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a frontend for Home Assistant.
"""
import re import re
import os import os
import time import logging
import gzip
from . import frontend from . import version
import homeassistant.util as util import homeassistant.util as util
DOMAIN = 'frontend' DOMAIN = 'frontend'
@ -21,10 +26,13 @@ HTTP_UNPROCESSABLE_ENTITY = 422
URL_ROOT = "/" URL_ROOT = "/"
_LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
""" Setup serving the frontend. """ """ Setup serving the frontend. """
if 'http' not in hass.components: if 'http' not in hass.components:
_LOGGER.error('Dependency http is not loaded')
return False return False
hass.http.register_path('GET', URL_ROOT, _handle_get_root, 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: if handler.server.development:
app_url = "polymer/splash-login.html" app_url = "polymer/splash-login.html"
else: 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 # auto login if no password was set, else check api_password param
auth = (handler.server.api_password if handler.server.no_password_set 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): 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')) req_file = util.sanitize_path(path_match.group('file'))
# Strip md5 hash out of frontend filename # 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) path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
inp = None handler.write_file(path)
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()

View File

@ -74,18 +74,17 @@ Example result:
import json import json
import threading import threading
import logging import logging
import re import time
import gzip
import os
from http.server import SimpleHTTPRequestHandler, HTTPServer from http.server import SimpleHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import homeassistant as ha import homeassistant as ha
from homeassistant.const import ( from homeassistant.const import SERVER_PORT, AUTH_HEADER
SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER)
import homeassistant.remote as rem import homeassistant.remote as rem
import homeassistant.util as util import homeassistant.util as util
from . import frontend
DOMAIN = "http" DOMAIN = "http"
DEPENDENCIES = [] DEPENDENCIES = []
@ -220,12 +219,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
try: try:
data.update(json.loads(body_content)) data.update(json.loads(body_content))
except (TypeError, ValueError): 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 # ValueError if we could not parse JSON
_LOGGER.exception("Exception parsing JSON: %s", _LOGGER.exception(
body_content) "Exception parsing JSON: %s", body_content)
self.write_json_message(
self._json_message(
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return return
@ -271,7 +269,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
# For some calls we need a valid password # For some calls we need a valid password
if require_auth and api_password != self.server.api_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) "API password missing or incorrect.", HTTP_UNAUTHORIZED)
else: else:
@ -305,11 +303,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
""" DELETE request handler. """ """ DELETE request handler. """
self._handle_request('DELETE') 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. """ """ 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. """ """ Helper method to return JSON to the caller. """
self.send_response(status_code) self.send_response(status_code)
self.send_header('Content-type', 'application/json') self.send_header('Content-type', 'application/json')
@ -323,3 +321,56 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.wfile.write( self.wfile.write(
json.dumps(data, indent=4, sort_keys=True, json.dumps(data, indent=4, sort_keys=True,
cls=rem.JSONEncoder).encode("UTF-8")) 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)

View File

@ -23,11 +23,11 @@ mv polymer/bower_components/polymer/polymer.html.bak polymer/bower_components/po
# Generate the MD5 hash of the new frontend # Generate the MD5 hash of the new frontend
cd .. 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 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 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 else
echo 'Could not find a MD5 utility' echo 'Could not find a MD5 utility'
fi fi