Home Assistant Unofficial Reference 2024.12.1
data_entry_flow.py
Go to the documentation of this file.
1 """Classes to help gather user submissions."""
2 
3 from __future__ import annotations
4 
5 import abc
6 import asyncio
7 from collections import defaultdict
8 from collections.abc import Callable, Container, Hashable, Iterable, Mapping
9 from contextlib import suppress
10 import copy
11 from dataclasses import dataclass
12 from enum import StrEnum
13 from functools import partial
14 import logging
15 from types import MappingProxyType
16 from typing import Any, Generic, Required, TypedDict, cast
17 
18 from typing_extensions import TypeVar
19 import voluptuous as vol
20 
21 from .core import HomeAssistant, callback
22 from .exceptions import HomeAssistantError
23 from .helpers.deprecation import (
24  DeprecatedConstantEnum,
25  all_with_deprecated_constants,
26  check_if_deprecated_constant,
27  dir_with_deprecated_constants,
28 )
29 from .helpers.frame import ReportBehavior, report_usage
30 from .loader import async_suggest_report_issue
31 from .util import uuid as uuid_util
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 
36 class FlowResultType(StrEnum):
37  """Result type for a data entry flow."""
38 
39  FORM = "form"
40  CREATE_ENTRY = "create_entry"
41  ABORT = "abort"
42  EXTERNAL_STEP = "external"
43  EXTERNAL_STEP_DONE = "external_done"
44  SHOW_PROGRESS = "progress"
45  SHOW_PROGRESS_DONE = "progress_done"
46  MENU = "menu"
47 
48 
49 # RESULT_TYPE_* is deprecated, to be removed in 2025.1
50 _DEPRECATED_RESULT_TYPE_FORM = DeprecatedConstantEnum(FlowResultType.FORM, "2025.1")
51 _DEPRECATED_RESULT_TYPE_CREATE_ENTRY = DeprecatedConstantEnum(
52  FlowResultType.CREATE_ENTRY, "2025.1"
53 )
54 _DEPRECATED_RESULT_TYPE_ABORT = DeprecatedConstantEnum(FlowResultType.ABORT, "2025.1")
55 _DEPRECATED_RESULT_TYPE_EXTERNAL_STEP = DeprecatedConstantEnum(
56  FlowResultType.EXTERNAL_STEP, "2025.1"
57 )
58 _DEPRECATED_RESULT_TYPE_EXTERNAL_STEP_DONE = DeprecatedConstantEnum(
59  FlowResultType.EXTERNAL_STEP_DONE, "2025.1"
60 )
61 _DEPRECATED_RESULT_TYPE_SHOW_PROGRESS = DeprecatedConstantEnum(
62  FlowResultType.SHOW_PROGRESS, "2025.1"
63 )
64 _DEPRECATED_RESULT_TYPE_SHOW_PROGRESS_DONE = DeprecatedConstantEnum(
65  FlowResultType.SHOW_PROGRESS_DONE, "2025.1"
66 )
67 _DEPRECATED_RESULT_TYPE_MENU = DeprecatedConstantEnum(FlowResultType.MENU, "2025.1")
68 
69 # Event that is fired when a flow is progressed via external or progress source.
70 EVENT_DATA_ENTRY_FLOW_PROGRESSED = "data_entry_flow_progressed"
71 
72 FLOW_NOT_COMPLETE_STEPS = {
73  FlowResultType.FORM,
74  FlowResultType.EXTERNAL_STEP,
75  FlowResultType.EXTERNAL_STEP_DONE,
76  FlowResultType.SHOW_PROGRESS,
77  FlowResultType.SHOW_PROGRESS_DONE,
78  FlowResultType.MENU,
79 }
80 
81 
82 STEP_ID_OPTIONAL_STEPS = {
83  FlowResultType.EXTERNAL_STEP,
84  FlowResultType.FORM,
85  FlowResultType.MENU,
86  FlowResultType.SHOW_PROGRESS,
87 }
88 
89 
90 _FlowContextT = TypeVar("_FlowContextT", bound="FlowContext", default="FlowContext")
91 _FlowResultT = TypeVar(
92  "_FlowResultT", bound="FlowResult[Any, Any]", default="FlowResult"
93 )
94 _HandlerT = TypeVar("_HandlerT", default=str)
95 
96 
97 @dataclass(slots=True)
99  """Base class for discovery ServiceInfo."""
100 
101 
102 class FlowError(HomeAssistantError):
103  """Base class for data entry errors."""
104 
105 
107  """Unknown handler specified."""
108 
109 
111  """Unknown flow specified."""
112 
113 
115  """Unknown step specified."""
116 
117 
118 class InvalidData(vol.Invalid):
119  """Invalid data provided."""
120 
121  def __init__(
122  self,
123  message: str,
124  path: list[Hashable] | None,
125  error_message: str | None,
126  schema_errors: dict[str, Any],
127  **kwargs: Any,
128  ) -> None:
129  super().__init__(message, path, error_message, **kwargs)
130  self.schema_errorsschema_errors = schema_errors
131 
132 
134  """Exception to indicate a flow needs to be aborted."""
135 
136  def __init__(
137  self, reason: str, description_placeholders: Mapping[str, str] | None = None
138  ) -> None:
139  """Initialize an abort flow exception."""
140  super().__init__(f"Flow aborted: {reason}")
141  self.reasonreason = reason
142  self.description_placeholdersdescription_placeholders = description_placeholders
143 
144 
145 class FlowContext(TypedDict, total=False):
146  """Typed context dict."""
147 
148  show_advanced_options: bool
149  source: str
150 
151 
152 class FlowResult(TypedDict, Generic[_FlowContextT, _HandlerT], total=False):
153  """Typed result dict."""
154 
155  context: _FlowContextT
156  data_schema: vol.Schema | None
157  data: Mapping[str, Any]
158  description_placeholders: Mapping[str, str] | None
159  description: str | None
160  errors: dict[str, str] | None
161  extra: str
162  flow_id: Required[str]
163  handler: Required[_HandlerT]
164  last_step: bool | None
165  menu_options: Container[str]
166  preview: str | None
167  progress_action: str
168  progress_task: asyncio.Task[Any] | None
169  reason: str
170  required: bool
171  result: Any
172  step_id: str
173  title: str
174  translation_domain: str
175  type: FlowResultType
176  url: str
177 
178 
180  schema_errors: dict[str, Any],
181  error: vol.Invalid,
182  data_schema: vol.Schema,
183 ) -> None:
184  """Map an error to the correct position in the schema_errors.
185 
186  Raises ValueError if the error path could not be found in the schema.
187  Limitation: Nested schemas are not supported and a ValueError will be raised.
188  """
189  schema = data_schema.schema
190  error_path = error.path
191  if not error_path or (path_part := error_path[0]) not in schema:
192  raise ValueError("Could not find path in schema")
193 
194  if len(error_path) > 1:
195  raise ValueError("Nested schemas are not supported")
196 
197  # path_part can also be vol.Marker, but we need a string key
198  path_part_str = str(path_part)
199  schema_errors[path_part_str] = error.error_message
200 
201 
202 class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]):
203  """Manage all the flows that are in progress."""
204 
205  _flow_result: type[_FlowResultT] = FlowResult # type: ignore[assignment]
206 
207  def __init__(
208  self,
209  hass: HomeAssistant,
210  ) -> None:
211  """Initialize the flow manager."""
212  self.hasshass = hass
213  self._preview: set[_HandlerT] = set()
214  self._progress: dict[
215  str, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]
216  ] = {}
217  self._handler_progress_index: defaultdict[
218  _HandlerT, set[FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]]
219  ] = defaultdict(set)
220  self._init_data_process_index: defaultdict[
221  type, set[FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]]
222  ] = defaultdict(set)
223 
224  @abc.abstractmethod
225  async def async_create_flow(
226  self,
227  handler_key: _HandlerT,
228  *,
229  context: _FlowContextT | None = None,
230  data: dict[str, Any] | None = None,
231  ) -> FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]:
232  """Create a flow for specified handler.
233 
234  Handler key is the domain of the component that we want to set up.
235  """
236 
237  @abc.abstractmethod
238  async def async_finish_flow(
239  self,
240  flow: FlowHandler[_FlowContextT, _FlowResultT, _HandlerT],
241  result: _FlowResultT,
242  ) -> _FlowResultT:
243  """Finish a data entry flow.
244 
245  This method is called when a flow step returns FlowResultType.ABORT or
246  FlowResultType.CREATE_ENTRY.
247  """
248 
249  async def async_post_init(
250  self,
251  flow: FlowHandler[_FlowContextT, _FlowResultT, _HandlerT],
252  result: _FlowResultT,
253  ) -> None:
254  """Entry has finished executing its first step asynchronously."""
255 
256  @callback
257  def async_get(self, flow_id: str) -> _FlowResultT:
258  """Return a flow in progress as a partial FlowResult."""
259  if (flow := self._progress.get(flow_id)) is None:
260  raise UnknownFlow
261  return self._async_flow_handler_to_flow_result_async_flow_handler_to_flow_result([flow], False)[0]
262 
263  @callback
264  def async_progress(self, include_uninitialized: bool = False) -> list[_FlowResultT]:
265  """Return the flows in progress as a partial FlowResult."""
266  return self._async_flow_handler_to_flow_result_async_flow_handler_to_flow_result(
267  self._progress.values(), include_uninitialized
268  )
269 
270  @callback
272  self,
273  handler: _HandlerT,
274  include_uninitialized: bool = False,
275  match_context: dict[str, Any] | None = None,
276  ) -> list[_FlowResultT]:
277  """Return the flows in progress by handler as a partial FlowResult.
278 
279  If match_context is specified, only return flows with a context that
280  is a superset of match_context.
281  """
282  return self._async_flow_handler_to_flow_result_async_flow_handler_to_flow_result(
283  self._async_progress_by_handler_async_progress_by_handler(handler, match_context),
284  include_uninitialized,
285  )
286 
287  @callback
289  self,
290  init_data_type: type,
291  matcher: Callable[[Any], bool],
292  include_uninitialized: bool = False,
293  ) -> list[_FlowResultT]:
294  """Return flows in progress init matching by data type as a partial FlowResult."""
295  return self._async_flow_handler_to_flow_result_async_flow_handler_to_flow_result(
296  [
297  progress
298  for progress in self._init_data_process_index.get(init_data_type, ())
299  if matcher(progress.init_data)
300  ],
301  include_uninitialized,
302  )
303 
304  @callback
306  self, handler: _HandlerT, match_context: dict[str, Any] | None
307  ) -> list[FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]]:
308  """Return the flows in progress by handler.
309 
310  If match_context is specified, only return flows with a context that
311  is a superset of match_context.
312  """
313  if not match_context:
314  return list(self._handler_progress_index.get(handler, ()))
315  match_context_items = match_context.items()
316  return [
317  progress
318  for progress in self._handler_progress_index.get(handler, ())
319  if match_context_items <= progress.context.items()
320  ]
321 
322  async def async_init(
323  self,
324  handler: _HandlerT,
325  *,
326  context: _FlowContextT | None = None,
327  data: Any = None,
328  ) -> _FlowResultT:
329  """Start a data entry flow."""
330  if context is None:
331  context = cast(_FlowContextT, {})
332  flow = await self.async_create_flowasync_create_flow(handler, context=context, data=data)
333  if not flow:
334  raise UnknownFlow("Flow was not created")
335  flow.hass = self.hasshass
336  flow.handler = handler
337  flow.flow_id = uuid_util.random_uuid_hex()
338  flow.context = context
339  flow.init_data = data
340  self._async_add_flow_progress_async_add_flow_progress(flow)
341 
342  result = await self._async_handle_step_async_handle_step(flow, flow.init_step, data)
343 
344  if result["type"] != FlowResultType.ABORT:
345  await self.async_post_initasync_post_init(flow, result)
346 
347  return result
348 
349  async def async_configure(
350  self, flow_id: str, user_input: dict | None = None
351  ) -> _FlowResultT:
352  """Continue a data entry flow."""
353  result: _FlowResultT | None = None
354 
355  # Workaround for flow handlers which have not been upgraded to pass a show
356  # progress task, needed because of the change to eager tasks in HA Core 2024.5,
357  # can be removed in HA Core 2024.8.
358  flow = self._progress.get(flow_id)
359  if flow and flow.deprecated_show_progress:
360  if (cur_step := flow.cur_step) and cur_step[
361  "type"
362  ] == FlowResultType.SHOW_PROGRESS:
363  # Allow the progress task to finish before we call the flow handler
364  await asyncio.sleep(0)
365 
366  while not result or result["type"] == FlowResultType.SHOW_PROGRESS_DONE:
367  result = await self._async_configure_async_configure(flow_id, user_input)
368  flow = self._progress.get(flow_id)
369  if flow and flow.deprecated_show_progress:
370  break
371  return result
372 
373  async def _async_configure(
374  self, flow_id: str, user_input: dict | None = None
375  ) -> _FlowResultT:
376  """Continue a data entry flow."""
377  if (flow := self._progress.get(flow_id)) is None:
378  raise UnknownFlow
379 
380  cur_step = flow.cur_step
381  assert cur_step is not None
382 
383  if (
384  data_schema := cur_step.get("data_schema")
385  ) is not None and user_input is not None:
386  data_schema = cast(vol.Schema, data_schema)
387  try:
388  user_input = data_schema(user_input)
389  except vol.Invalid as ex:
390  raised_errors = [ex]
391  if isinstance(ex, vol.MultipleInvalid):
392  raised_errors = ex.errors
393 
394  schema_errors: dict[str, Any] = {}
395  for error in raised_errors:
396  try:
397  _map_error_to_schema_errors(schema_errors, error, data_schema)
398  except ValueError:
399  # If we get here, the path in the exception does not exist in the schema.
400  schema_errors.setdefault("base", []).append(str(error))
401  raise InvalidData(
402  "Schema validation failed",
403  path=ex.path,
404  error_message=ex.error_message,
405  schema_errors=schema_errors,
406  ) from ex
407 
408  # Handle a menu navigation choice
409  if cur_step["type"] == FlowResultType.MENU and user_input:
410  result = await self._async_handle_step_async_handle_step(
411  flow, user_input["next_step_id"], None
412  )
413  else:
414  result = await self._async_handle_step_async_handle_step(
415  flow, cur_step["step_id"], user_input
416  )
417 
418  if cur_step["type"] in (
419  FlowResultType.EXTERNAL_STEP,
420  FlowResultType.SHOW_PROGRESS,
421  ):
422  if cur_step["type"] == FlowResultType.EXTERNAL_STEP and result[
423  "type"
424  ] not in (
425  FlowResultType.EXTERNAL_STEP,
426  FlowResultType.EXTERNAL_STEP_DONE,
427  ):
428  raise ValueError(
429  "External step can only transition to "
430  "external step or external step done."
431  )
432  if cur_step["type"] == FlowResultType.SHOW_PROGRESS and result[
433  "type"
434  ] not in (
435  FlowResultType.SHOW_PROGRESS,
436  FlowResultType.SHOW_PROGRESS_DONE,
437  ):
438  raise ValueError(
439  "Show progress can only transition to show progress or show"
440  " progress done."
441  )
442 
443  # If the result has changed from last result, fire event to update
444  # the frontend. The result is considered to have changed if:
445  # - The step has changed
446  # - The step is same but result type is SHOW_PROGRESS and progress_action
447  # or description_placeholders has changed
448  if cur_step["step_id"] != result.get("step_id") or (
449  result["type"] == FlowResultType.SHOW_PROGRESS
450  and (
451  cur_step["progress_action"] != result.get("progress_action")
452  or cur_step["description_placeholders"]
453  != result.get("description_placeholders")
454  )
455  ):
456  # Tell frontend to reload the flow state.
457  self.hasshass.bus.async_fire_internal(
458  EVENT_DATA_ENTRY_FLOW_PROGRESSED,
459  {"handler": flow.handler, "flow_id": flow_id, "refresh": True},
460  )
461 
462  return result
463 
464  @callback
465  def async_abort(self, flow_id: str) -> None:
466  """Abort a flow."""
467  self._async_remove_flow_progress_async_remove_flow_progress(flow_id)
468 
469  @callback
471  self, flow: FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]
472  ) -> None:
473  """Add a flow to in progress."""
474  if flow.init_data is not None:
475  self._init_data_process_index[type(flow.init_data)].add(flow)
476  self._progress[flow.flow_id] = flow
477  self._handler_progress_index[flow.handler].add(flow)
478 
479  @callback
481  self, flow: FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]
482  ) -> None:
483  """Remove a flow from in progress."""
484  if flow.init_data is not None:
485  init_data_type = type(flow.init_data)
486  self._init_data_process_index[init_data_type].remove(flow)
487  if not self._init_data_process_index[init_data_type]:
488  del self._init_data_process_index[init_data_type]
489  handler = flow.handler
490  self._handler_progress_index[handler].remove(flow)
491  if not self._handler_progress_index[handler]:
492  del self._handler_progress_index[handler]
493 
494  @callback
495  def _async_remove_flow_progress(self, flow_id: str) -> None:
496  """Remove a flow from in progress."""
497  if (flow := self._progress.pop(flow_id, None)) is None:
498  raise UnknownFlow
499  self._async_remove_flow_from_index_async_remove_flow_from_index(flow)
500  flow.async_cancel_progress_task()
501  try:
502  flow.async_remove()
503  except Exception:
504  _LOGGER.exception("Error removing %s flow", flow.handler)
505 
507  self,
508  flow: FlowHandler[_FlowContextT, _FlowResultT, _HandlerT],
509  step_id: str,
510  user_input: dict | BaseServiceInfo | None,
511  ) -> _FlowResultT:
512  """Handle a step of a flow."""
513  self._raise_if_step_does_not_exist_raise_if_step_does_not_exist(flow, step_id)
514 
515  method = f"async_step_{step_id}"
516  try:
517  result: _FlowResultT = await getattr(flow, method)(user_input)
518  except AbortFlow as err:
519  result = self._flow_result(
520  type=FlowResultType.ABORT,
521  flow_id=flow.flow_id,
522  handler=flow.handler,
523  reason=err.reason,
524  description_placeholders=err.description_placeholders,
525  )
526 
527  # Setup the flow handler's preview if needed
528  if result.get("preview") is not None:
529  await self._async_setup_preview_async_setup_preview(flow)
530 
531  if not isinstance(result["type"], FlowResultType):
532  result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable]
533  report_usage(
534  "does not use FlowResultType enum for data entry flow result type",
535  core_behavior=ReportBehavior.LOG,
536  breaks_in_ha_version="2025.1",
537  )
538 
539  if (
540  result["type"] == FlowResultType.SHOW_PROGRESS
541  # Mypy does not agree with using pop on _FlowResultT
542  and (progress_task := result.pop("progress_task", None)) # type: ignore[arg-type]
543  and progress_task != flow.async_get_progress_task()
544  ):
545  # The flow's progress task was changed, register a callback on it
546  async def call_configure() -> None:
547  with suppress(UnknownFlow):
548  await self._async_configure_async_configure(flow.flow_id)
549 
550  def schedule_configure(_: asyncio.Task) -> None:
551  self.hasshass.async_create_task(call_configure())
552 
553  # The mypy ignores are a consequence of mypy not accepting the pop above
554  progress_task.add_done_callback(schedule_configure) # type: ignore[attr-defined]
555  flow.async_set_progress_task(progress_task) # type: ignore[arg-type]
556 
557  elif result["type"] != FlowResultType.SHOW_PROGRESS:
558  flow.async_cancel_progress_task()
559 
560  if result["type"] in STEP_ID_OPTIONAL_STEPS:
561  if "step_id" not in result:
562  result["step_id"] = step_id
563 
564  if result["type"] in FLOW_NOT_COMPLETE_STEPS:
565  self._raise_if_step_does_not_exist_raise_if_step_does_not_exist(flow, result["step_id"])
566  flow.cur_step = result
567  return result
568 
569  # We pass a copy of the result because we're mutating our version
570  result = await self.async_finish_flowasync_finish_flow(flow, result.copy())
571 
572  # _async_finish_flow may change result type, check it again
573  if result["type"] == FlowResultType.FORM:
574  flow.cur_step = result
575  return result
576 
577  # Abort and Success results both finish the flow
578  self._async_remove_flow_progress_async_remove_flow_progress(flow.flow_id)
579 
580  return result
581 
583  self, flow: FlowHandler[_FlowContextT, _FlowResultT, _HandlerT], step_id: str
584  ) -> None:
585  """Raise if the step does not exist."""
586  method = f"async_step_{step_id}"
587 
588  if not hasattr(flow, method):
589  self._async_remove_flow_progress_async_remove_flow_progress(flow.flow_id)
590  raise UnknownStep(
591  f"Handler {self.__class__.__name__} doesn't support step {step_id}"
592  )
593 
595  self, flow: FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]
596  ) -> None:
597  """Set up preview for a flow handler."""
598  if flow.handler not in self._preview:
599  self._preview.add(flow.handler)
600  await flow.async_setup_preview(self.hasshass)
601 
602  @callback
604  self,
605  flows: Iterable[FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]],
606  include_uninitialized: bool,
607  ) -> list[_FlowResultT]:
608  """Convert a list of FlowHandler to a partial FlowResult that can be serialized."""
609  return [
610  self._flow_result(
611  flow_id=flow.flow_id,
612  handler=flow.handler,
613  context=flow.context,
614  step_id=flow.cur_step["step_id"],
615  )
616  if flow.cur_step
617  else self._flow_result(
618  flow_id=flow.flow_id,
619  handler=flow.handler,
620  context=flow.context,
621  )
622  for flow in flows
623  if include_uninitialized or flow.cur_step is not None
624  ]
625 
626 
627 class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
628  """Handle a data entry flow."""
629 
630  _flow_result: type[_FlowResultT] = FlowResult # type: ignore[assignment]
631 
632  # Set by flow manager
633  cur_step: _FlowResultT | None = None
634 
635  # While not purely typed, it makes typehinting more useful for us
636  # and removes the need for constant None checks or asserts.
637  flow_id: str = None # type: ignore[assignment]
638  hass: HomeAssistant = None # type: ignore[assignment]
639  handler: _HandlerT = None # type: ignore[assignment]
640  # Ensure the attribute has a subscriptable, but immutable, default value.
641  context: _FlowContextT = MappingProxyType({}) # type: ignore[assignment]
642 
643  # Set by _async_create_flow callback
644  init_step = "init"
645 
646  # The initial data that was used to start the flow
647  init_data: Any = None
648 
649  # Set by developer
650  VERSION = 1
651  MINOR_VERSION = 1
652 
653  __progress_task: asyncio.Task[Any] | None = None
654  __no_progress_task_reported = False
655  deprecated_show_progress = False
656 
657  @property
658  def source(self) -> str | None:
659  """Source that initialized the flow."""
660  return self.context.get("source", None) # type: ignore[return-value]
661 
662  @property
663  def show_advanced_options(self) -> bool:
664  """If we should show advanced options."""
665  return self.context.get("show_advanced_options", False) # type: ignore[return-value]
666 
668  self, data_schema: vol.Schema, suggested_values: Mapping[str, Any] | None
669  ) -> vol.Schema:
670  """Make a copy of the schema, populated with suggested values.
671 
672  For each schema marker matching items in `suggested_values`,
673  the `suggested_value` will be set. The existing `suggested_value` will
674  be left untouched if there is no matching item.
675  """
676  schema = {}
677  for key, val in data_schema.schema.items():
678  if isinstance(key, vol.Marker):
679  # Exclude advanced field
680  if (
681  key.description
682  and key.description.get("advanced")
683  and not self.show_advanced_optionsshow_advanced_options
684  ):
685  continue
686 
687  new_key = key
688  if (
689  suggested_values
690  and key in suggested_values
691  and isinstance(key, vol.Marker)
692  ):
693  # Copy the marker to not modify the flow schema
694  new_key = copy.copy(key)
695  new_key.description = {"suggested_value": suggested_values[key.schema]}
696  schema[new_key] = val
697  return vol.Schema(schema)
698 
699  @callback
701  self,
702  *,
703  step_id: str | None = None,
704  data_schema: vol.Schema | None = None,
705  errors: dict[str, str] | None = None,
706  description_placeholders: Mapping[str, str] | None = None,
707  last_step: bool | None = None,
708  preview: str | None = None,
709  ) -> _FlowResultT:
710  """Return the definition of a form to gather user input.
711 
712  The step_id parameter is deprecated and will be removed in a future release.
713  """
714  flow_result = self._flow_result(
715  type=FlowResultType.FORM,
716  flow_id=self.flow_id,
717  handler=self.handler,
718  data_schema=data_schema,
719  errors=errors,
720  description_placeholders=description_placeholders,
721  last_step=last_step, # Display next or submit button in frontend
722  preview=preview, # Display preview component in frontend
723  )
724  if step_id is not None:
725  flow_result["step_id"] = step_id
726  return flow_result
727 
728  @callback
730  self,
731  *,
732  title: str | None = None,
733  data: Mapping[str, Any],
734  description: str | None = None,
735  description_placeholders: Mapping[str, str] | None = None,
736  ) -> _FlowResultT:
737  """Finish flow."""
738  flow_result = self._flow_result(
739  type=FlowResultType.CREATE_ENTRY,
740  flow_id=self.flow_id,
741  handler=self.handler,
742  data=data,
743  description=description,
744  description_placeholders=description_placeholders,
745  context=self.context,
746  )
747  if title is not None:
748  flow_result["title"] = title
749  return flow_result
750 
751  @callback
753  self,
754  *,
755  reason: str,
756  description_placeholders: Mapping[str, str] | None = None,
757  ) -> _FlowResultT:
758  """Abort the flow."""
759  return self._flow_result(
760  type=FlowResultType.ABORT,
761  flow_id=self.flow_id,
762  handler=self.handler,
763  reason=reason,
764  description_placeholders=description_placeholders,
765  )
766 
767  @callback
769  self,
770  *,
771  step_id: str | None = None,
772  url: str,
773  description_placeholders: Mapping[str, str] | None = None,
774  ) -> _FlowResultT:
775  """Return the definition of an external step for the user to take.
776 
777  The step_id parameter is deprecated and will be removed in a future release.
778  """
779  flow_result = self._flow_result(
780  type=FlowResultType.EXTERNAL_STEP,
781  flow_id=self.flow_id,
782  handler=self.handler,
783  url=url,
784  description_placeholders=description_placeholders,
785  )
786  if step_id is not None:
787  flow_result["step_id"] = step_id
788  return flow_result
789 
790  @callback
791  def async_external_step_done(self, *, next_step_id: str) -> _FlowResultT:
792  """Return the definition of an external step for the user to take."""
793  return self._flow_result(
794  type=FlowResultType.EXTERNAL_STEP_DONE,
795  flow_id=self.flow_id,
796  handler=self.handler,
797  step_id=next_step_id,
798  )
799 
800  @callback
802  self,
803  *,
804  step_id: str | None = None,
805  progress_action: str,
806  description_placeholders: Mapping[str, str] | None = None,
807  progress_task: asyncio.Task[Any] | None = None,
808  ) -> _FlowResultT:
809  """Show a progress message to the user, without user input allowed.
810 
811  The step_id parameter is deprecated and will be removed in a future release.
812  """
813  if progress_task is None and not self.__no_progress_task_reported__no_progress_task_reported__no_progress_task_reported:
814  self.__no_progress_task_reported__no_progress_task_reported__no_progress_task_reported = True
815  cls = self.__class__
816  report_issue = async_suggest_report_issue(self.hass, module=cls.__module__)
817  _LOGGER.warning(
818  (
819  "%s::%s calls async_show_progress without passing a progress task, "
820  "this is not valid and will break in Home Assistant Core 2024.8. "
821  "Please %s"
822  ),
823  cls.__module__,
824  cls.__name__,
825  report_issue,
826  )
827 
828  if progress_task is None:
829  self.deprecated_show_progressdeprecated_show_progressdeprecated_show_progress = True
830 
831  flow_result = self._flow_result(
832  type=FlowResultType.SHOW_PROGRESS,
833  flow_id=self.flow_id,
834  handler=self.handler,
835  progress_action=progress_action,
836  description_placeholders=description_placeholders,
837  progress_task=progress_task,
838  )
839  if step_id is not None:
840  flow_result["step_id"] = step_id
841  return flow_result
842 
843  @callback
844  def async_show_progress_done(self, *, next_step_id: str) -> _FlowResultT:
845  """Mark the progress done."""
846  return self._flow_result(
847  type=FlowResultType.SHOW_PROGRESS_DONE,
848  flow_id=self.flow_id,
849  handler=self.handler,
850  step_id=next_step_id,
851  )
852 
853  @callback
855  self,
856  *,
857  step_id: str | None = None,
858  menu_options: Container[str],
859  description_placeholders: Mapping[str, str] | None = None,
860  ) -> _FlowResultT:
861  """Show a navigation menu to the user.
862 
863  Options dict maps step_id => i18n label
864  The step_id parameter is deprecated and will be removed in a future release.
865  """
866  flow_result = self._flow_result(
867  type=FlowResultType.MENU,
868  flow_id=self.flow_id,
869  handler=self.handler,
870  data_schema=vol.Schema({"next_step_id": vol.In(menu_options)}),
871  menu_options=menu_options,
872  description_placeholders=description_placeholders,
873  )
874  if step_id is not None:
875  flow_result["step_id"] = step_id
876  return flow_result
877 
878  @callback
879  def async_remove(self) -> None:
880  """Notification that the flow has been removed."""
881 
882  @staticmethod
883  async def async_setup_preview(hass: HomeAssistant) -> None:
884  """Set up preview."""
885 
886  @callback
887  def async_cancel_progress_task(self) -> None:
888  """Cancel in progress task."""
889  if self.__progress_task__progress_task and not self.__progress_task__progress_task.done():
890  self.__progress_task__progress_task.cancel()
891  self.__progress_task__progress_task = None
892 
893  @callback
894  def async_get_progress_task(self) -> asyncio.Task[Any] | None:
895  """Get in progress task."""
896  return self.__progress_task__progress_task
897 
898  @callback
900  self,
901  progress_task: asyncio.Task[Any],
902  ) -> None:
903  """Set in progress task."""
904  self.__progress_task__progress_task = progress_task
905 
906 
907 class SectionConfig(TypedDict, total=False):
908  """Class to represent a section config."""
909 
910  collapsed: bool
911 
912 
913 class section:
914  """Data entry flow section."""
915 
916  CONFIG_SCHEMA = vol.Schema(
917  {
918  vol.Optional("collapsed", default=False): bool,
919  },
920  )
921 
922  def __init__(
923  self, schema: vol.Schema, options: SectionConfig | None = None
924  ) -> None:
925  """Initialize."""
926  self.schemaschema = schema
927  self.options: SectionConfig = self.CONFIG_SCHEMACONFIG_SCHEMA(options or {})
928 
929  def __call__(self, value: Any) -> Any:
930  """Validate input."""
931  return self.schemaschema(value)
932 
933 
934 # These can be removed if no deprecated constant are in this module anymore
935 __getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
936 __dir__ = partial(
937  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
938 )
939 __all__ = all_with_deprecated_constants(globals())
description_placeholders
None __init__(self, str reason, Mapping[str, str]|None description_placeholders=None)
reason
None async_remove(self)
None async_setup_preview(HomeAssistant hass)
deprecated_show_progress
_FlowResultT async_external_step(self, *str|None step_id=None, str url, Mapping[str, str]|None description_placeholders=None)
None async_cancel_progress_task(self)
__progress_task
asyncio.Task[Any]|None async_get_progress_task(self)
bool show_advanced_options(self)
__no_progress_task_reported
vol.Schema add_suggested_values_to_schema(self, vol.Schema data_schema, Mapping[str, Any]|None suggested_values)
bool deprecated_show_progress
None async_set_progress_task(self, asyncio.Task[Any] progress_task)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
bool __no_progress_task_reported
_FlowResultT async_external_step_done(self, *str next_step_id)
_FlowResultT async_show_progress(self, *str|None step_id=None, str progress_action, Mapping[str, str]|None description_placeholders=None, asyncio.Task[Any]|None progress_task=None)
_FlowResultT async_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_show_progress_done(self, *str next_step_id)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_remove_flow_from_index(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow)
None _raise_if_step_does_not_exist(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow, str step_id)
list[_FlowResultT] async_progress_by_init_data_type(self, type init_data_type, Callable[[Any], bool] matcher, bool include_uninitialized=False)
_FlowResultT async_finish_flow(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow, _FlowResultT result)
list[_FlowResultT] async_progress_by_handler(self, _HandlerT handler, bool include_uninitialized=False, dict[str, Any]|None match_context=None)
FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] async_create_flow(self, _HandlerT handler_key, *_FlowContextT|None context=None, dict[str, Any]|None data=None)
None _async_remove_flow_progress(self, str flow_id)
None _async_setup_preview(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow)
hass
_FlowResultT async_get(self, str flow_id)
_FlowResultT async_init(self, _HandlerT handler, *_FlowContextT|None context=None, Any data=None)
_FlowResultT _async_configure(self, str flow_id, dict|None user_input=None)
None __init__(self, HomeAssistant hass)
_FlowResultT _async_handle_step(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow, str step_id, dict|BaseServiceInfo|None user_input)
list[_FlowResultT] async_progress(self, bool include_uninitialized=False)
list[FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]] _async_progress_by_handler(self, _HandlerT handler, dict[str, Any]|None match_context)
_FlowResultT async_configure(self, str flow_id, dict|None user_input=None)
None async_post_init(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow, _FlowResultT result)
None async_abort(self, str flow_id)
None _async_add_flow_progress(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow)
list[_FlowResultT] _async_flow_handler_to_flow_result(self, Iterable[FlowHandler[_FlowContextT, _FlowResultT, _HandlerT]] flows, bool include_uninitialized)
schema_errors
None __init__(self, str message, list[Hashable]|None path, str|None error_message, dict[str, Any] schema_errors, **Any kwargs)
Any __call__(self, Any value)
None __init__(self, vol.Schema schema, SectionConfig|None options=None)
schema
CONFIG_SCHEMA
bool add(self, _T matcher)
Definition: match.py:185
bool remove(self, _T matcher)
Definition: match.py:214
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None _map_error_to_schema_errors(dict[str, Any] schema_errors, vol.Invalid error, vol.Schema data_schema)
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356
None report_usage(str what, *str|None breaks_in_ha_version=None, ReportBehavior core_behavior=ReportBehavior.ERROR, ReportBehavior core_integration_behavior=ReportBehavior.LOG, ReportBehavior custom_integration_behavior=ReportBehavior.LOG, set[str]|None exclude_integrations=None, str|None integration_domain=None, int level=logging.WARNING)
Definition: frame.py:195
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1752