PageRenderTime 103ms CodeModel.GetById 15ms RepoModel.GetById 3ms app.codeStats 1ms

/homeassistant/components/api.py

https://gitlab.com/mkerfoot/home-assistant
Python | 399 lines | 247 code | 93 blank | 59 comment | 48 complexity | b21226f7a335f82250fff8abcaf642b8 MD5 | raw file
  1. """
  2. Rest API for Home Assistant.
  3. For more details about the RESTful API, please refer to the documentation at
  4. https://home-assistant.io/developers/api/
  5. """
  6. import json
  7. import logging
  8. import re
  9. import threading
  10. import homeassistant.core as ha
  11. import homeassistant.remote as rem
  12. from homeassistant.bootstrap import ERROR_LOG_FILENAME
  13. from homeassistant.const import (
  14. CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
  15. HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
  16. HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
  17. URL_API_CONFIG, URL_API_ERROR_LOG, URL_API_EVENT_FORWARD, URL_API_EVENTS,
  18. URL_API_LOG_OUT, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY,
  19. URL_API_STREAM, URL_API_TEMPLATE)
  20. from homeassistant.exceptions import TemplateError
  21. from homeassistant.helpers.state import TrackStates
  22. from homeassistant.helpers import template
  23. DOMAIN = 'api'
  24. DEPENDENCIES = ['http']
  25. STREAM_PING_PAYLOAD = "ping"
  26. STREAM_PING_INTERVAL = 50 # seconds
  27. _LOGGER = logging.getLogger(__name__)
  28. def setup(hass, config):
  29. """Register the API with the HTTP interface."""
  30. # /api - for validation purposes
  31. hass.http.register_path('GET', URL_API, _handle_get_api)
  32. # /api/stream
  33. hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
  34. # /api/config
  35. hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
  36. # /states
  37. hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
  38. hass.http.register_path(
  39. 'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
  40. _handle_get_api_states_entity)
  41. hass.http.register_path(
  42. 'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
  43. _handle_post_state_entity)
  44. hass.http.register_path(
  45. 'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
  46. _handle_post_state_entity)
  47. hass.http.register_path(
  48. 'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
  49. _handle_delete_state_entity)
  50. # /events
  51. hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
  52. hass.http.register_path(
  53. 'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
  54. _handle_api_post_events_event)
  55. # /services
  56. hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
  57. hass.http.register_path(
  58. 'POST',
  59. re.compile((r'/api/services/'
  60. r'(?P<domain>[a-zA-Z\._0-9]+)/'
  61. r'(?P<service>[a-zA-Z\._0-9]+)')),
  62. _handle_post_api_services_domain_service)
  63. # /event_forwarding
  64. hass.http.register_path(
  65. 'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
  66. hass.http.register_path(
  67. 'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
  68. # /components
  69. hass.http.register_path(
  70. 'GET', URL_API_COMPONENTS, _handle_get_api_components)
  71. hass.http.register_path('GET', URL_API_ERROR_LOG,
  72. _handle_get_api_error_log)
  73. hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
  74. hass.http.register_path('POST', URL_API_TEMPLATE,
  75. _handle_post_api_template)
  76. return True
  77. def _handle_get_api(handler, path_match, data):
  78. """Renders the debug interface."""
  79. handler.write_json_message("API running.")
  80. def _handle_get_api_stream(handler, path_match, data):
  81. """Provide a streaming interface for the event bus."""
  82. gracefully_closed = False
  83. hass = handler.server.hass
  84. wfile = handler.wfile
  85. write_lock = threading.Lock()
  86. block = threading.Event()
  87. session_id = None
  88. restrict = data.get('restrict')
  89. if restrict:
  90. restrict = restrict.split(',')
  91. def write_message(payload):
  92. """Writes a message to the output."""
  93. with write_lock:
  94. msg = "data: {}\n\n".format(payload)
  95. try:
  96. wfile.write(msg.encode("UTF-8"))
  97. wfile.flush()
  98. except (IOError, ValueError):
  99. # IOError: socket errors
  100. # ValueError: raised when 'I/O operation on closed file'
  101. block.set()
  102. def forward_events(event):
  103. """Forwards events to the open request."""
  104. nonlocal gracefully_closed
  105. if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
  106. return
  107. elif event.event_type == EVENT_HOMEASSISTANT_STOP:
  108. gracefully_closed = True
  109. block.set()
  110. return
  111. handler.server.sessions.extend_validation(session_id)
  112. write_message(json.dumps(event, cls=rem.JSONEncoder))
  113. handler.send_response(HTTP_OK)
  114. handler.send_header('Content-type', 'text/event-stream')
  115. session_id = handler.set_session_cookie_header()
  116. handler.end_headers()
  117. if restrict:
  118. for event in restrict:
  119. hass.bus.listen(event, forward_events)
  120. else:
  121. hass.bus.listen(MATCH_ALL, forward_events)
  122. while True:
  123. write_message(STREAM_PING_PAYLOAD)
  124. block.wait(STREAM_PING_INTERVAL)
  125. if block.is_set():
  126. break
  127. if not gracefully_closed:
  128. _LOGGER.info("Found broken event stream to %s, cleaning up",
  129. handler.client_address[0])
  130. if restrict:
  131. for event in restrict:
  132. hass.bus.remove_listener(event, forward_events)
  133. else:
  134. hass.bus.remove_listener(MATCH_ALL, forward_events)
  135. def _handle_get_api_config(handler, path_match, data):
  136. """Returns the Home Assistant configuration."""
  137. handler.write_json(handler.server.hass.config.as_dict())
  138. def _handle_get_api_states(handler, path_match, data):
  139. """Returns a dict containing all entity ids and their state."""
  140. handler.write_json(handler.server.hass.states.all())
  141. def _handle_get_api_states_entity(handler, path_match, data):
  142. """Returns the state of a specific entity."""
  143. entity_id = path_match.group('entity_id')
  144. state = handler.server.hass.states.get(entity_id)
  145. if state:
  146. handler.write_json(state)
  147. else:
  148. handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
  149. def _handle_post_state_entity(handler, path_match, data):
  150. """Handles updating the state of an entity.
  151. This handles the following paths:
  152. /api/states/<entity_id>
  153. """
  154. entity_id = path_match.group('entity_id')
  155. try:
  156. new_state = data['state']
  157. except KeyError:
  158. handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
  159. return
  160. attributes = data['attributes'] if 'attributes' in data else None
  161. is_new_state = handler.server.hass.states.get(entity_id) is None
  162. # Write state
  163. handler.server.hass.states.set(entity_id, new_state, attributes)
  164. state = handler.server.hass.states.get(entity_id)
  165. status_code = HTTP_CREATED if is_new_state else HTTP_OK
  166. handler.write_json(
  167. state.as_dict(),
  168. status_code=status_code,
  169. location=URL_API_STATES_ENTITY.format(entity_id))
  170. def _handle_delete_state_entity(handler, path_match, data):
  171. """Handle request to delete an entity from state machine.
  172. This handles the following paths:
  173. /api/states/<entity_id>
  174. """
  175. entity_id = path_match.group('entity_id')
  176. if handler.server.hass.states.remove(entity_id):
  177. handler.write_json_message(
  178. "Entity not found", HTTP_NOT_FOUND)
  179. else:
  180. handler.write_json_message(
  181. "Entity removed", HTTP_OK)
  182. def _handle_get_api_events(handler, path_match, data):
  183. """Handles getting overview of event listeners."""
  184. handler.write_json(events_json(handler.server.hass))
  185. def _handle_api_post_events_event(handler, path_match, event_data):
  186. """Handles firing of an event.
  187. This handles the following paths:
  188. /api/events/<event_type>
  189. Events from /api are threated as remote events.
  190. """
  191. event_type = path_match.group('event_type')
  192. if event_data is not None and not isinstance(event_data, dict):
  193. handler.write_json_message(
  194. "event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
  195. return
  196. event_origin = ha.EventOrigin.remote
  197. # Special case handling for event STATE_CHANGED
  198. # We will try to convert state dicts back to State objects
  199. if event_type == ha.EVENT_STATE_CHANGED and event_data:
  200. for key in ('old_state', 'new_state'):
  201. state = ha.State.from_dict(event_data.get(key))
  202. if state:
  203. event_data[key] = state
  204. handler.server.hass.bus.fire(event_type, event_data, event_origin)
  205. handler.write_json_message("Event {} fired.".format(event_type))
  206. def _handle_get_api_services(handler, path_match, data):
  207. """Handles getting overview of services."""
  208. handler.write_json(services_json(handler.server.hass))
  209. # pylint: disable=invalid-name
  210. def _handle_post_api_services_domain_service(handler, path_match, data):
  211. """Handles calling a service.
  212. This handles the following paths:
  213. /api/services/<domain>/<service>
  214. """
  215. domain = path_match.group('domain')
  216. service = path_match.group('service')
  217. with TrackStates(handler.server.hass) as changed_states:
  218. handler.server.hass.services.call(domain, service, data, True)
  219. handler.write_json(changed_states)
  220. # pylint: disable=invalid-name
  221. def _handle_post_api_event_forward(handler, path_match, data):
  222. """Handles adding an event forwarding target."""
  223. try:
  224. host = data['host']
  225. api_password = data['api_password']
  226. except KeyError:
  227. handler.write_json_message(
  228. "No host or api_password received.", HTTP_BAD_REQUEST)
  229. return
  230. try:
  231. port = int(data['port']) if 'port' in data else None
  232. except ValueError:
  233. handler.write_json_message(
  234. "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
  235. return
  236. api = rem.API(host, api_password, port)
  237. if not api.validate_api():
  238. handler.write_json_message(
  239. "Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
  240. return
  241. if handler.server.event_forwarder is None:
  242. handler.server.event_forwarder = \
  243. rem.EventForwarder(handler.server.hass)
  244. handler.server.event_forwarder.connect(api)
  245. handler.write_json_message("Event forwarding setup.")
  246. def _handle_delete_api_event_forward(handler, path_match, data):
  247. """Handles deleting an event forwarding target."""
  248. try:
  249. host = data['host']
  250. except KeyError:
  251. handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
  252. return
  253. try:
  254. port = int(data['port']) if 'port' in data else None
  255. except ValueError:
  256. handler.write_json_message(
  257. "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
  258. return
  259. if handler.server.event_forwarder is not None:
  260. api = rem.API(host, None, port)
  261. handler.server.event_forwarder.disconnect(api)
  262. handler.write_json_message("Event forwarding cancelled.")
  263. def _handle_get_api_components(handler, path_match, data):
  264. """Returns all the loaded components."""
  265. handler.write_json(handler.server.hass.config.components)
  266. def _handle_get_api_error_log(handler, path_match, data):
  267. """Returns the logged errors for this session."""
  268. handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
  269. False)
  270. def _handle_post_api_log_out(handler, path_match, data):
  271. """Log user out."""
  272. handler.send_response(HTTP_OK)
  273. handler.destroy_session()
  274. handler.end_headers()
  275. def _handle_post_api_template(handler, path_match, data):
  276. """Log user out."""
  277. template_string = data.get('template', '')
  278. try:
  279. rendered = template.render(handler.server.hass, template_string)
  280. handler.send_response(HTTP_OK)
  281. handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
  282. handler.end_headers()
  283. handler.wfile.write(rendered.encode('utf-8'))
  284. except TemplateError as e:
  285. handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
  286. return
  287. def services_json(hass):
  288. """Generate services data to JSONify."""
  289. return [{"domain": key, "services": value}
  290. for key, value in hass.services.services.items()]
  291. def events_json(hass):
  292. """Generate event data to JSONify."""
  293. return [{"event": key, "listener_count": value}
  294. for key, value in hass.bus.listeners.items()]