core/homeassistant/components/api.py

397 lines
12 KiB
Python
Raw Normal View History

2015-01-30 16:26:06 +00:00
"""
homeassistant.components.api
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a 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
"""
import re
2015-01-30 16:26:06 +00:00
import logging
2015-02-14 02:59:42 +00:00
import threading
import json
2015-08-17 03:44:46 +00:00
import homeassistant.core as ha
2015-03-16 06:36:42 +00:00
from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem
2015-12-10 07:56:20 +00:00
from homeassistant.util import template
2015-11-07 09:44:02 +00:00
from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
2015-12-10 07:56:20 +00:00
URL_API_TEMPLATE, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
2015-12-10 07:56:20 +00:00
HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN)
DOMAIN = 'api'
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):
2015-01-30 16:26:06 +00:00
""" Register the API with the HTTP interface. """
# /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api)
2015-02-14 02:59:42 +00:00
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
2015-05-02 01:24:32 +00:00
# /api/config
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
2015-05-02 02:02:29 +00:00
# /api/bootstrap
hass.http.register_path(
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
# /states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path(
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_get_api_states_entity)
hass.http.register_path(
'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_post_state_entity)
hass.http.register_path(
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_handle_post_state_entity)
# /events
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
hass.http.register_path(
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
_handle_api_post_events_event)
# /services
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
hass.http.register_path(
'POST',
re.compile((r'/api/services/'
r'(?P<domain>[a-zA-Z\._0-9]+)/'
r'(?P<service>[a-zA-Z\._0-9]+)')),
_handle_post_api_services_domain_service)
# /event_forwarding
hass.http.register_path(
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
hass.http.register_path(
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
2015-02-01 03:08:50 +00:00
# /components
hass.http.register_path(
'GET', URL_API_COMPONENTS, _handle_get_api_components)
2015-11-07 09:44:02 +00:00
hass.http.register_path('GET', URL_API_ERROR_LOG,
_handle_get_api_error_log)
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
2015-12-10 07:56:20 +00:00
hass.http.register_path('POST', URL_API_TEMPLATE,
_handle_post_api_template)
return True
2015-01-30 16:26:06 +00:00
def _handle_get_api(handler, path_match, data):
""" Renders the debug interface. """
2015-01-30 16:26:06 +00:00
handler.write_json_message("API running.")
2015-02-14 02:59:42 +00:00
def _handle_get_api_stream(handler, path_match, data):
""" Provide a streaming interface for the event bus. """
2015-02-19 08:15:21 +00:00
gracefully_closed = False
2015-02-14 02:59:42 +00:00
hass = handler.server.hass
wfile = handler.wfile
2015-02-19 08:15:21 +00:00
write_lock = threading.Lock()
2015-02-14 02:59:42 +00:00
block = threading.Event()
2015-11-29 06:14:40 +00:00
session_id = None
2015-02-14 02:59:42 +00:00
2015-09-24 04:35:23 +00:00
restrict = data.get('restrict')
if restrict:
restrict = restrict.split(',')
2015-02-19 08:15:21 +00:00
def write_message(payload):
""" Writes a message to the output. """
with write_lock:
msg = "data: {}\n\n".format(payload)
try:
wfile.write(msg.encode("UTF-8"))
wfile.flush()
2015-12-12 02:43:00 +00:00
except (IOError, ValueError):
# IOError: socket errors
# ValueError: raised when 'I/O operation on closed file'
2015-02-19 08:15:21 +00:00
block.set()
def forward_events(event):
2015-02-14 02:59:42 +00:00
""" Forwards events to the open request. """
2015-02-19 08:15:21 +00:00
nonlocal gracefully_closed
2015-12-15 07:20:43 +00:00
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
2015-02-14 02:59:42 +00:00
return
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
2015-02-19 08:15:21 +00:00
gracefully_closed = True
2015-02-14 02:59:42 +00:00
block.set()
return
2015-12-15 07:20:43 +00:00
handler.server.sessions.extend_validation(session_id)
2015-02-19 08:15:21 +00:00
write_message(json.dumps(event, cls=rem.JSONEncoder))
2015-02-14 02:59:42 +00:00
handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/event-stream')
2015-11-29 06:14:40 +00:00
session_id = handler.set_session_cookie_header()
2015-02-14 02:59:42 +00:00
handler.end_headers()
2015-12-15 07:20:43 +00:00
if restrict:
for event in restrict:
hass.bus.listen(event, forward_events)
else:
hass.bus.listen(MATCH_ALL, forward_events)
2015-02-19 08:15:21 +00:00
while True:
write_message(STREAM_PING_PAYLOAD)
2015-02-19 08:15:21 +00:00
block.wait(STREAM_PING_INTERVAL)
if block.is_set():
break
if not gracefully_closed:
_LOGGER.info("Found broken event stream to %s, cleaning up",
handler.client_address[0])
2015-02-14 02:59:42 +00:00
2015-12-15 07:20:43 +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-14 02:59:42 +00:00
2015-05-02 01:24:32 +00:00
def _handle_get_api_config(handler, path_match, data):
2015-05-02 02:02:29 +00:00
""" Returns the Home Assistant config. """
2015-05-02 01:24:32 +00:00
handler.write_json(handler.server.hass.config.as_dict())
2015-05-02 02:02:29 +00:00
def _handle_get_api_bootstrap(handler, path_match, data):
""" Returns all data needed to bootstrap Home Assistant. """
hass = handler.server.hass
handler.write_json({
'config': hass.config.as_dict(),
'states': hass.states.all(),
'events': _events_json(hass),
'services': _services_json(hass),
})
def _handle_get_api_states(handler, path_match, data):
""" Returns a dict containing all entity ids and their state. """
2015-01-30 16:26:06 +00:00
handler.write_json(handler.server.hass.states.all())
def _handle_get_api_states_entity(handler, path_match, data):
""" Returns the state of a specific entity. """
entity_id = path_match.group('entity_id')
state = handler.server.hass.states.get(entity_id)
if state:
2015-01-30 16:26:06 +00:00
handler.write_json(state)
else:
2015-01-30 16:26:06 +00:00
handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
def _handle_post_state_entity(handler, path_match, data):
""" Handles updating the state of an entity.
This handles the following paths:
/api/states/<entity_id>
"""
entity_id = path_match.group('entity_id')
try:
new_state = data['state']
except KeyError:
2015-01-30 16:26:06 +00:00
handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
return
attributes = data['attributes'] if 'attributes' in data else None
is_new_state = handler.server.hass.states.get(entity_id) is None
# Write state
handler.server.hass.states.set(entity_id, new_state, attributes)
state = handler.server.hass.states.get(entity_id)
status_code = HTTP_CREATED if is_new_state else HTTP_OK
2015-01-30 16:26:06 +00:00
handler.write_json(
state.as_dict(),
status_code=status_code,
location=URL_API_STATES_ENTITY.format(entity_id))
def _handle_get_api_events(handler, path_match, data):
""" Handles getting overview of event listeners. """
2015-05-02 02:02:29 +00:00
handler.write_json(_events_json(handler.server.hass))
def _handle_api_post_events_event(handler, path_match, event_data):
""" Handles firing of an event.
This handles the following paths:
/api/events/<event_type>
Events from /api are threated as remote events.
"""
event_type = path_match.group('event_type')
if event_data is not None and not isinstance(event_data, dict):
2015-01-30 16:26:06 +00:00
handler.write_json_message(
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
event_origin = ha.EventOrigin.remote
# 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
handler.server.hass.bus.fire(event_type, event_data, event_origin)
2015-01-30 16:26:06 +00:00
handler.write_json_message("Event {} fired.".format(event_type))
def _handle_get_api_services(handler, path_match, data):
""" Handles getting overview of services. """
2015-05-02 02:02:29 +00:00
handler.write_json(_services_json(handler.server.hass))
# pylint: disable=invalid-name
def _handle_post_api_services_domain_service(handler, path_match, data):
""" Handles calling a service.
This handles the following paths:
/api/services/<domain>/<service>
"""
domain = path_match.group('domain')
service = path_match.group('service')
with TrackStates(handler.server.hass) as changed_states:
handler.server.hass.services.call(domain, service, data, True)
2015-01-30 16:26:06 +00:00
handler.write_json(changed_states)
# pylint: disable=invalid-name
def _handle_post_api_event_forward(handler, path_match, data):
""" Handles adding an event forwarding target. """
try:
host = data['host']
api_password = data['api_password']
except KeyError:
2015-01-30 16:26:06 +00:00
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:
2015-01-30 16:26:06 +00:00
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():
2015-01-30 16:26:06 +00:00
handler.write_json_message(
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
return
if handler.server.event_forwarder is None:
handler.server.event_forwarder = \
rem.EventForwarder(handler.server.hass)
handler.server.event_forwarder.connect(api)
2015-01-30 16:26:06 +00:00
handler.write_json_message("Event forwarding setup.")
def _handle_delete_api_event_forward(handler, path_match, data):
""" Handles deleting an event forwarding target. """
try:
host = data['host']
except KeyError:
2015-01-30 16:26:06 +00:00
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
return
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
2015-01-30 16:26:06 +00:00
handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
if handler.server.event_forwarder is not None:
api = rem.API(host, None, port)
handler.server.event_forwarder.disconnect(api)
2015-01-30 16:26:06 +00:00
handler.write_json_message("Event forwarding cancelled.")
2015-02-01 03:08:50 +00:00
def _handle_get_api_components(handler, path_match, data):
""" Returns all the loaded components. """
handler.write_json(handler.server.hass.config.components)
2015-05-02 02:02:29 +00:00
2015-05-02 03:56:10 +00:00
2015-11-07 09:44:02 +00:00
def _handle_get_api_error_log(handler, path_match, data):
""" Returns the logged errors for this session. """
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
False)
2015-11-07 09:44:02 +00:00
def _handle_post_api_log_out(handler, path_match, data):
""" Log user out. """
handler.send_response(HTTP_OK)
handler.destroy_session()
handler.end_headers()
2015-12-10 07:56:20 +00:00
def _handle_post_api_template(handler, path_match, data):
""" Log user out. """
template_string = data.get('template', '')
handler.send_response(HTTP_OK)
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
handler.end_headers()
handler.wfile.write(
template.render(handler.server.hass, template_string).encode('utf-8'))
2015-05-02 02:02:29 +00:00
def _services_json(hass):
""" Generate services data to JSONify. """
return [{"domain": key, "services": value}
for key, value in hass.services.services.items()]
2015-05-02 03:56:10 +00:00
2015-05-02 02:02:29 +00:00
def _events_json(hass):
""" Generate event data to JSONify. """
return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()]