Home Assistant Unofficial Reference 2024.12.1
schema_config_entry_flow.py
Go to the documentation of this file.
1 """Helpers for creating schema based data entry flows."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 from collections.abc import Callable, Container, Coroutine, Mapping
7 import copy
8 from dataclasses import dataclass
9 import types
10 from typing import Any, cast
11 
12 import voluptuous as vol
13 
14 from homeassistant.config_entries import (
15  ConfigEntry,
16  ConfigFlow,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.core import HomeAssistant, callback, split_entity_id
21 from homeassistant.data_entry_flow import UnknownHandler
22 
23 from . import entity_registry as er, selector
24 from .typing import UNDEFINED, UndefinedType
25 
26 
27 class SchemaFlowError(Exception):
28  """Validation failed."""
29 
30 
31 @dataclass
33  """Define a config or options flow step."""
34 
35 
36 @dataclass(slots=True)
38  """Define a config or options flow form step."""
39 
40  schema: (
41  vol.Schema
42  | Callable[[SchemaCommonFlowHandler], Coroutine[Any, Any, vol.Schema | None]]
43  | None
44  ) = None
45  """Optional voluptuous schema, or function which returns a schema or None, for
46  requesting and validating user input.
47 
48  - If a function is specified, the function will be passed the current
49  `SchemaCommonFlowHandler`.
50  - If schema validation fails, the step will be retried. If the schema is None, no
51  user input is requested.
52  """
53 
54  validate_user_input: (
55  Callable[
56  [SchemaCommonFlowHandler, dict[str, Any]],
57  Coroutine[Any, Any, dict[str, Any]],
58  ]
59  | None
60  ) = None
61  """Optional function to validate user input.
62 
63  - The `validate_user_input` function is called if the schema validates successfully.
64  - The first argument is a reference to the current `SchemaCommonFlowHandler`.
65  - The second argument is the user input from the current step.
66  - The `validate_user_input` should raise `SchemaFlowError` if user input is invalid.
67  """
68 
69  next_step: (
70  Callable[[dict[str, Any]], Coroutine[Any, Any, str | None]] | str | None
71  ) = None
72  """Optional property to identify next step.
73 
74  - If `next_step` is a function, it is called if the schema validates successfully or
75  if no schema is defined. The `next_step` function is passed the union of
76  config entry options and user input from previous steps. If the function returns
77  None, the flow is ended with `FlowResultType.CREATE_ENTRY`.
78  - If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`.
79  """
80 
81  suggested_values: (
82  Callable[[SchemaCommonFlowHandler], Coroutine[Any, Any, dict[str, Any]]]
83  | None
84  | UndefinedType
85  ) = UNDEFINED
86  """Optional property to populate suggested values.
87 
88  - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested
89  value from an option with the same key.
90 
91  Note: if a step is retried due to a validation failure, then the user input will
92  have priority over the suggested values.
93  """
94 
95  preview: str | None = None
96  """Optional preview component."""
97 
98 
99 @dataclass(slots=True)
101  """Define a config or options flow menu step."""
102 
103  # Menu options
104  options: Container[str]
105 
106 
108  """Handle a schema based config or options flow."""
109 
110  def __init__(
111  self,
112  handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
113  flow: Mapping[str, SchemaFlowStep],
114  options: dict[str, Any] | None,
115  ) -> None:
116  """Initialize a common handler."""
117  self._flow_flow = flow
118  self._handler_handler = handler
119  self._options_options = options if options is not None else {}
120  self._flow_state: dict[str, Any] = {}
121 
122  @property
123  def parent_handler(self) -> SchemaConfigFlowHandler | SchemaOptionsFlowHandler:
124  """Return parent handler."""
125  return self._handler_handler
126 
127  @property
128  def options(self) -> dict[str, Any]:
129  """Return the options linked to the current flow handler."""
130  return self._options_options
131 
132  @property
133  def flow_state(self) -> dict[str, Any]:
134  """Return the flow state, used to store temporary data.
135 
136  It can be used for example to store the key or the index of a sub-item
137  that will be edited in the next step.
138  """
139  return self._flow_state
140 
141  async def async_step(
142  self, step_id: str, user_input: dict[str, Any] | None = None
143  ) -> ConfigFlowResult:
144  """Handle a step."""
145  if isinstance(self._flow_flow[step_id], SchemaFlowFormStep):
146  return await self._async_form_step_async_form_step(step_id, user_input)
147  return await self._async_menu_step_async_menu_step(step_id, user_input)
148 
149  async def _get_schema(self, form_step: SchemaFlowFormStep) -> vol.Schema | None:
150  if form_step.schema is None:
151  return None
152  if isinstance(form_step.schema, vol.Schema):
153  return form_step.schema
154  return await form_step.schema(self)
155 
156  async def _async_form_step(
157  self, step_id: str, user_input: dict[str, Any] | None = None
158  ) -> ConfigFlowResult:
159  """Handle a form step."""
160  form_step: SchemaFlowFormStep = cast(SchemaFlowFormStep, self._flow_flow[step_id])
161 
162  if (
163  user_input is not None
164  and (data_schema := await self._get_schema_get_schema(form_step))
165  and data_schema.schema
166  and not self._handler_handler.show_advanced_options
167  ):
168  # Add advanced field default if not set
169  for key in data_schema.schema:
170  if isinstance(key, (vol.Optional, vol.Required)):
171  if (
172  key.description
173  and key.description.get("advanced")
174  and key.default is not vol.UNDEFINED
175  and key not in self._options_options
176  ):
177  user_input[str(key.schema)] = cast(
178  Callable[[], Any], key.default
179  )()
180 
181  if user_input is not None and form_step.validate_user_input is not None:
182  # Do extra validation of user input
183  try:
184  user_input = await form_step.validate_user_input(self, user_input)
185  except SchemaFlowError as exc:
186  return await self._show_next_step_show_next_step(step_id, exc, user_input)
187 
188  if user_input is not None:
189  # User input was validated successfully, update options
190  self._update_and_remove_omitted_optional_keys_update_and_remove_omitted_optional_keys(
191  self._options_options, user_input, data_schema
192  )
193 
194  if user_input is not None or form_step.schema is None:
195  return await self._show_next_step_or_create_entry_show_next_step_or_create_entry(form_step)
196 
197  return await self._show_next_step_show_next_step(step_id)
198 
200  self,
201  values: dict[str, Any],
202  user_input: dict[str, Any],
203  data_schema: vol.Schema | None,
204  ) -> None:
205  values.update(user_input)
206  if data_schema and data_schema.schema:
207  for key in data_schema.schema:
208  if (
209  isinstance(key, vol.Optional)
210  and key not in user_input
211  and not (
212  # don't remove advanced keys, if they are hidden
213  key.description
214  and key.description.get("advanced")
215  and not self._handler_handler.show_advanced_options
216  )
217  ):
218  # Key not present, delete keys old value (if present) too
219  values.pop(key.schema, None)
220 
222  self, form_step: SchemaFlowFormStep
223  ) -> ConfigFlowResult:
224  next_step_id_or_end_flow: str | None
225 
226  if callable(form_step.next_step):
227  next_step_id_or_end_flow = await form_step.next_step(self._options_options)
228  else:
229  next_step_id_or_end_flow = form_step.next_step
230 
231  if next_step_id_or_end_flow is None:
232  # Flow done, create entry or update config entry options
233  return self._handler_handler.async_create_entry(data=self._options_options)
234  return await self._show_next_step_show_next_step(next_step_id_or_end_flow)
235 
236  async def _show_next_step(
237  self,
238  next_step_id: str,
239  error: SchemaFlowError | None = None,
240  user_input: dict[str, Any] | None = None,
241  ) -> ConfigFlowResult:
242  """Show form for next step."""
243  if isinstance(self._flow_flow[next_step_id], SchemaFlowMenuStep):
244  menu_step = cast(SchemaFlowMenuStep, self._flow_flow[next_step_id])
245  return self._handler_handler.async_show_menu(
246  step_id=next_step_id,
247  menu_options=menu_step.options,
248  )
249 
250  form_step = cast(SchemaFlowFormStep, self._flow_flow[next_step_id])
251 
252  if (data_schema := await self._get_schema_get_schema(form_step)) is None:
253  return await self._show_next_step_or_create_entry_show_next_step_or_create_entry(form_step)
254 
255  suggested_values: dict[str, Any] = {}
256  if form_step.suggested_values is UNDEFINED:
257  suggested_values = self._options_options
258  elif form_step.suggested_values:
259  suggested_values = await form_step.suggested_values(self)
260 
261  if user_input:
262  # We don't want to mutate the existing options
263  suggested_values = copy.deepcopy(suggested_values)
264  self._update_and_remove_omitted_optional_keys_update_and_remove_omitted_optional_keys(
265  suggested_values, user_input, await self._get_schema_get_schema(form_step)
266  )
267 
268  if data_schema.schema:
269  # Make a copy of the schema with suggested values set to saved options
270  data_schema = self._handler_handler.add_suggested_values_to_schema(
271  data_schema, suggested_values
272  )
273 
274  errors = {"base": str(error)} if error else None
275 
276  # Show form for next step
277  last_step = None
278  if not callable(form_step.next_step):
279  last_step = form_step.next_step is None
280  return self._handler_handler.async_show_form(
281  step_id=next_step_id,
282  data_schema=data_schema,
283  errors=errors,
284  last_step=last_step,
285  preview=form_step.preview,
286  )
287 
288  async def _async_menu_step(
289  self, step_id: str, user_input: dict[str, Any] | None = None
290  ) -> ConfigFlowResult:
291  """Handle a menu step."""
292  menu_step: SchemaFlowMenuStep = cast(SchemaFlowMenuStep, self._flow_flow[step_id])
293  return self._handler_handler.async_show_menu(
294  step_id=step_id,
295  menu_options=menu_step.options,
296  )
297 
298 
300  """Handle a schema based config flow."""
301 
302  config_flow: Mapping[str, SchemaFlowStep]
303  options_flow: Mapping[str, SchemaFlowStep] | None = None
304 
305  VERSION = 1
306 
307  def __init_subclass__(cls, **kwargs: Any) -> None:
308  """Initialize a subclass."""
309  super().__init_subclass__(**kwargs)
310 
311  @callback
312  def _async_get_options_flow(
313  config_entry: ConfigEntry,
314  ) -> OptionsFlow:
315  """Get the options flow for this handler."""
316  if cls.options_flow is None:
317  raise UnknownHandler
318 
320  config_entry,
321  cls.options_flow,
322  cls.async_options_flow_finishedasync_options_flow_finished,
323  cls.async_setup_previewasync_setup_previewasync_setup_preview,
324  )
325 
326  # Create an async_get_options_flow method
327  cls.async_get_options_flowasync_get_options_flowasync_get_options_flow = _async_get_options_flow # type: ignore[method-assign]
328 
329  # Create flow step methods for each step defined in the flow schema
330  for step in cls.config_flow:
331  setattr(cls, f"async_step_{step}", cls._async_step_async_step(step))
332 
333  def __init__(self) -> None:
334  """Initialize config flow."""
335  self._common_handler_common_handler = SchemaCommonFlowHandler(self, self.config_flow, None)
336 
337  @staticmethod
338  async def async_setup_preview(hass: HomeAssistant) -> None:
339  """Set up preview."""
340 
341  @classmethod
342  @callback
343  def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
344  """Return options flow support for this handler."""
345  return cls.options_flow is not None
346 
347  @staticmethod
349  step_id: str,
350  ) -> Callable[
351  [SchemaConfigFlowHandler, dict[str, Any] | None],
352  Coroutine[Any, Any, ConfigFlowResult],
353  ]:
354  """Generate a step handler."""
355 
356  async def _async_step(
357  self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None
358  ) -> ConfigFlowResult:
359  """Handle a config flow step."""
360  return await self._common_handler_common_handler.async_step(step_id, user_input)
361 
362  return _async_step
363 
364  @abstractmethod
365  @callback
366  def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
367  """Return config entry title.
368 
369  The options parameter contains config entry options, which is the union of user
370  input from the config flow steps.
371  """
372 
373  @callback
374  def async_config_flow_finished(self, options: Mapping[str, Any]) -> None:
375  """Take necessary actions after the config flow is finished, if needed.
376 
377  The options parameter contains config entry options, which is the union of user
378  input from the config flow steps.
379  """
380 
381  @callback
382  @staticmethod
384  hass: HomeAssistant, options: Mapping[str, Any]
385  ) -> None:
386  """Take necessary actions after the options flow is finished, if needed.
387 
388  The options parameter contains config entry options, which is the union of
389  stored options and user input from the options flow steps.
390  """
391 
392  @callback
394  self,
395  data: Mapping[str, Any],
396  **kwargs: Any,
397  ) -> ConfigFlowResult:
398  """Finish config flow and create a config entry."""
399  self.async_config_flow_finishedasync_config_flow_finished(data)
400  return super().async_create_entry(
401  data={}, options=data, title=self.async_config_entry_titleasync_config_entry_title(data), **kwargs
402  )
403 
404 
406  """Handle a schema based options flow."""
407 
408  def __init__(
409  self,
410  config_entry: ConfigEntry,
411  options_flow: Mapping[str, SchemaFlowStep],
412  async_options_flow_finished: Callable[[HomeAssistant, Mapping[str, Any]], None]
413  | None = None,
414  async_setup_preview: Callable[[HomeAssistant], Coroutine[Any, Any, None]]
415  | None = None,
416  ) -> None:
417  """Initialize options flow.
418 
419  If needed, `async_options_flow_finished` can be set to take necessary actions
420  after the options flow is finished. The second parameter contains config entry
421  options, which is the union of stored options and user input from the options
422  flow steps.
423  """
424  self._options_options = copy.deepcopy(dict(config_entry.options))
425  self._common_handler_common_handler = SchemaCommonFlowHandler(self, options_flow, self.optionsoptions)
426  self._async_options_flow_finished_async_options_flow_finished = async_options_flow_finished
427 
428  for step in options_flow:
429  setattr(
430  self,
431  f"async_step_{step}",
432  types.MethodType(self._async_step_async_step(step), self),
433  )
434 
435  if async_setup_preview:
436  setattr(self, "async_setup_preview", async_setup_preview)
437 
438  @property
439  def options(self) -> dict[str, Any]:
440  """Return a mutable copy of the config entry options."""
441  return self._options_options
442 
443  @staticmethod
445  step_id: str,
446  ) -> Callable[
447  [SchemaConfigFlowHandler, dict[str, Any] | None],
448  Coroutine[Any, Any, ConfigFlowResult],
449  ]:
450  """Generate a step handler."""
451 
452  async def _async_step(
453  self: SchemaConfigFlowHandler, user_input: dict[str, Any] | None = None
454  ) -> ConfigFlowResult:
455  """Handle an options flow step."""
456  return await self._common_handler_common_handler.async_step(step_id, user_input)
457 
458  return _async_step
459 
460  @callback
462  self,
463  data: Mapping[str, Any],
464  **kwargs: Any,
465  ) -> ConfigFlowResult:
466  """Finish config flow and create a config entry."""
467  if self._async_options_flow_finished_async_options_flow_finished:
468  self._async_options_flow_finished_async_options_flow_finished(self.hass, data)
469  return super().async_create_entry(data=data, **kwargs)
470 
471 
472 @callback
474  hass: HomeAssistant, entity_id_or_uuid: str
475 ) -> str:
476  """Generate title for a config entry wrapping a single entity.
477 
478  If the entity is registered, use the registry entry's name.
479  If the entity is in the state machine, use the name from the state.
480  Otherwise, fall back to the object ID.
481  """
482  registry = er.async_get(hass)
483  entity_id = er.async_validate_entity_id(registry, entity_id_or_uuid)
484  object_id = split_entity_id(entity_id)[1]
485  entry = registry.async_get(entity_id)
486  if entry:
487  return entry.name or entry.original_name or object_id
488  state = hass.states.get(entity_id)
489  if state:
490  return state.name or object_id
491  return object_id
492 
493 
494 @callback
496  handler: SchemaOptionsFlowHandler,
497  entity_selector_config: selector.EntitySelectorConfig,
499  """Return an entity selector which excludes own entities."""
500  entity_registry = er.async_get(handler.hass)
501  entities = er.async_entries_for_config_entry(
502  entity_registry,
503  handler.config_entry.entry_id,
504  )
505  entity_ids = [ent.entity_id for ent in entities]
506 
507  final_selector_config = entity_selector_config.copy()
508  final_selector_config["exclude_entities"] = entity_ids
509 
510  return selector.EntitySelector(final_selector_config)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
None async_setup_preview(HomeAssistant hass)
ConfigFlowResult async_step(self, str step_id, dict[str, Any]|None user_input=None)
ConfigFlowResult _async_menu_step(self, str step_id, dict[str, Any]|None user_input=None)
dict[str, Any] flow_state(self)
None _update_and_remove_omitted_optional_keys(self, dict[str, Any] values, dict[str, Any] user_input, vol.Schema|None data_schema)
None __init__(self, SchemaConfigFlowHandler|SchemaOptionsFlowHandler handler, Mapping[str, SchemaFlowStep] flow, dict[str, Any]|None options)
_flow
ConfigFlowResult _show_next_step(self, str next_step_id, SchemaFlowError|None error=None, dict[str, Any]|None user_input=None)
_options
ConfigFlowResult _async_form_step(self, str step_id, dict[str, Any]|None user_input=None)
_handler
dict[str, Any] options(self)
vol.Schema|None _get_schema(self, SchemaFlowFormStep form_step)
ConfigFlowResult _show_next_step_or_create_entry(self, SchemaFlowFormStep form_step)
SchemaConfigFlowHandler|SchemaOptionsFlowHandler parent_handler(self)
async_get_options_flow
None async_config_flow_finished(self, Mapping[str, Any] options)
bool async_supports_options_flow(cls, ConfigEntry config_entry)
Callable[[SchemaConfigFlowHandler, dict[str, Any]|None], Coroutine[Any, Any, ConfigFlowResult],] _async_step(str step_id)
None async_setup_preview(HomeAssistant hass)
str async_config_entry_title(self, Mapping[str, Any] options)
None async_options_flow_finished(HomeAssistant hass, Mapping[str, Any] options)
_common_handler
None __init_subclass__(cls, **Any kwargs)
None __init__(self)
ConfigFlowResult async_create_entry(self, Mapping[str, Any] data, **Any kwargs)
None __init__(self, ConfigEntry config_entry, Mapping[str, SchemaFlowStep] options_flow, Callable[[HomeAssistant, Mapping[str, Any]], None]|None async_options_flow_finished=None, Callable[[HomeAssistant], Coroutine[Any, Any, None]]|None async_setup_preview=None)
_common_handler
ConfigFlowResult async_create_entry(self, Mapping[str, Any] data, **Any kwargs)
_async_options_flow_finished
_options
Callable[[SchemaConfigFlowHandler, dict[str, Any]|None], Coroutine[Any, Any, ConfigFlowResult],] _async_step(str step_id)
dict[str, Any] options(self)
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
selector.EntitySelector entity_selector_without_own_entities(SchemaOptionsFlowHandler handler, selector.EntitySelectorConfig entity_selector_config)
str wrapped_entity_config_entry_title(HomeAssistant hass, str entity_id_or_uuid)