Clean up http related components
parent
61f6aff056
commit
13ac71bdf0
|
@ -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:
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue