Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Rest API for Home Assistant."""
2 
3 import asyncio
4 from asyncio import shield, timeout
5 from functools import lru_cache
6 from http import HTTPStatus
7 import logging
8 from typing import Any
9 
10 from aiohttp import web
11 from aiohttp.web_exceptions import HTTPBadRequest
12 import voluptuous as vol
13 
14 from homeassistant.auth.models import User
15 from homeassistant.auth.permissions.const import POLICY_READ
17  KEY_HASS,
18  KEY_HASS_USER,
19  HomeAssistantView,
20  require_admin,
21 )
22 from homeassistant.const import (
23  CONTENT_TYPE_JSON,
24  EVENT_HOMEASSISTANT_STOP,
25  EVENT_STATE_CHANGED,
26  KEY_DATA_LOGGING as DATA_LOGGING,
27  MATCH_ALL,
28  URL_API,
29  URL_API_COMPONENTS,
30  URL_API_CONFIG,
31  URL_API_CORE_STATE,
32  URL_API_ERROR_LOG,
33  URL_API_EVENTS,
34  URL_API_SERVICES,
35  URL_API_STATES,
36  URL_API_STREAM,
37  URL_API_TEMPLATE,
38 )
39 import homeassistant.core as ha
40 from homeassistant.core import Event, EventStateChangedData, HomeAssistant
41 from homeassistant.exceptions import (
42  InvalidEntityFormatError,
43  InvalidStateError,
44  ServiceNotFound,
45  TemplateError,
46  Unauthorized,
47 )
48 from homeassistant.helpers import config_validation as cv, recorder, template
49 from homeassistant.helpers.json import json_dumps, json_fragment
50 from homeassistant.helpers.service import async_get_all_descriptions
51 from homeassistant.helpers.typing import ConfigType
52 from homeassistant.util.event_type import EventType
53 from homeassistant.util.json import json_loads
54 
55 _LOGGER = logging.getLogger(__name__)
56 
57 ATTR_BASE_URL = "base_url"
58 ATTR_EXTERNAL_URL = "external_url"
59 ATTR_INTERNAL_URL = "internal_url"
60 ATTR_LOCATION_NAME = "location_name"
61 ATTR_INSTALLATION_TYPE = "installation_type"
62 ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
63 ATTR_UUID = "uuid"
64 ATTR_VERSION = "version"
65 
66 DOMAIN = "api"
67 STREAM_PING_PAYLOAD = "ping"
68 STREAM_PING_INTERVAL = 50 # seconds
69 SERVICE_WAIT_TIMEOUT = 10
70 
71 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
72 
73 
74 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
75  """Register the API with the HTTP interface."""
76  hass.http.register_view(APIStatusView)
77  hass.http.register_view(APICoreStateView)
78  hass.http.register_view(APIEventStream)
79  hass.http.register_view(APIConfigView)
80  hass.http.register_view(APIStatesView)
81  hass.http.register_view(APIEntityStateView)
82  hass.http.register_view(APIEventListenersView)
83  hass.http.register_view(APIEventView)
84  hass.http.register_view(APIServicesView)
85  hass.http.register_view(APIDomainServicesView)
86  hass.http.register_view(APIComponentsView)
87  hass.http.register_view(APITemplateView)
88 
89  if DATA_LOGGING in hass.data:
90  hass.http.register_view(APIErrorLog)
91 
92  return True
93 
94 
95 class APIStatusView(HomeAssistantView):
96  """View to handle Status requests."""
97 
98  url = URL_API
99  name = "api:status"
100 
101  @ha.callback
102  def get(self, request: web.Request) -> web.Response:
103  """Retrieve if API is running."""
104  return self.json_message("API running.")
105 
106 
107 class APICoreStateView(HomeAssistantView):
108  """View to handle core state requests."""
109 
110  url = URL_API_CORE_STATE
111  name = "api:core:state"
112 
113  @ha.callback
114  def get(self, request: web.Request) -> web.Response:
115  """Retrieve the current core state.
116 
117  This API is intended to be a fast and lightweight way to check if the
118  Home Assistant core is running. Its primary use case is for supervisor
119  to check if Home Assistant is running.
120  """
121  hass = request.app[KEY_HASS]
122  migration = recorder.async_migration_in_progress(hass)
123  live = recorder.async_migration_is_live(hass)
124  recorder_state = {"migration_in_progress": migration, "migration_is_live": live}
125  return self.json({"state": hass.state.value, "recorder_state": recorder_state})
126 
127 
128 class APIEventStream(HomeAssistantView):
129  """View to handle EventStream requests."""
130 
131  url = URL_API_STREAM
132  name = "api:stream"
133 
134  @require_admin
135  async def get(self, request: web.Request) -> web.StreamResponse:
136  """Provide a streaming interface for the event bus."""
137  hass = request.app[KEY_HASS]
138  stop_obj = object()
139  to_write: asyncio.Queue[object | str] = asyncio.Queue()
140 
141  restrict: list[EventType[Any] | str] | None = None
142  if restrict_str := request.query.get("restrict"):
143  restrict = [*restrict_str.split(","), EVENT_HOMEASSISTANT_STOP]
144 
145  async def forward_events(event: Event) -> None:
146  """Forward events to the open request."""
147  if restrict and event.event_type not in restrict:
148  return
149 
150  _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event)
151 
152  if event.event_type == EVENT_HOMEASSISTANT_STOP:
153  data = stop_obj
154  else:
155  data = json_dumps(event)
156 
157  await to_write.put(data)
158 
159  response = web.StreamResponse()
160  response.content_type = "text/event-stream"
161  await response.prepare(request)
162 
163  unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
164 
165  try:
166  _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj))
167 
168  # Fire off one message so browsers fire open event right away
169  await to_write.put(STREAM_PING_PAYLOAD)
170 
171  while True:
172  try:
173  async with timeout(STREAM_PING_INTERVAL):
174  payload = await to_write.get()
175 
176  if payload is stop_obj:
177  break
178 
179  msg = f"data: {payload}\n\n"
180  _LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip())
181  await response.write(msg.encode("UTF-8"))
182  except TimeoutError:
183  await to_write.put(STREAM_PING_PAYLOAD)
184 
185  except asyncio.CancelledError:
186  _LOGGER.debug("STREAM %s ABORT", id(stop_obj))
187 
188  finally:
189  _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
190  unsub_stream()
191 
192  return response
193 
194 
195 class APIConfigView(HomeAssistantView):
196  """View to handle Configuration requests."""
197 
198  url = URL_API_CONFIG
199  name = "api:config"
200 
201  @ha.callback
202  def get(self, request: web.Request) -> web.Response:
203  """Get current configuration."""
204  return self.json(request.app[KEY_HASS].config.as_dict())
205 
206 
207 class APIStatesView(HomeAssistantView):
208  """View to handle States requests."""
209 
210  url = URL_API_STATES
211  name = "api:states"
212 
213  @ha.callback
214  def get(self, request: web.Request) -> web.Response:
215  """Get current states."""
216  user: User = request[KEY_HASS_USER]
217  hass = request.app[KEY_HASS]
218  if user.is_admin:
219  states = (state.as_dict_json for state in hass.states.async_all())
220  else:
221  entity_perm = user.permissions.check_entity
222  states = (
223  state.as_dict_json
224  for state in hass.states.async_all()
225  if entity_perm(state.entity_id, "read")
226  )
227  response = web.Response(
228  body=b"".join((b"[", b",".join(states), b"]")),
229  content_type=CONTENT_TYPE_JSON,
230  zlib_executor_size=32768,
231  )
232  response.enable_compression()
233  return response
234 
235 
236 class APIEntityStateView(HomeAssistantView):
237  """View to handle EntityState requests."""
238 
239  url = "/api/states/{entity_id}"
240  name = "api:entity-state"
241 
242  @ha.callback
243  def get(self, request: web.Request, entity_id: str) -> web.Response:
244  """Retrieve state of entity."""
245  user: User = request[KEY_HASS_USER]
246  hass = request.app[KEY_HASS]
247  if not user.permissions.check_entity(entity_id, POLICY_READ):
248  raise Unauthorized(entity_id=entity_id)
249 
250  if state := hass.states.get(entity_id):
251  return web.Response(
252  body=state.as_dict_json,
253  content_type=CONTENT_TYPE_JSON,
254  )
255  return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND)
256 
257  async def post(self, request: web.Request, entity_id: str) -> web.Response:
258  """Update state of entity."""
259  user: User = request[KEY_HASS_USER]
260  if not user.is_admin:
261  raise Unauthorized(entity_id=entity_id)
262  hass = request.app[KEY_HASS]
263  try:
264  data = await request.json()
265  except ValueError:
266  return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST)
267 
268  if (new_state := data.get("state")) is None:
269  return self.json_message("No state specified.", HTTPStatus.BAD_REQUEST)
270 
271  attributes = data.get("attributes")
272  force_update = data.get("force_update", False)
273 
274  is_new_state = hass.states.get(entity_id) is None
275 
276  # Write state
277  try:
278  hass.states.async_set(
279  entity_id, new_state, attributes, force_update, self.context(request)
280  )
281  except InvalidEntityFormatError:
282  return self.json_message(
283  "Invalid entity ID specified.", HTTPStatus.BAD_REQUEST
284  )
285  except InvalidStateError:
286  return self.json_message("Invalid state specified.", HTTPStatus.BAD_REQUEST)
287 
288  # Read the state back for our response
289  status_code = HTTPStatus.CREATED if is_new_state else HTTPStatus.OK
290  state = hass.states.get(entity_id)
291  assert state
292  resp = self.json(state.as_dict(), status_code)
293 
294  resp.headers.add("Location", f"/api/states/{entity_id}")
295 
296  return resp
297 
298  @ha.callback
299  def delete(self, request: web.Request, entity_id: str) -> web.Response:
300  """Remove entity."""
301  if not request[KEY_HASS_USER].is_admin:
302  raise Unauthorized(entity_id=entity_id)
303  if request.app[KEY_HASS].states.async_remove(entity_id):
304  return self.json_message("Entity removed.")
305  return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND)
306 
307 
308 class APIEventListenersView(HomeAssistantView):
309  """View to handle EventListeners requests."""
310 
311  url = URL_API_EVENTS
312  name = "api:event-listeners"
313 
314  @ha.callback
315  def get(self, request: web.Request) -> web.Response:
316  """Get event listeners."""
317  return self.json(async_events_json(request.app[KEY_HASS]))
318 
319 
320 class APIEventView(HomeAssistantView):
321  """View to handle Event requests."""
322 
323  url = "/api/events/{event_type}"
324  name = "api:event"
325 
326  @require_admin
327  async def post(self, request: web.Request, event_type: str) -> web.Response:
328  """Fire events."""
329  body = await request.text()
330  try:
331  event_data: Any = json_loads(body) if body else None
332  except ValueError:
333  return self.json_message(
334  "Event data should be valid JSON.", HTTPStatus.BAD_REQUEST
335  )
336 
337  if event_data is not None and not isinstance(event_data, dict):
338  return self.json_message(
339  "Event data should be a JSON object", HTTPStatus.BAD_REQUEST
340  )
341 
342  # Special case handling for event STATE_CHANGED
343  # We will try to convert state dicts back to State objects
344  if event_type == EVENT_STATE_CHANGED and event_data:
345  for key in ("old_state", "new_state"):
346  state = ha.State.from_dict(event_data[key])
347 
348  if state:
349  event_data[key] = state
350 
351  request.app[KEY_HASS].bus.async_fire(
352  event_type, event_data, ha.EventOrigin.remote, self.context(request)
353  )
354 
355  return self.json_message(f"Event {event_type} fired.")
356 
357 
358 class APIServicesView(HomeAssistantView):
359  """View to handle Services requests."""
360 
361  url = URL_API_SERVICES
362  name = "api:services"
363 
364  async def get(self, request: web.Request) -> web.Response:
365  """Get registered services."""
366  services = await async_services_json(request.app[KEY_HASS])
367  return self.json(services)
368 
369 
370 class APIDomainServicesView(HomeAssistantView):
371  """View to handle DomainServices requests."""
372 
373  url = "/api/services/{domain}/{service}"
374  name = "api:domain-services"
375 
376  async def post(
377  self, request: web.Request, domain: str, service: str
378  ) -> web.Response:
379  """Call a service.
380 
381  Returns a list of changed states.
382  """
383  hass = request.app[KEY_HASS]
384  body = await request.text()
385  try:
386  data = json_loads(body) if body else None
387  except ValueError:
388  return self.json_message(
389  "Data should be valid JSON.", HTTPStatus.BAD_REQUEST
390  )
391 
392  context = self.context(request)
393  if not hass.services.has_service(domain, service):
394  raise HTTPBadRequest from ServiceNotFound(domain, service)
395 
396  if response_requested := "return_response" in request.query:
397  if (
398  hass.services.supports_response(domain, service)
399  is ha.SupportsResponse.NONE
400  ):
401  return self.json_message(
402  "Service does not support responses. Remove return_response from request.",
403  HTTPStatus.BAD_REQUEST,
404  )
405  elif (
406  hass.services.supports_response(domain, service) is ha.SupportsResponse.ONLY
407  ):
408  return self.json_message(
409  "Service call requires responses but caller did not ask for responses. "
410  "Add ?return_response to query parameters.",
411  HTTPStatus.BAD_REQUEST,
412  )
413 
414  changed_states: list[json_fragment] = []
415 
416  @ha.callback
417  def _async_save_changed_entities(
418  event: Event[EventStateChangedData],
419  ) -> None:
420  if event.context == context and (state := event.data["new_state"]):
421  changed_states.append(state.json_fragment)
422 
423  cancel_listen = hass.bus.async_listen(
424  EVENT_STATE_CHANGED,
425  _async_save_changed_entities,
426  )
427 
428  try:
429  # shield the service call from cancellation on connection drop
430  response = await shield(
431  hass.services.async_call(
432  domain,
433  service,
434  data, # type: ignore[arg-type]
435  blocking=True,
436  context=context,
437  return_response=response_requested,
438  )
439  )
440  except (vol.Invalid, ServiceNotFound) as ex:
441  raise HTTPBadRequest from ex
442  finally:
443  cancel_listen()
444 
445  if response_requested:
446  return self.json(
447  {"changed_states": changed_states, "service_response": response}
448  )
449 
450  return self.json(changed_states)
451 
452 
453 class APIComponentsView(HomeAssistantView):
454  """View to handle Components requests."""
455 
456  url = URL_API_COMPONENTS
457  name = "api:components"
458 
459  @ha.callback
460  def get(self, request: web.Request) -> web.Response:
461  """Get current loaded components."""
462  return self.json(request.app[KEY_HASS].config.components)
463 
464 
465 @lru_cache
466 def _cached_template(template_str: str, hass: HomeAssistant) -> template.Template:
467  """Return a cached template."""
468  return template.Template(template_str, hass)
469 
470 
471 class APITemplateView(HomeAssistantView):
472  """View to handle Template requests."""
473 
474  url = URL_API_TEMPLATE
475  name = "api:template"
476 
477  @require_admin
478  async def post(self, request: web.Request) -> web.Response:
479  """Render a template."""
480  try:
481  data = await request.json()
482  tpl = _cached_template(data["template"], request.app[KEY_HASS])
483  return tpl.async_render(variables=data.get("variables"), parse_result=False) # type: ignore[no-any-return]
484  except (ValueError, TemplateError) as ex:
485  return self.json_message(
486  f"Error rendering template: {ex}", HTTPStatus.BAD_REQUEST
487  )
488 
489 
490 class APIErrorLog(HomeAssistantView):
491  """View to fetch the API error log."""
492 
493  url = URL_API_ERROR_LOG
494  name = "api:error_log"
495 
496  @require_admin
497  async def get(self, request: web.Request) -> web.FileResponse:
498  """Retrieve API error log."""
499  hass = request.app[KEY_HASS]
500  response = web.FileResponse(hass.data[DATA_LOGGING])
501  response.enable_compression()
502  return response
503 
504 
505 async def async_services_json(hass: HomeAssistant) -> list[dict[str, Any]]:
506  """Generate services data to JSONify."""
507  descriptions = await async_get_all_descriptions(hass)
508  return [{"domain": key, "services": value} for key, value in descriptions.items()]
509 
510 
511 @ha.callback
512 def async_events_json(hass: HomeAssistant) -> list[dict[str, Any]]:
513  """Generate event data to JSONify."""
514  return [
515  {"event": key, "listener_count": value}
516  for key, value in hass.bus.async_listeners().items()
517  ]
web.Response get(self, web.Request request)
Definition: __init__.py:460
web.Response get(self, web.Request request)
Definition: __init__.py:202
web.Response get(self, web.Request request)
Definition: __init__.py:114
web.Response post(self, web.Request request, str domain, str service)
Definition: __init__.py:378
web.Response post(self, web.Request request, str entity_id)
Definition: __init__.py:257
web.Response get(self, web.Request request, str entity_id)
Definition: __init__.py:243
web.Response delete(self, web.Request request, str entity_id)
Definition: __init__.py:299
web.FileResponse get(self, web.Request request)
Definition: __init__.py:497
web.Response get(self, web.Request request)
Definition: __init__.py:315
web.StreamResponse get(self, web.Request request)
Definition: __init__.py:135
web.Response post(self, web.Request request, str event_type)
Definition: __init__.py:327
web.Response get(self, web.Request request)
Definition: __init__.py:364
web.Response get(self, web.Request request)
Definition: __init__.py:214
web.Response get(self, web.Request request)
Definition: __init__.py:102
web.Response post(self, web.Request request)
Definition: __init__.py:478
list[dict[str, Any]] async_events_json(HomeAssistant hass)
Definition: __init__.py:512
template.Template _cached_template(str template_str, HomeAssistant hass)
Definition: __init__.py:466
list[dict[str, Any]] async_services_json(HomeAssistant hass)
Definition: __init__.py:505
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:74
str json_dumps(Any data)
Definition: json.py:149
dict[str, dict[str, Any]] async_get_all_descriptions(HomeAssistant hass)
Definition: service.py:699