1 """Http views to control the config manager."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from http
import HTTPStatus
7 from typing
import Any, NoReturn
9 from aiohttp
import web
10 import aiohttp.web_exceptions
11 import voluptuous
as vol
13 from homeassistant
import config_entries, data_entry_flow
23 FlowManagerResourceView,
30 async_get_config_flows,
31 async_get_integrations,
32 async_get_loaded_integration,
38 """Enable the Home Assistant views."""
39 hass.http.register_view(ConfigManagerEntryIndexView)
40 hass.http.register_view(ConfigManagerEntryResourceView)
41 hass.http.register_view(ConfigManagerEntryResourceReloadView)
44 hass.http.register_view(ConfigManagerAvailableFlowView)
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)
61 """View to get available config entries."""
63 url =
"/api/config/config_entries/entry"
64 name =
"api:config:config_entries:entry"
66 async
def get(self, request: web.Request) -> web.Response:
67 """List available config entries."""
68 hass = request.app[KEY_HASS]
70 if "domain" in request.query:
71 domain = request.query[
"domain"]
73 if "type" in request.query:
74 type_filter = [request.query[
"type"]]
76 hass, type_filter, domain
78 return self.json(fragments)
82 """View to interact with a config entry."""
84 url =
"/api/config/config_entries/entry/{entry_id}"
85 name =
"api:config:config_entries:entry:resource"
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")
92 hass = request.app[KEY_HASS]
95 result = await hass.config_entries.async_remove(entry_id)
97 return self.json_message(
"Invalid entry specified", HTTPStatus.NOT_FOUND)
99 return self.json(result)
103 """View to reload a config entry."""
105 url =
"/api/config/config_entries/entry/{entry_id}/reload"
106 name =
"api:config:config_entries:entry:resource:reload"
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")
113 hass = request.app[KEY_HASS]
114 entry = hass.config_entries.async_get_entry(entry_id)
116 return self.json_message(
"Invalid entry specified", HTTPStatus.NOT_FOUND)
120 await hass.config_entries.async_reload(entry_id)
122 return self.json_message(
"Entry cannot be reloaded", HTTPStatus.FORBIDDEN)
124 return self.json({
"require_restart":
not entry.state.recoverable})
128 result: data_entry_flow.FlowResult,
129 prepare_result_json: Callable[
133 """Convert result to JSON."""
134 if result[
"type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
135 return prepare_result_json(result)
139 data[
"result"] = entry.as_json_fragment
148 """View to create config flows."""
150 url =
"/api/config/config_entries/flow"
151 name =
"api:config:config_entries:flow"
153 async
def get(self, request: web.Request) -> NoReturn:
154 """Not implemented."""
155 raise aiohttp.web_exceptions.HTTPMethodNotAllowed(
"GET", [
"POST"])
157 @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
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,
163 extra=vol.ALLOW_EXTRA,
166 async
def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
167 """Initialize a POST request for a config entry flow."""
171 self, request: web.Request, data: dict[str, Any]
173 """Handle a POST request for a config entry flow."""
175 return await super().
_post_impl(request, data)
176 except DependencyError
as exc:
178 text=f
"Failed dependencies {', '.join(exc.failed_dependencies)}",
179 status=HTTPStatus.BAD_REQUEST,
183 """Return context."""
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
192 self, result: data_entry_flow.FlowResult
194 """Convert result to JSON."""
201 """View to interact with the flow manager."""
203 url =
"/api/config/config_entries/flow/{flow_id}"
204 name =
"api:config:config_entries:flow:resource"
206 @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
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)
212 @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
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)
219 self, result: data_entry_flow.FlowResult
221 """Convert result to JSON."""
226 """View to query available flows."""
228 url =
"/api/config/config_entries/flow_handlers"
229 name =
"api:config:config_entries:flow_handlers"
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"]
243 """View to create option flows."""
245 url =
"/api/config/config_entries/options/flow"
246 name =
"api:config:config_entries:option:flow"
248 @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
250 async
def post(self, request: web.Request) -> web.Response:
251 """Handle a POST request.
253 handler in request is entry_id.
255 return await super().
post(request)
261 """View to interact with the option flow manager."""
263 url =
"/api/config/config_entries/options/flow/{flow_id}"
264 name =
"api:config:config_entries:options:flow:resource"
266 @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
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)
272 @require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
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)
279 @websocket_api.require_admin
280 @websocket_api.websocket_command({"type": "config_entries/flow/progress"})
286 """List flows that are in progress but not started by a user.
288 Example of a non-user initiated flow is a discovered Hue hub that
289 requires user interaction to finish setup.
291 connection.send_result(
295 for flw
in hass.config_entries.flow.async_progress()
296 if flw[
"context"][
"source"] != config_entries.SOURCE_USER
302 connection: websocket_api.ActiveConnection, msg_id: int
304 """Send Config entry not found error."""
305 connection.send_error(msg_id, websocket_api.ERR_NOT_FOUND,
"Config entry not found")
314 """Get entry, send error message if it doesn't exist."""
315 if (entry := hass.config_entries.async_get_entry(entry_id))
is None:
320 @websocket_api.require_admin
321 @websocket_api.websocket_command(
{
"type": "config_entries/get_single",
"entry_id": str,
}
)
322 @websocket_api.async_response
328 """Update config entry."""
329 entry =
get_entry(hass, connection, msg[
"entry_id"], msg[
"id"])
333 result = {
"config_entry": entry.as_json_fragment}
334 connection.send_result(msg[
"id"], result)
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,
343 @websocket_api.async_response
349 """Update config entry."""
353 changes.pop(
"entry_id")
355 entry =
get_entry(hass, connection, msg[
"entry_id"], msg[
"id"])
359 old_disable_polling = entry.pref_disable_polling
361 hass.config_entries.async_update_entry(entry, **changes)
364 "config_entry": entry.as_json_fragment,
365 "require_restart":
False,
368 initial_state = entry.state
370 old_disable_polling != entry.pref_disable_polling
371 and initial_state
is config_entries.ConfigEntryState.LOADED
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
378 connection.send_result(msg[
"id"], result)
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),
385 @websocket_api.async_response
391 """Disable config entry."""
392 if (disabled_by := msg[
"disabled_by"])
is not None:
397 success = await hass.config_entries.async_set_disabled_by(
398 msg[
"entry_id"], disabled_by
407 result = {
"require_restart":
not success}
409 connection.send_result(msg[
"id"], result)
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
420 """Ignore a config flow."""
424 for flw
in hass.config_entries.flow.async_progress()
425 if flw[
"flow_id"] == msg[
"flow_id"]
434 if "unique_id" not in flow[
"context"]:
435 connection.send_error(
436 msg[
"id"],
"no_unique_id",
"Specified flow has no unique ID."
441 if "discovery_key" in flow[
"context"]:
442 context[
"discovery_key"] = flow[
"context"][
"discovery_key"]
443 await hass.config_entries.flow.async_init(
446 data={
"unique_id": flow[
"context"][
"unique_id"],
"title": msg[
"title"]},
448 connection.send_result(msg[
"id"])
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,
456 @websocket_api.async_response
462 """Return matching config entries by type and/or domain."""
464 hass, msg.get(
"type_filter"), msg.get(
"domain")
466 connection.send_result(msg[
"id"], fragments)
469 @websocket_api.websocket_command(
{
vol.Required("type"):
"config_entries/subscribe",
470 vol.Optional(
"type_filter"): vol.All(cv.ensure_list, [str]),
473 @websocket_api.async_response
479 """Subscribe to config entry updates."""
480 type_filter = msg.get(
"type_filter")
483 def async_forward_config_entry_changes(
486 """Forward config entry state events to websocket."""
489 if integration.integration_type
not in type_filter:
492 connection.send_message(
493 websocket_api.event_message(
498 "entry": entry.as_json_fragment,
505 hass, type_filter,
None
509 config_entries.SIGNAL_CONFIG_ENTRY_CHANGED,
510 async_forward_config_entry_changes,
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]
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."""
525 entries = hass.config_entries.async_entries(domain)
527 entries = hass.config_entries.async_entries()
530 return [entry.as_json_fragment
for entry
in entries]
532 integrations: dict[str, Integration] = {}
534 domains = {entry.domain
for entry
in entries}
535 for domain_key, integration_or_exc
in (
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
547 filter_is_not_helper = type_filter != [
"helper"]
548 filter_set = set(type_filter)
550 entry.as_json_fragment
556 (integration := integrations.get(entry.domain))
557 and integration.integration_type
in filter_set
559 or (filter_is_not_helper
and entry.domain
not in integrations)
561
web.Response get(self, web.Request request)
web.Response get(self, web.Request request)
web.Response post(self, web.Request request, str entry_id)
web.Response delete(self, web.Request request, str entry_id)
NoReturn get(self, web.Request request)
data_entry_flow.FlowResult _prepare_result_json(self, data_entry_flow.FlowResult result)
dict[str, Any] get_context(self, dict[str, Any] data)
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 post(self, web.Request request)
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)
bool async_setup(HomeAssistant hass)
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)
set[str] async_get_config_flows(HomeAssistant hass, Literal["device", "helper", "hub", "service"]|None type_filter=None)
Integration async_get_loaded_integration(HomeAssistant hass, str domain)
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)