WIP: Add WSGI stack
This is a fair chunk of the way towards adding a WSGI compatible stack for Home Assistant. The majot missing piece is auth/sessions. I was undecided on implementing the current auth mechanism, or adding a new mechanism (likely based on Werkzeug's signed cookies). Plenty of TODOs...pull/2063/head
parent
9116eb166b
commit
d0320a9099
|
@ -9,6 +9,8 @@ import logging
|
|||
import re
|
||||
import threading
|
||||
|
||||
from werkzeug.exceptions import NotFound, BadRequest
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||
|
@ -23,9 +25,10 @@ from homeassistant.const import (
|
|||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.wsgi import HomeAssistantView
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
DEPENDENCIES = ['http', 'wsgi']
|
||||
|
||||
STREAM_PING_PAYLOAD = "ping"
|
||||
STREAM_PING_INTERVAL = 50 # seconds
|
||||
|
@ -99,14 +102,38 @@ def setup(hass, config):
|
|||
hass.http.register_path('POST', URL_API_TEMPLATE,
|
||||
_handle_post_api_template)
|
||||
|
||||
hass.wsgi.register_view(APIStatusView)
|
||||
hass.wsgi.register_view(APIConfigView)
|
||||
hass.wsgi.register_view(APIDiscoveryView)
|
||||
hass.wsgi.register_view(APIEntityStateView)
|
||||
hass.wsgi.register_view(APIStatesView)
|
||||
hass.wsgi.register_view(APIEventListenersView)
|
||||
hass.wsgi.register_view(APIServicesView)
|
||||
hass.wsgi.register_view(APIDomainServicesView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class APIStatusView(HomeAssistantView):
|
||||
url = URL_API
|
||||
name = "api:status"
|
||||
|
||||
def get(self, request):
|
||||
return {'message': 'API running.'}
|
||||
|
||||
|
||||
def _handle_get_api(handler, path_match, data):
|
||||
"""Render the debug interface."""
|
||||
handler.write_json_message("API running.")
|
||||
|
||||
|
||||
class APIEventStream(HomeAssistantView):
|
||||
url = ""
|
||||
name = ""
|
||||
|
||||
# TODO Implement this...
|
||||
|
||||
|
||||
def _handle_get_api_stream(handler, path_match, data):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
gracefully_closed = False
|
||||
|
@ -177,11 +204,28 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
|
||||
|
||||
class APIConfigView(HomeAssistantView):
|
||||
url = URL_API_CONFIG
|
||||
name = "api:config"
|
||||
|
||||
def get(self, request):
|
||||
return self.hass.config.as_dict()
|
||||
|
||||
|
||||
def _handle_get_api_config(handler, path_match, data):
|
||||
"""Return the Home Assistant configuration."""
|
||||
handler.write_json(handler.server.hass.config.as_dict())
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = "api:discovery"
|
||||
|
||||
def get(self, request):
|
||||
# TODO
|
||||
return {}
|
||||
|
||||
|
||||
def _handle_get_api_discovery_info(handler, path_match, data):
|
||||
needs_auth = (handler.server.hass.config.api.api_password is not None)
|
||||
params = {
|
||||
|
@ -193,11 +237,69 @@ def _handle_get_api_discovery_info(handler, path_match, data):
|
|||
handler.write_json(params)
|
||||
|
||||
|
||||
class APIStatesView(HomeAssistantView):
|
||||
url = URL_API_STATES
|
||||
name = "api:states"
|
||||
|
||||
def get(self, request):
|
||||
return self.hass.states.all()
|
||||
|
||||
|
||||
def _handle_get_api_states(handler, path_match, data):
|
||||
"""Return a dict containing all entity ids and their state."""
|
||||
handler.write_json(handler.server.hass.states.all())
|
||||
|
||||
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
url = "/api/states/<entity_id>"
|
||||
name = "api:entity-state"
|
||||
|
||||
def get(self, request, entity_id):
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state:
|
||||
return state
|
||||
else:
|
||||
raise NotFound("State does not exist.")
|
||||
|
||||
def post(self, request, entity_id):
|
||||
try:
|
||||
new_state = request.values['state']
|
||||
except KeyError:
|
||||
raise BadRequest("state not specified")
|
||||
|
||||
attributes = request.values.get('attributes')
|
||||
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
self.hass.states.set(entity_id, new_state, attributes)
|
||||
|
||||
# Read the state back for our response
|
||||
msg = json.dumps(
|
||||
self.hass.states.get(entity_id).as_dict(),
|
||||
sort_keys=True,
|
||||
cls=rem.JSONEncoder
|
||||
).encode('UTF-8')
|
||||
|
||||
resp = Response(msg, mimetype="application/json")
|
||||
|
||||
if is_new_state:
|
||||
resp.status_code = HTTP_CREATED
|
||||
|
||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
return resp
|
||||
|
||||
def delete(self, request, entity_id):
|
||||
if self.hass.states.remove(entity_id):
|
||||
return {"message:" "Entity removed"}
|
||||
else:
|
||||
return {
|
||||
"message": "Entity not found",
|
||||
"status_code": HTTP_NOT_FOUND,
|
||||
}
|
||||
|
||||
|
||||
def _handle_get_api_states_entity(handler, path_match, data):
|
||||
"""Return the state of a specific entity."""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
@ -257,11 +359,40 @@ def _handle_delete_state_entity(handler, path_match, data):
|
|||
"Entity removed", HTTP_OK)
|
||||
|
||||
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
url = URL_API_EVENTS
|
||||
name = "api:event-listeners"
|
||||
|
||||
def get(self, request):
|
||||
return events_json(self.hass)
|
||||
|
||||
|
||||
def _handle_get_api_events(handler, path_match, data):
|
||||
"""Handle getting overview of event listeners."""
|
||||
handler.write_json(events_json(handler.server.hass))
|
||||
|
||||
|
||||
class APIEventView(HomeAssistantView):
|
||||
url = '/api/events/<event_type>'
|
||||
name = "api:event"
|
||||
|
||||
def post(self, request, event_type):
|
||||
event_data = request.values
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
self.hass.bus.fire(event_type, request.values, ha.EventOrigin.remote)
|
||||
|
||||
return {"message": "Event {} fired.".format(event_type)}
|
||||
|
||||
|
||||
def _handle_api_post_events_event(handler, path_match, event_data):
|
||||
"""Handle firing of an event.
|
||||
|
||||
|
@ -292,11 +423,30 @@ def _handle_api_post_events_event(handler, path_match, event_data):
|
|||
handler.write_json_message("Event {} fired.".format(event_type))
|
||||
|
||||
|
||||
class APIServicesView(HomeAssistantView):
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
def get(self, request):
|
||||
return services_json(self.hass)
|
||||
|
||||
|
||||
def _handle_get_api_services(handler, path_match, data):
|
||||
"""Handle getting overview of services."""
|
||||
handler.write_json(services_json(handler.server.hass))
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
url = "/api/services/<domain>/<service>"
|
||||
name = "api:domain-services"
|
||||
|
||||
def post(self, request):
|
||||
with TrackStates(self.hass) as changed_states:
|
||||
self.hass.services.call(domain, service, request.values, True)
|
||||
|
||||
return changed_states
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_services_domain_service(handler, path_match, data):
|
||||
"""Handle calling a service.
|
||||
|
@ -312,6 +462,68 @@ def _handle_post_api_services_domain_service(handler, path_match, data):
|
|||
handler.write_json(changed_states)
|
||||
|
||||
|
||||
class APIEventForwardingView(HomeAssistantView):
|
||||
url = URL_API_EVENT_FORWARD
|
||||
name = "api:event-forward"
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
host = request.values['host']
|
||||
api_password = request.values['api_password']
|
||||
except KeyError:
|
||||
return {
|
||||
"message": "No host or api_password received.",
|
||||
"status_code": HTTP_BAD_REQUEST,
|
||||
}
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return {
|
||||
"message": "Invalid value received for port.",
|
||||
"status_code": HTTP_UNPROCESSABLE_ENTITY,
|
||||
}
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
if not api.validate_api():
|
||||
return {
|
||||
"message": "Unable to validate API.",
|
||||
"status_code": HTTP_UNPROCESSABLE_ENTITY,
|
||||
}
|
||||
|
||||
if self.hass.event_forwarder is None:
|
||||
self.hass.event_forwarder = rem.EventForwarder(self.hass)
|
||||
|
||||
self.hass.event_forwarder.connect(api)
|
||||
|
||||
return {"message": "Event forwarding setup."}
|
||||
|
||||
def delete(self, request):
|
||||
try:
|
||||
host = request.values['host']
|
||||
except KeyError:
|
||||
return {
|
||||
"message": "No host received.",
|
||||
"status_code": HTTP_BAD_REQUEST,
|
||||
}
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return {
|
||||
"message": "Invalid value received for port",
|
||||
"status_code": HTTP_UNPROCESSABLE_ENTITY,
|
||||
}
|
||||
|
||||
if self.hass.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.hass.event_forwarder.disconnect(api)
|
||||
|
||||
return {"message": "Event forwarding cancelled."}
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_event_forward(handler, path_match, data):
|
||||
"""Handle adding an event forwarding target."""
|
||||
|
@ -369,17 +581,43 @@ def _handle_delete_api_event_forward(handler, path_match, data):
|
|||
handler.write_json_message("Event forwarding cancelled.")
|
||||
|
||||
|
||||
class APIComponentsView(HomeAssistantView):
|
||||
url = URL_API_COMPONENTS
|
||||
name = "api:components"
|
||||
|
||||
def get(self, request):
|
||||
return self.hass.config.components
|
||||
|
||||
|
||||
def _handle_get_api_components(handler, path_match, data):
|
||||
"""Return all the loaded components."""
|
||||
handler.write_json(handler.server.hass.config.components)
|
||||
|
||||
|
||||
class APIErrorLogView(HomeAssistantView):
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error-log"
|
||||
|
||||
def get(self, request):
|
||||
# TODO
|
||||
return {}
|
||||
|
||||
|
||||
def _handle_get_api_error_log(handler, path_match, data):
|
||||
"""Return the logged errors for this session."""
|
||||
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
|
||||
False)
|
||||
|
||||
|
||||
class APILogOutView(HomeAssistantView):
|
||||
url = URL_API_LOG_OUT
|
||||
name = "api:log-out"
|
||||
|
||||
def post(self, request):
|
||||
# TODO
|
||||
return {}
|
||||
|
||||
|
||||
def _handle_post_api_log_out(handler, path_match, data):
|
||||
"""Log user out."""
|
||||
handler.send_response(HTTP_OK)
|
||||
|
@ -387,6 +625,15 @@ def _handle_post_api_log_out(handler, path_match, data):
|
|||
handler.end_headers()
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
|
||||
def post(self, request):
|
||||
# TODO
|
||||
return {}
|
||||
|
||||
|
||||
def _handle_post_api_template(handler, path_match, data):
|
||||
"""Log user out."""
|
||||
template_string = data.get('template', '')
|
||||
|
|
|
@ -3,16 +3,26 @@ import re
|
|||
import os
|
||||
import logging
|
||||
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from . import version, mdi_version
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||
from homeassistant.components import api
|
||||
from homeassistant.components.wsgi import HomeAssistantView
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
|
||||
INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
|
||||
|
||||
TEMPLATES = Environment(
|
||||
loader=FileSystemLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||
)
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FRONTEND_URLS = [
|
||||
|
@ -49,9 +59,61 @@ def setup(hass, config):
|
|||
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_local, False)
|
||||
|
||||
hass.wsgi.register_view(IndexView)
|
||||
hass.wsgi.register_view(BootstrapView)
|
||||
|
||||
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
if hass.wsgi.development:
|
||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||
else:
|
||||
sw_path = "service_worker.js"
|
||||
|
||||
hass.wsgi.register_static_path(
|
||||
"/service_worker.js",
|
||||
os.path.join(www_static_path, sw_path)
|
||||
)
|
||||
hass.wsgi.register_static_path("/static", www_static_path)
|
||||
hass.wsgi.register_static_path("/local", hass.config.path('www'))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class BootstrapView(HomeAssistantView):
|
||||
url = URL_API_BOOTSTRAP
|
||||
name = "api:bootstrap"
|
||||
|
||||
def get(self, request):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
|
||||
return {
|
||||
'config': self.hass.config.as_dict(),
|
||||
'states': self.hass.states.all(),
|
||||
'events': api.events_json(self.hass),
|
||||
'services': api.services_json(self.hass),
|
||||
}
|
||||
|
||||
|
||||
class IndexView(HomeAssistantView):
|
||||
url = URL_ROOT
|
||||
name = "frontend:index"
|
||||
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo', '/devTemplate', '/states/<entity>']
|
||||
|
||||
def get(self, request):
|
||||
app_url = "frontend-{}.html".format(version.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = ('no_password_set' if request.api_password is None
|
||||
else request.values.get('api_password', ''))
|
||||
|
||||
template = TEMPLATES.get_template('index.html')
|
||||
|
||||
resp = template.render(app_url=app_url, auth=auth,
|
||||
icons=mdi_version.VERSION)
|
||||
|
||||
return Response(resp, mimetype="text/html")
|
||||
|
||||
|
||||
def _handle_get_api_bootstrap(handler, path_match, data):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
hass = handler.server.hass
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home Assistant</title>
|
||||
|
||||
<link rel='manifest' href='/static/manifest.json'>
|
||||
<link rel='icon' href='/static/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
<style>
|
||||
#ha-init-skeleton {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-webkit-justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin-bottom: 123px;
|
||||
}
|
||||
</style>
|
||||
<link rel='import' href='/static/{{ app_url }}' async>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
<div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
|
||||
<script>
|
||||
var webComponentsSupported = ('registerElement' in document &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'))
|
||||
if (!webComponentsSupported) {
|
||||
var script = document.createElement('script')
|
||||
script.async = true
|
||||
script.src = '/static/webcomponents-lite.min.js'
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
</script>
|
||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,201 @@
|
|||
"""
|
||||
This module provides WSGI application to serve the Home Assistant API.
|
||||
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import re
|
||||
|
||||
from eventlet import wsgi
|
||||
import eventlet
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, HTTP_OK, HTTP_NOT_FOUND, HTTP_BAD_REQUEST
|
||||
)
|
||||
|
||||
from static import Cling
|
||||
|
||||
from werkzeug.wsgi import DispatcherMiddleware
|
||||
from werkzeug.wrappers import Response, BaseRequest, AcceptMixin
|
||||
from werkzeug.routing import Map, Rule
|
||||
from werkzeug.exceptions import (
|
||||
MethodNotAllowed, NotFound, BadRequest, Unauthorized
|
||||
)
|
||||
|
||||
|
||||
class Request(BaseRequest, AcceptMixin):
|
||||
pass
|
||||
|
||||
|
||||
class StaticFileServer(object):
|
||||
def __call__(self, environ, start_response):
|
||||
app = DispatcherMiddleware(self.base_app, self.extra_apps)
|
||||
# Strip out any cachebusting MD% fingerprints
|
||||
fingerprinted = _FINGERPRINT.match(environ['PATH_INFO'])
|
||||
if fingerprinted:
|
||||
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
|
||||
return app(environ, start_response)
|
||||
|
||||
DOMAIN = "wsgi"
|
||||
REQUIREMENTS = ("eventlet==0.18.4", "static3==0.6.1", "Werkzeug==0.11.5",)
|
||||
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
CONF_SERVER_HOST = "server_host"
|
||||
CONF_SERVER_PORT = "server_port"
|
||||
CONF_DEVELOPMENT = "development"
|
||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||
CONF_SSL_KEY = 'ssl_key'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the HTTP API and debug interface."""
|
||||
conf = config.get(DOMAIN, {})
|
||||
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
development=str(conf.get(CONF_DEVELOPMENT, "")) == "1",
|
||||
server_host=conf.get(CONF_SERVER_HOST, '0.0.0.0'),
|
||||
server_port=conf.get(CONF_SERVER_PORT, SERVER_PORT),
|
||||
api_password=util.convert(conf.get(CONF_API_PASSWORD), str),
|
||||
ssl_certificate=conf.get(CONF_SSL_CERTIFICATE),
|
||||
ssl_key=conf.get(CONF_SSL_KEY),
|
||||
)
|
||||
|
||||
hass.bus.listen_once(
|
||||
ha.EVENT_HOMEASSISTANT_START,
|
||||
lambda event:
|
||||
threading.Thread(target=server.start, daemon=True,
|
||||
name='WSGI-server').start())
|
||||
|
||||
hass.wsgi = server
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class HomeAssistantWSGI(object):
|
||||
def __init__(self, hass, development, api_password, ssl_certificate,
|
||||
ssl_key, server_host, server_port):
|
||||
self.url_map = Map()
|
||||
self.views = {}
|
||||
self.hass = hass
|
||||
self.extra_apps = {}
|
||||
self.development = development
|
||||
self.api_password = api_password
|
||||
self.ssl_certificate = ssl_certificate
|
||||
self.ssl_key = ssl_key
|
||||
|
||||
def register_view(self, view):
|
||||
""" Register a view with the WSGI server.
|
||||
|
||||
The view argument must inherit from the HomeAssistantView class, and
|
||||
it must have (globally unique) 'url' and 'name' attributes.
|
||||
"""
|
||||
if view.name in self.views:
|
||||
_LOGGER.warning("View '{}' is being overwritten".format(view.name))
|
||||
self.views[view.name] = view(self.hass)
|
||||
# TODO Warn if we're overriding an existing view
|
||||
rule = Rule(view.url, endpoint=view.name)
|
||||
self.url_map.add(rule)
|
||||
for url in view.extra_urls:
|
||||
rule = Rule(url, endpoint=view.name)
|
||||
self.url_map.add(rule)
|
||||
|
||||
def register_static_path(self, url_root, path):
|
||||
# TODO Warn if we're overwriting an existing path
|
||||
self.extra_apps[url_root] = Cling(path)
|
||||
|
||||
def start(self):
|
||||
sock = eventlet.listen(('', 8090))
|
||||
if self.ssl_certificate:
|
||||
eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
|
||||
keyfile=self.ssl_key, server_side=True)
|
||||
wsgi.server(sock, self)
|
||||
|
||||
def dispatch_request(self, request):
|
||||
adapter = self.url_map.bind_to_environ(request.environ)
|
||||
try:
|
||||
endpoint, values = adapter.match()
|
||||
return self.views[endpoint].handle_request(request, **values)
|
||||
except BadRequest as e:
|
||||
return self.handle_error(request, str(e), HTTP_BAD_REQUEST)
|
||||
except NotFound as e:
|
||||
return self.handle_error(request, str(e), HTTP_NOT_FOUND)
|
||||
except MethodNotAllowed as e:
|
||||
return self.handle_error(request, str(e), 405)
|
||||
except Unauthorized as e:
|
||||
return self.handle_error(request, str(e), 401)
|
||||
# TODO This long chain of except blocks is silly. _handle_error should
|
||||
# just take the exception as an argument and parse the status code
|
||||
# itself
|
||||
|
||||
def base_app(self, environ, start_response):
|
||||
request = Request(environ)
|
||||
request.api_password = self.api_password
|
||||
request.development = self.development
|
||||
response = self.dispatch_request(request)
|
||||
return response(environ, start_response)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
app = DispatcherMiddleware(self.base_app, self.extra_apps)
|
||||
# Strip out any cachebusting MD5 fingerprints
|
||||
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
|
||||
if fingerprinted:
|
||||
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
|
||||
return app(environ, start_response)
|
||||
|
||||
def _handle_error(self, request, message, status):
|
||||
if request.accept_mimetypes.accept_json:
|
||||
message = json.dumps({
|
||||
"result": "error",
|
||||
"message": message,
|
||||
})
|
||||
mimetype = "application/json"
|
||||
else:
|
||||
mimetype = "text/plain"
|
||||
return Response(message, status=status, mimetype=mimetype)
|
||||
|
||||
|
||||
class HomeAssistantView(object):
|
||||
extra_urls = []
|
||||
requires_auth = True # Views inheriting from this class can override this
|
||||
|
||||
def __init__(self, hass):
|
||||
self.hass = hass
|
||||
|
||||
def handle_request(self, request, **values):
|
||||
try:
|
||||
handler = getattr(self, request.method.lower())
|
||||
except AttributeError:
|
||||
raise MethodNotAllowed
|
||||
# TODO This would be a good place to check the auth if
|
||||
# self.requires_auth is true, and raise Unauthorized on a failure
|
||||
result = handler(request, **values)
|
||||
if isinstance(result, Response):
|
||||
# The method handler returned a ready-made Response, how nice of it
|
||||
return result
|
||||
elif (isinstance(result, dict) or
|
||||
isinstance(result, list) or
|
||||
isinstance(result, ha.State)):
|
||||
# There are a few result types we know we always want to jsonify
|
||||
if isinstance(result, dict) and 'status_code' in result:
|
||||
status_code = result['status_code']
|
||||
del result['status_code']
|
||||
else:
|
||||
status_code = HTTP_OK
|
||||
msg = json.dumps(
|
||||
result,
|
||||
sort_keys=True,
|
||||
cls=rem.JSONEncoder
|
||||
).encode('UTF-8')
|
||||
return Response(msg, mimetype="application/json",
|
||||
status_code=status_code)
|
|
@ -23,6 +23,9 @@ SoCo==0.11.1
|
|||
# homeassistant.components.notify.twitter
|
||||
TwitterAPI==2.4.1
|
||||
|
||||
# homeassistant.components.wsgi
|
||||
Werkzeug==0.11.5
|
||||
|
||||
# homeassistant.components.apcupsd
|
||||
apcaccess==0.0.4
|
||||
|
||||
|
@ -53,6 +56,9 @@ dweepy==0.2.0
|
|||
# homeassistant.components.sensor.eliqonline
|
||||
eliqonline==1.0.12
|
||||
|
||||
# homeassistant.components.wsgi
|
||||
eventlet==0.18.4
|
||||
|
||||
# homeassistant.components.thermostat.honeywell
|
||||
evohomeclient==0.2.5
|
||||
|
||||
|
@ -331,6 +337,9 @@ somecomfort==0.2.1
|
|||
# homeassistant.components.sensor.speedtest
|
||||
speedtest-cli==0.3.4
|
||||
|
||||
# homeassistant.components.wsgi
|
||||
static3==0.6.1
|
||||
|
||||
# homeassistant.components.sensor.steam_online
|
||||
steamodd==4.21
|
||||
|
||||
|
|
Loading…
Reference in New Issue