Clean up http related components

pull/26/head
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(
"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:

View File

@ -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.")

View File

@ -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)

View File

@ -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)

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
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