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...
This commit is contained in:
parent
9116eb166b
commit
d0320a9099
5 changed files with 571 additions and 1 deletions
|
@ -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', '')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue