Home Assistant Unofficial Reference 2024.12.1
config_entries.py
Go to the documentation of this file.
1 """Http views to control the config manager."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from http import HTTPStatus
7 from typing import Any, NoReturn
8 
9 from aiohttp import web
10 import aiohttp.web_exceptions
11 import voluptuous as vol
12 
13 from homeassistant import config_entries, data_entry_flow
14 from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT
15 from homeassistant.components import websocket_api
16 from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
17 from homeassistant.components.http.data_validator import RequestDataValidator
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.exceptions import DependencyError, Unauthorized
22  FlowManagerIndexView,
23  FlowManagerResourceView,
24 )
25 from homeassistant.helpers.dispatcher import async_dispatcher_connect
26 from homeassistant.helpers.json import json_fragment
27 from homeassistant.loader import (
28  Integration,
29  IntegrationNotFound,
30  async_get_config_flows,
31  async_get_integrations,
32  async_get_loaded_integration,
33 )
34 
35 
36 @callback
37 def async_setup(hass: HomeAssistant) -> bool:
38  """Enable the Home Assistant views."""
39  hass.http.register_view(ConfigManagerEntryIndexView)
40  hass.http.register_view(ConfigManagerEntryResourceView)
41  hass.http.register_view(ConfigManagerEntryResourceReloadView)
42  hass.http.register_view(ConfigManagerFlowIndexView(hass.config_entries.flow))
43  hass.http.register_view(ConfigManagerFlowResourceView(hass.config_entries.flow))
44  hass.http.register_view(ConfigManagerAvailableFlowView)
45 
46  hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options))
47  hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options))
48 
49  websocket_api.async_register_command(hass, config_entries_get)
50  websocket_api.async_register_command(hass, config_entry_disable)
51  websocket_api.async_register_command(hass, config_entry_get_single)
52  websocket_api.async_register_command(hass, config_entry_update)
53  websocket_api.async_register_command(hass, config_entries_subscribe)
54  websocket_api.async_register_command(hass, config_entries_progress)
55  websocket_api.async_register_command(hass, ignore_config_flow)
56 
57  return True
58 
59 
60 class ConfigManagerEntryIndexView(HomeAssistantView):
61  """View to get available config entries."""
62 
63  url = "/api/config/config_entries/entry"
64  name = "api:config:config_entries:entry"
65 
66  async def get(self, request: web.Request) -> web.Response:
67  """List available config entries."""
68  hass = request.app[KEY_HASS]
69  domain = None
70  if "domain" in request.query:
71  domain = request.query["domain"]
72  type_filter = None
73  if "type" in request.query:
74  type_filter = [request.query["type"]]
76  hass, type_filter, domain
77  )
78  return self.json(fragments)
79 
80 
81 class ConfigManagerEntryResourceView(HomeAssistantView):
82  """View to interact with a config entry."""
83 
84  url = "/api/config/config_entries/entry/{entry_id}"
85  name = "api:config:config_entries:entry:resource"
86 
87  async def delete(self, request: web.Request, entry_id: str) -> web.Response:
88  """Delete a config entry."""
89  if not request["hass_user"].is_admin:
90  raise Unauthorized(config_entry_id=entry_id, permission="remove")
91 
92  hass = request.app[KEY_HASS]
93 
94  try:
95  result = await hass.config_entries.async_remove(entry_id)
97  return self.json_message("Invalid entry specified", HTTPStatus.NOT_FOUND)
98 
99  return self.json(result)
100 
101 
102 class ConfigManagerEntryResourceReloadView(HomeAssistantView):
103  """View to reload a config entry."""
104 
105  url = "/api/config/config_entries/entry/{entry_id}/reload"
106  name = "api:config:config_entries:entry:resource:reload"
107 
108  async def post(self, request: web.Request, entry_id: str) -> web.Response:
109  """Reload a config entry."""
110  if not request["hass_user"].is_admin:
111  raise Unauthorized(config_entry_id=entry_id, permission="remove")
112 
113  hass = request.app[KEY_HASS]
114  entry = hass.config_entries.async_get_entry(entry_id)
115  if not entry:
116  return self.json_message("Invalid entry specified", HTTPStatus.NOT_FOUND)
117  assert isinstance(entry, config_entries.ConfigEntry)
118 
119  try:
120  await hass.config_entries.async_reload(entry_id)
122  return self.json_message("Entry cannot be reloaded", HTTPStatus.FORBIDDEN)
123 
124  return self.json({"require_restart": not entry.state.recoverable})
125 
126 
128  result: data_entry_flow.FlowResult,
129  prepare_result_json: Callable[
131  ],
133  """Convert result to JSON."""
134  if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
135  return prepare_result_json(result)
136 
137  data = result.copy()
138  entry: config_entries.ConfigEntry = data["result"]
139  data["result"] = entry.as_json_fragment
140  data.pop("data")
141  data.pop("context")
142  return data
143 
144 
146  FlowManagerIndexView[config_entries.ConfigEntriesFlowManager]
147 ):
148  """View to create config flows."""
149 
150  url = "/api/config/config_entries/flow"
151  name = "api:config:config_entries:flow"
152 
153  async def get(self, request: web.Request) -> NoReturn:
154  """Not implemented."""
155  raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"])
156 
157  @require_admin( error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
158  )
159  @RequestDataValidator( vol.Schema( { vol.Required("handler"): vol.Any(str, list),
160  vol.Optional("show_advanced_options", default=False): cv.boolean,
161  vol.Optional("entry_id"): cv.string,
162  },
163  extra=vol.ALLOW_EXTRA,
164  )
165  )
166  async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
167  """Initialize a POST request for a config entry flow."""
168  return await self._post_impl_post_impl_post_impl(request, data)
169 
170  async def _post_impl(
171  self, request: web.Request, data: dict[str, Any]
172  ) -> web.Response:
173  """Handle a POST request for a config entry flow."""
174  try:
175  return await super()._post_impl(request, data)
176  except DependencyError as exc:
177  return web.Response(
178  text=f"Failed dependencies {', '.join(exc.failed_dependencies)}",
179  status=HTTPStatus.BAD_REQUEST,
180  )
181 
182  def get_context(self, data: dict[str, Any]) -> dict[str, Any]:
183  """Return context."""
184  context = super().get_context(data)
185  context["source"] = config_entries.SOURCE_USER
186  if entry_id := data.get("entry_id"):
187  context["source"] = config_entries.SOURCE_RECONFIGURE
188  context["entry_id"] = entry_id
189  return context
190 
192  self, result: data_entry_flow.FlowResult
194  """Convert result to JSON."""
195  return _prepare_config_flow_result_json(result, super()._prepare_result_json)
196 
197 
199  FlowManagerResourceView[config_entries.ConfigEntriesFlowManager]
200 ):
201  """View to interact with the flow manager."""
202 
203  url = "/api/config/config_entries/flow/{flow_id}"
204  name = "api:config:config_entries:flow:resource"
205 
206  @require_admin( error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
207  )
208  async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
209  """Get the current state of a data_entry_flow."""
210  return await super().get(request, flow_id)
211 
212  @require_admin( error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
213  )
214  async def post(self, request: web.Request, flow_id: str) -> web.Response:
215  """Handle a POST request."""
216  return await super().post(request, flow_id)
217 
219  self, result: data_entry_flow.FlowResult
221  """Convert result to JSON."""
222  return _prepare_config_flow_result_json(result, super()._prepare_result_json)
223 
224 
225 class ConfigManagerAvailableFlowView(HomeAssistantView):
226  """View to query available flows."""
227 
228  url = "/api/config/config_entries/flow_handlers"
229  name = "api:config:config_entries:flow_handlers"
230 
231  async def get(self, request: web.Request) -> web.Response:
232  """List available flow handlers."""
233  hass = request.app[KEY_HASS]
234  kwargs: dict[str, Any] = {}
235  if "type" in request.query:
236  kwargs["type_filter"] = request.query["type"]
237  return self.json(await async_get_config_flows(hass, **kwargs))
238 
239 
241  FlowManagerIndexView[config_entries.OptionsFlowManager]
242 ):
243  """View to create option flows."""
244 
245  url = "/api/config/config_entries/options/flow"
246  name = "api:config:config_entries:option:flow"
247 
248  @require_admin( error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
249  )
250  async def post(self, request: web.Request) -> web.Response:
251  """Handle a POST request.
252 
253  handler in request is entry_id.
254  """
255  return await super().post(request)
256 
257 
259  FlowManagerResourceView[config_entries.OptionsFlowManager]
260 ):
261  """View to interact with the option flow manager."""
262 
263  url = "/api/config/config_entries/options/flow/{flow_id}"
264  name = "api:config:config_entries:options:flow:resource"
265 
266  @require_admin( error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
267  )
268  async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
269  """Get the current state of a data_entry_flow."""
270  return await super().get(request, flow_id)
271 
272  @require_admin( error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
273  )
274  async def post(self, request: web.Request, flow_id: str) -> web.Response:
275  """Handle a POST request."""
276  return await super().post(request, flow_id)
277 
278 
279 @websocket_api.require_admin
280 @websocket_api.websocket_command({"type": "config_entries/flow/progress"})
282  hass: HomeAssistant,
283  connection: websocket_api.ActiveConnection,
284  msg: dict[str, Any],
285 ) -> None:
286  """List flows that are in progress but not started by a user.
287 
288  Example of a non-user initiated flow is a discovered Hue hub that
289  requires user interaction to finish setup.
290  """
291  connection.send_result(
292  msg["id"],
293  [
294  flw
295  for flw in hass.config_entries.flow.async_progress()
296  if flw["context"]["source"] != config_entries.SOURCE_USER
297  ],
298  )
299 
300 
302  connection: websocket_api.ActiveConnection, msg_id: int
303 ) -> None:
304  """Send Config entry not found error."""
305  connection.send_error(msg_id, websocket_api.ERR_NOT_FOUND, "Config entry not found")
306 
307 
308 def get_entry(
309  hass: HomeAssistant,
311  entry_id: str,
312  msg_id: int,
313 ) -> config_entries.ConfigEntry | None:
314  """Get entry, send error message if it doesn't exist."""
315  if (entry := hass.config_entries.async_get_entry(entry_id)) is None:
316  send_entry_not_found(connection, msg_id)
317  return entry
318 
319 
320 @websocket_api.require_admin
321 @websocket_api.websocket_command( { "type": "config_entries/get_single", "entry_id": str, } )
322 @websocket_api.async_response
323 async def config_entry_get_single(
324  hass: HomeAssistant,
325  connection: websocket_api.ActiveConnection,
326  msg: dict[str, Any],
327 ) -> None:
328  """Update config entry."""
329  entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
330  if entry is None:
331  return
332 
333  result = {"config_entry": entry.as_json_fragment}
334  connection.send_result(msg["id"], result)
335 
336 
337 @websocket_api.require_admin
338 @websocket_api.websocket_command( { "type": "config_entries/update", "entry_id": str, vol.Optional("title"): str,
339  vol.Optional("pref_disable_new_entities"): bool,
340  vol.Optional("pref_disable_polling"): bool,
341  }
342 )
343 @websocket_api.async_response
344 async def config_entry_update(
345  hass: HomeAssistant,
346  connection: websocket_api.ActiveConnection,
347  msg: dict[str, Any],
348 ) -> None:
349  """Update config entry."""
350  changes = dict(msg)
351  changes.pop("id")
352  changes.pop("type")
353  changes.pop("entry_id")
354 
355  entry = get_entry(hass, connection, msg["entry_id"], msg["id"])
356  if entry is None:
357  return
358 
359  old_disable_polling = entry.pref_disable_polling
360 
361  hass.config_entries.async_update_entry(entry, **changes)
362 
363  result = {
364  "config_entry": entry.as_json_fragment,
365  "require_restart": False,
366  }
367 
368  initial_state = entry.state
369  if (
370  old_disable_polling != entry.pref_disable_polling
371  and initial_state is config_entries.ConfigEntryState.LOADED
372  ):
373  if not await hass.config_entries.async_reload(entry.entry_id):
374  result["require_restart"] = (
375  entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD
376  )
377 
378  connection.send_result(msg["id"], result)
379 
380 
381 @websocket_api.require_admin
382 @websocket_api.websocket_command( { "type": "config_entries/disable", "entry_id": str, # We only allow setting disabled_by user via API. # No Enum support like this in voluptuous, use .value "disabled_by": vol.Any(config_entries.ConfigEntryDisabler.USER.value, None),
383  }
384 )
385 @websocket_api.async_response
386 async def config_entry_disable(
387  hass: HomeAssistant,
388  connection: websocket_api.ActiveConnection,
389  msg: dict[str, Any],
390 ) -> None:
391  """Disable config entry."""
392  if (disabled_by := msg["disabled_by"]) is not None:
393  disabled_by = config_entries.ConfigEntryDisabler(disabled_by)
394 
395  success = False
396  try:
397  success = await hass.config_entries.async_set_disabled_by(
398  msg["entry_id"], disabled_by
399  )
401  # Failed to unload the config entry
402  pass
404  send_entry_not_found(connection, msg["id"])
405  return
406 
407  result = {"require_restart": not success}
408 
409  connection.send_result(msg["id"], result)
410 
411 
412 @websocket_api.require_admin
413 @websocket_api.websocket_command( {"type": "config_entries/ignore_flow", "flow_id": str, "title": str} )
414 @websocket_api.async_response
415 async def ignore_config_flow(
416  hass: HomeAssistant,
417  connection: websocket_api.ActiveConnection,
418  msg: dict[str, Any],
419 ) -> None:
420  """Ignore a config flow."""
421  flow = next(
422  (
423  flw
424  for flw in hass.config_entries.flow.async_progress()
425  if flw["flow_id"] == msg["flow_id"]
426  ),
427  None,
428  )
429 
430  if flow is None:
431  send_entry_not_found(connection, msg["id"])
432  return
433 
434  if "unique_id" not in flow["context"]:
435  connection.send_error(
436  msg["id"], "no_unique_id", "Specified flow has no unique ID."
437  )
438  return
439 
440  context = config_entries.ConfigFlowContext(source=config_entries.SOURCE_IGNORE)
441  if "discovery_key" in flow["context"]:
442  context["discovery_key"] = flow["context"]["discovery_key"]
443  await hass.config_entries.flow.async_init(
444  flow["handler"],
445  context=context,
446  data={"unique_id": flow["context"]["unique_id"], "title": msg["title"]},
447  )
448  connection.send_result(msg["id"])
449 
450 
451 @websocket_api.websocket_command( { vol.Required("type"): "config_entries/get",
452  vol.Optional("type_filter"): vol.All(cv.ensure_list, [str]),
453  vol.Optional("domain"): str,
454  }
455 )
456 @websocket_api.async_response
457 async def config_entries_get(
458  hass: HomeAssistant,
459  connection: websocket_api.ActiveConnection,
460  msg: dict[str, Any],
461 ) -> None:
462  """Return matching config entries by type and/or domain."""
464  hass, msg.get("type_filter"), msg.get("domain")
465  )
466  connection.send_result(msg["id"], fragments)
467 
468 
469 @websocket_api.websocket_command( { vol.Required("type"): "config_entries/subscribe",
470  vol.Optional("type_filter"): vol.All(cv.ensure_list, [str]),
471  }
472 )
473 @websocket_api.async_response
474 async def config_entries_subscribe(
475  hass: HomeAssistant,
476  connection: websocket_api.ActiveConnection,
477  msg: dict[str, Any],
478 ) -> None:
479  """Subscribe to config entry updates."""
480  type_filter = msg.get("type_filter")
481 
482  @callback
483  def async_forward_config_entry_changes(
484  change: config_entries.ConfigEntryChange, entry: config_entries.ConfigEntry
485  ) -> None:
486  """Forward config entry state events to websocket."""
487  if type_filter:
488  integration = async_get_loaded_integration(hass, entry.domain)
489  if integration.integration_type not in type_filter:
490  return
491 
492  connection.send_message(
493  websocket_api.event_message(
494  msg["id"],
495  [
496  {
497  "type": change,
498  "entry": entry.as_json_fragment,
499  }
500  ],
501  )
502  )
503 
505  hass, type_filter, None
506  )
507  connection.subscriptions[msg["id"]] = async_dispatcher_connect(
508  hass,
509  config_entries.SIGNAL_CONFIG_ENTRY_CHANGED,
510  async_forward_config_entry_changes,
511  )
512  connection.send_result(msg["id"])
513  connection.send_message(
514  websocket_api.event_message(
515  msg["id"], [{"type": None, "entry": entry} for entry in current_entries]
516  )
517  )
518 
519 
521  hass: HomeAssistant, type_filter: list[str] | None, domain: str | None
522 ) -> list[json_fragment]:
523  """Return matching config entries by type and/or domain."""
524  if domain:
525  entries = hass.config_entries.async_entries(domain)
526  else:
527  entries = hass.config_entries.async_entries()
528 
529  if not type_filter:
530  return [entry.as_json_fragment for entry in entries]
531 
532  integrations: dict[str, Integration] = {}
533  # Fetch all the integrations so we can check their type
534  domains = {entry.domain for entry in entries}
535  for domain_key, integration_or_exc in (
536  await async_get_integrations(hass, domains)
537  ).items():
538  if isinstance(integration_or_exc, Integration):
539  integrations[domain_key] = integration_or_exc
540  elif not isinstance(integration_or_exc, IntegrationNotFound):
541  raise integration_or_exc
542 
543  # Filter out entries that don't match the type filter
544  # when only helpers are requested, also filter out entries
545  # from unknown integrations. This prevent them from showing
546  # up in the helpers UI.
547  filter_is_not_helper = type_filter != ["helper"]
548  filter_set = set(type_filter)
549  return [
550  entry.as_json_fragment
551  for entry in entries
552  # If the filter is not 'helper', we still include the integration
553  # even if its not returned from async_get_integrations for backwards
554  # compatibility.
555  if (
556  (integration := integrations.get(entry.domain))
557  and integration.integration_type in filter_set
558  )
559  or (filter_is_not_helper and entry.domain not in integrations)
560  ]
561 
web.Response delete(self, web.Request request, str entry_id)
data_entry_flow.FlowResult _prepare_result_json(self, data_entry_flow.FlowResult result)
web.Response _post_impl(self, web.Request request, dict[str, Any] data)
web.Response post(self, web.Request request, dict[str, Any] data)
data_entry_flow.FlowResult _prepare_result_json(self, data_entry_flow.FlowResult result)
web.Response get(self, web.Request request, str flow_id)
web.Response post(self, web.Request request, str flow_id)
web.Response get(self, web.Request request, str flow_id)
web.Response post(self, web.Request request, str flow_id)
web.Response _post_impl(self, web.Request request, dict[str, Any] data)
data_entry_flow.FlowResult _prepare_config_flow_result_json(data_entry_flow.FlowResult result, Callable[[data_entry_flow.FlowResult], data_entry_flow.FlowResult] prepare_result_json)
None config_entry_disable(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
None config_entries_subscribe(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
None config_entries_progress(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
None config_entry_get_single(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
list[json_fragment] _async_matching_config_entries_json_fragments(HomeAssistant hass, list[str]|None type_filter, str|None domain)
None ignore_config_flow(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
None send_entry_not_found(websocket_api.ActiveConnection connection, int msg_id)
None config_entries_get(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
config_entries.ConfigEntry|None get_entry(HomeAssistant hass, websocket_api.ActiveConnection connection, str entry_id, int msg_id)
None config_entry_update(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
set[str] async_get_config_flows(HomeAssistant hass, Literal["device", "helper", "hub", "service"]|None type_filter=None)
Definition: loader.py:339
Integration async_get_loaded_integration(HomeAssistant hass, str domain)
Definition: loader.py:1341
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
Definition: loader.py:1368