core/homeassistant/components/api.py

450 lines
13 KiB
Python
Raw Normal View History

2015-01-30 16:26:06 +00:00
"""
2016-02-23 20:06:50 +00:00
Rest API for Home Assistant.
2015-10-25 14:30:56 +00:00
For more details about the RESTful API, please refer to the documentation at
2015-11-09 12:12:18 +00:00
https://home-assistant.io/developers/api/
2015-01-30 16:26:06 +00:00
"""
2016-02-19 05:27:50 +00:00
import json
2015-01-30 16:26:06 +00:00
import logging
2015-08-17 03:44:46 +00:00
import homeassistant.core as ha
import homeassistant.remote as rem
2015-11-07 09:44:02 +00:00
from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import (
2016-05-14 07:58:36 +00:00
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES,
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__)
2016-02-19 05:27:50 +00:00
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates
2016-02-23 20:06:50 +00:00
from homeassistant.helpers import template
2016-05-14 07:58:36 +00:00
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'api'
2016-05-14 07:58:36 +00:00
DEPENDENCIES = ['http']
2015-02-19 08:15:21 +00:00
STREAM_PING_PAYLOAD = "ping"
STREAM_PING_INTERVAL = 50 # seconds
2015-01-30 16:26:06 +00:00
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
2016-02-23 20:06:50 +00:00
"""Register the API with the HTTP interface."""
hass.wsgi.register_view(APIStatusView)
2016-05-10 01:26:52 +00:00
hass.wsgi.register_view(APIEventStream)
hass.wsgi.register_view(APIConfigView)
hass.wsgi.register_view(APIDiscoveryView)
hass.wsgi.register_view(APIStatesView)
2016-05-10 01:26:52 +00:00
hass.wsgi.register_view(APIEntityStateView)
hass.wsgi.register_view(APIEventListenersView)
2016-05-10 01:26:52 +00:00
hass.wsgi.register_view(APIEventView)
hass.wsgi.register_view(APIServicesView)
hass.wsgi.register_view(APIDomainServicesView)
2016-05-10 01:26:52 +00:00
hass.wsgi.register_view(APIEventForwardingView)
hass.wsgi.register_view(APIComponentsView)
hass.wsgi.register_view(APIErrorLogView)
hass.wsgi.register_view(APILogOutView)
hass.wsgi.register_view(APITemplateView)
return True
2015-01-30 16:26:06 +00:00
class APIStatusView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Status requests."""
url = URL_API
name = "api:status"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Retrieve if API is running."""
return self.json_message('API running.')
class APIEventStream(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle EventSt requests."""
2015-02-14 02:59:42 +00:00
2016-05-14 07:58:36 +00:00
url = URL_API_STREAM
name = "api:stream"
2015-09-24 04:35:23 +00:00
2016-05-14 07:58:36 +00:00
def get(self, request):
"""Provide a streaming interface for the event bus."""
2016-05-14 22:07:20 +00:00
import eventlet
from eventlet import queue as eventlet_queue
import queue as thread_queue
from threading import Event
from time import time
to_write = thread_queue.Queue()
# to_write = eventlet.Queue()
2016-05-14 07:58:36 +00:00
stop_obj = object()
hass = self.hass
2016-05-14 22:07:20 +00:00
connection_closed = Event()
2016-05-14 07:58:36 +00:00
restrict = request.args.get('restrict')
if restrict:
restrict = restrict.split(',')
2016-05-14 22:07:20 +00:00
restrict = False
2016-05-14 07:58:36 +00:00
def ping(now):
"""Add a ping message to queue."""
2016-05-14 22:07:20 +00:00
print(id(stop_obj), 'ping')
to_write.put(STREAM_PING_PAYLOAD)
2016-05-14 07:58:36 +00:00
def forward_events(event):
"""Forward events to the open request."""
2016-05-14 22:07:20 +00:00
print(id(stop_obj), 'forwarding', event)
2016-05-14 07:58:36 +00:00
if event.event_type == EVENT_TIME_CHANGED:
pass
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
2016-05-14 22:07:20 +00:00
to_write.put(stop_obj)
2016-05-14 07:58:36 +00:00
else:
2016-05-14 22:07:20 +00:00
to_write.put(json.dumps(event, cls=rem.JSONEncoder))
2016-05-14 07:58:36 +00:00
def stream():
"""Stream events to response."""
if restrict:
2016-05-14 22:07:20 +00:00
for event_type in restrict:
hass.bus.listen(event_type, forward_events)
2016-05-14 07:58:36 +00:00
else:
hass.bus.listen(MATCH_ALL, forward_events)
2016-05-14 22:07:20 +00:00
attached_ping = track_utc_time_change(
hass, ping, second=(0, 30))
print(id(stop_obj), 'attached goodness')
2015-02-19 08:15:21 +00:00
2016-05-14 22:07:20 +00:00
while not connection_closed.is_set():
try:
print(id(stop_obj), "Try getting obj")
payload = to_write.get(False)
2015-02-14 02:59:42 +00:00
2016-05-14 07:58:36 +00:00
if payload is stop_obj:
break
2015-02-14 02:59:42 +00:00
2016-05-14 07:58:36 +00:00
msg = "data: {}\n\n".format(payload)
2016-05-14 22:07:20 +00:00
print(id(stop_obj), msg)
2016-05-14 07:58:36 +00:00
yield msg.encode("UTF-8")
2016-05-14 22:07:20 +00:00
except eventlet_queue.Empty:
print(id(stop_obj), "queue empty, sleep 0.5")
eventlet.sleep(.5)
except GeneratorExit:
pass
print(id(stop_obj), "cleaning up")
2015-02-19 08:15:21 +00:00
2016-05-14 07:58:36 +00:00
hass.bus.remove_listener(EVENT_TIME_CHANGED, attached_ping)
2016-05-14 07:58:36 +00:00
if restrict:
for event in restrict:
hass.bus.remove_listener(event, forward_events)
else:
hass.bus.remove_listener(MATCH_ALL, forward_events)
2015-02-19 08:15:21 +00:00
2016-05-14 22:07:20 +00:00
resp = self.Response(stream(), mimetype='text/event-stream')
def closing():
print()
print()
print()
print()
print()
print()
print()
print()
print(id(stop_obj), "CLOSING RESPONSE")
print()
print()
print()
print()
print()
print()
print()
connection_closed.set()
resp.call_on_close(closing)
return resp
2015-02-14 02:59:42 +00:00
class APIConfigView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Config requests."""
url = URL_API_CONFIG
name = "api:config"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get current configuration."""
return self.json(self.hass.config.as_dict())
2015-05-02 01:24:32 +00:00
class APIDiscoveryView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to provide discovery info."""
requires_auth = False
url = URL_API_DISCOVERY_INFO
name = "api:discovery"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get discovery info."""
needs_auth = self.hass.config.api.api_password is not None
return self.json({
'base_url': self.hass.config.api.base_url,
'location_name': self.hass.config.location_name,
'requires_api_password': needs_auth,
'version': __version__
})
class APIStatesView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle States requests."""
url = URL_API_STATES
name = "api:states"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get current states."""
return self.json(self.hass.states.all())
class APIEntityStateView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle EntityState requests."""
2016-05-15 04:18:46 +00:00
url = "/api/states/<entity(exist=False):entity_id>"
name = "api:entity-state"
def get(self, request, entity_id):
2016-05-14 07:58:36 +00:00
"""Retrieve state of entity."""
state = self.hass.states.get(entity_id)
if state:
2016-05-14 07:58:36 +00:00
return self.json(state)
else:
2016-05-14 07:58:36 +00:00
return self.json_message('Entity not found', HTTP_NOT_FOUND)
def post(self, request, entity_id):
2016-05-14 07:58:36 +00:00
"""Update state of entity."""
try:
2016-05-14 07:58:36 +00:00
new_state = request.json['state']
except KeyError:
2016-05-14 07:58:36 +00:00
return self.json_message('No state specified', HTTP_BAD_REQUEST)
2016-05-14 07:58:36 +00:00
attributes = request.json.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
2016-05-14 07:58:36 +00:00
resp = self.json(self.hass.states.get(entity_id))
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):
2016-05-14 07:58:36 +00:00
"""Remove entity."""
if self.hass.states.remove(entity_id):
2016-05-14 07:58:36 +00:00
return self.json_message('Entity removed')
else:
2016-05-14 07:58:36 +00:00
return self.json_message('Entity not found', HTTP_NOT_FOUND)
class APIEventListenersView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle EventListeners requests."""
url = URL_API_EVENTS
name = "api:event-listeners"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get event listeners."""
return self.json(events_json(self.hass))
class APIEventView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Event requests."""
url = '/api/events/<event_type>'
name = "api:event"
def post(self, request, event_type):
2016-05-14 07:58:36 +00:00
"""Fire events."""
event_data = request.json
if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object',
HTTP_BAD_REQUEST)
# 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
2016-05-14 07:58:36 +00:00
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
2016-05-14 07:58:36 +00:00
return self.json_message("Event {} fired.".format(event_type))
class APIServicesView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Services requests."""
url = URL_API_SERVICES
name = "api:services"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get registered services."""
return self.json(services_json(self.hass))
class APIDomainServicesView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle DomainServices requests."""
url = "/api/services/<domain>/<service>"
name = "api:domain-services"
2016-05-14 07:58:36 +00:00
def post(self, request, domain, service):
"""Call a service.
2016-05-14 07:58:36 +00:00
Returns a list of changed states.
"""
with TrackStates(self.hass) as changed_states:
self.hass.services.call(domain, service, request.json, True)
2016-05-14 07:58:36 +00:00
return self.json(changed_states)
class APIEventForwardingView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle EventForwarding requests."""
url = URL_API_EVENT_FORWARD
name = "api:event-forward"
2016-05-14 07:58:36 +00:00
event_forwarder = None
def post(self, request):
2016-05-14 07:58:36 +00:00
"""Setup an event forwarder."""
data = request.json
if data is None:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
2016-05-14 07:58:36 +00:00
host = data['host']
api_password = data['api_password']
except KeyError:
2016-05-14 07:58:36 +00:00
return self.json_message("No host or api_password received.",
HTTP_BAD_REQUEST)
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
2016-05-14 07:58:36 +00:00
return self.json_message("Invalid value received for port.",
HTTP_UNPROCESSABLE_ENTITY)
api = rem.API(host, api_password, port)
if not api.validate_api():
2016-05-14 07:58:36 +00:00
return self.json_message("Unable to validate API.",
HTTP_UNPROCESSABLE_ENTITY)
2016-05-14 07:58:36 +00:00
if self.event_forwarder is None:
self.event_forwarder = rem.EventForwarder(self.hass)
2016-05-14 07:58:36 +00:00
self.event_forwarder.connect(api)
2016-05-14 07:58:36 +00:00
return self.json_message("Event forwarding setup.")
def delete(self, request):
2016-05-14 07:58:36 +00:00
"""Remove event forwarer."""
data = request.json
if data is None:
return self.json_message("No data received.", HTTP_BAD_REQUEST)
try:
2016-05-14 07:58:36 +00:00
host = data['host']
except KeyError:
2016-05-14 07:58:36 +00:00
return self.json_message("No host received.", HTTP_BAD_REQUEST)
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
2016-05-14 07:58:36 +00:00
return self.json_message("Invalid value received for port.",
HTTP_UNPROCESSABLE_ENTITY)
2016-05-14 07:58:36 +00:00
if self.event_forwarder is not None:
api = rem.API(host, None, port)
2016-05-14 07:58:36 +00:00
self.event_forwarder.disconnect(api)
2016-05-14 07:58:36 +00:00
return self.json_message("Event forwarding cancelled.")
2015-02-01 03:08:50 +00:00
class APIComponentsView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Components requests."""
url = URL_API_COMPONENTS
name = "api:components"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Get current loaded components."""
return self.json(self.hass.config.components)
2015-05-02 02:02:29 +00:00
2015-05-02 03:56:10 +00:00
class APIErrorLogView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle ErrorLog requests."""
url = URL_API_ERROR_LOG
name = "api:error-log"
def get(self, request):
2016-05-14 07:58:36 +00:00
"""Serve error log."""
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
2015-11-07 09:44:02 +00:00
class APILogOutView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle Log Out requests."""
url = URL_API_LOG_OUT
name = "api:log-out"
def post(self, request):
2016-05-14 07:58:36 +00:00
"""Handle log out."""
# TODO kill session
return {}
class APITemplateView(HomeAssistantView):
2016-05-14 07:58:36 +00:00
"""View to handle requests."""
url = URL_API_TEMPLATE
name = "api:template"
def post(self, request):
2016-05-14 07:58:36 +00:00
"""Render a template."""
try:
return template.render(self.hass, request.json['template'],
request.json.get('variables'))
except TemplateError as ex:
return self.json_message('Error rendering template: {}'.format(ex),
HTTP_BAD_REQUEST)
2015-12-10 07:56:20 +00:00
def services_json(hass):
2016-02-23 20:06:50 +00:00
"""Generate services data to JSONify."""
2015-05-02 02:02:29 +00:00
return [{"domain": key, "services": value}
for key, value in hass.services.services.items()]
2015-05-02 03:56:10 +00:00
def events_json(hass):
2016-02-23 20:06:50 +00:00
"""Generate event data to JSONify."""
2015-05-02 02:02:29 +00:00
return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()]