Home Assistant Unofficial Reference 2024.12.1
config_entries.py
Go to the documentation of this file.
1 """Manage config entries in Home Assistant."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import UserDict, defaultdict
7 from collections.abc import (
8  Callable,
9  Coroutine,
10  Generator,
11  Hashable,
12  Iterable,
13  Mapping,
14  ValuesView,
15 )
16 from contextvars import ContextVar
17 from copy import deepcopy
18 from datetime import datetime
19 from enum import Enum, StrEnum
20 import functools
21 from functools import cache
22 import logging
23 from random import randint
24 from types import MappingProxyType
25 from typing import TYPE_CHECKING, Any, Generic, Self, cast
26 
27 from async_interrupt import interrupt
28 from propcache import cached_property
29 from typing_extensions import TypeVar
30 import voluptuous as vol
31 
32 from . import data_entry_flow, loader
33 from .components import persistent_notification
34 from .const import (
35  CONF_NAME,
36  EVENT_HOMEASSISTANT_STARTED,
37  EVENT_HOMEASSISTANT_STOP,
38  Platform,
39 )
40 from .core import (
41  CALLBACK_TYPE,
42  DOMAIN as HOMEASSISTANT_DOMAIN,
43  CoreState,
44  Event,
45  HassJob,
46  HassJobType,
47  HomeAssistant,
48  callback,
49 )
50 from .data_entry_flow import FLOW_NOT_COMPLETE_STEPS, FlowContext, FlowResult
51 from .exceptions import (
52  ConfigEntryAuthFailed,
53  ConfigEntryError,
54  ConfigEntryNotReady,
55  HomeAssistantError,
56 )
57 from .helpers import (
58  device_registry as dr,
59  entity_registry as er,
60  issue_registry as ir,
61  storage,
62 )
63 from .helpers.debounce import Debouncer
64 from .helpers.discovery_flow import DiscoveryKey
65 from .helpers.dispatcher import SignalType, async_dispatcher_send_internal
66 from .helpers.event import (
67  RANDOM_MICROSECOND_MAX,
68  RANDOM_MICROSECOND_MIN,
69  async_call_later,
70 )
71 from .helpers.frame import ReportBehavior, report_usage
72 from .helpers.json import json_bytes, json_bytes_sorted, json_fragment
73 from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
74 from .loader import async_suggest_report_issue
75 from .setup import (
76  DATA_SETUP_DONE,
77  SetupPhases,
78  async_pause_setup,
79  async_process_deps_reqs,
80  async_setup_component,
81  async_start_setup,
82 )
83 from .util import ulid as ulid_util
84 from .util.async_ import create_eager_task
85 from .util.decorator import Registry
86 from .util.dt import utc_from_timestamp, utcnow
87 from .util.enum import try_parse_enum
88 
89 if TYPE_CHECKING:
90  from .components.bluetooth import BluetoothServiceInfoBleak
91  from .components.dhcp import DhcpServiceInfo
92  from .components.ssdp import SsdpServiceInfo
93  from .components.usb import UsbServiceInfo
94  from .components.zeroconf import ZeroconfServiceInfo
95  from .helpers.service_info.hassio import HassioServiceInfo
96  from .helpers.service_info.mqtt import MqttServiceInfo
97 
98 
99 _LOGGER = logging.getLogger(__name__)
100 
101 SOURCE_BLUETOOTH = "bluetooth"
102 SOURCE_DHCP = "dhcp"
103 SOURCE_DISCOVERY = "discovery"
104 SOURCE_HARDWARE = "hardware"
105 SOURCE_HASSIO = "hassio"
106 SOURCE_HOMEKIT = "homekit"
107 SOURCE_IMPORT = "import"
108 SOURCE_INTEGRATION_DISCOVERY = "integration_discovery"
109 SOURCE_MQTT = "mqtt"
110 SOURCE_SSDP = "ssdp"
111 SOURCE_SYSTEM = "system"
112 SOURCE_USB = "usb"
113 SOURCE_USER = "user"
114 SOURCE_ZEROCONF = "zeroconf"
115 
116 # If a user wants to hide a discovery from the UI they can "Ignore" it. The
117 # config_entries/ignore_flow websocket command creates a config entry with this
118 # source and while it exists normal discoveries with the same unique id are ignored.
119 SOURCE_IGNORE = "ignore"
120 
121 # This is used to signal that re-authentication is required by the user.
122 SOURCE_REAUTH = "reauth"
123 
124 # This is used to initiate a reconfigure flow by the user.
125 SOURCE_RECONFIGURE = "reconfigure"
126 
127 HANDLERS: Registry[str, type[ConfigFlow]] = Registry()
128 
129 STORAGE_KEY = "core.config_entries"
130 STORAGE_VERSION = 1
131 STORAGE_VERSION_MINOR = 4
132 
133 SAVE_DELAY = 1
134 
135 DISCOVERY_COOLDOWN = 1
136 
137 ISSUE_UNIQUE_ID_COLLISION = "config_entry_unique_id_collision"
138 UNIQUE_ID_COLLISION_TITLE_LIMIT = 5
139 
140 _DataT = TypeVar("_DataT", default=Any)
141 
142 
143 class ConfigEntryState(Enum):
144  """Config entry state."""
145 
146  LOADED = "loaded", True
147  """The config entry has been set up successfully"""
148  SETUP_ERROR = "setup_error", True
149  """There was an error while trying to set up this config entry"""
150  MIGRATION_ERROR = "migration_error", False
151  """There was an error while trying to migrate the config entry to a new version"""
152  SETUP_RETRY = "setup_retry", True
153  """The config entry was not ready to be set up yet, but might be later"""
154  NOT_LOADED = "not_loaded", True
155  """The config entry has not been loaded"""
156  FAILED_UNLOAD = "failed_unload", False
157  """An error occurred when trying to unload the entry"""
158  SETUP_IN_PROGRESS = "setup_in_progress", False
159  """The config entry is setting up."""
160 
161  _recoverable: bool
162 
163  def __new__(cls, value: str, recoverable: bool) -> Self:
164  """Create new ConfigEntryState."""
165  obj = object.__new__(cls)
166  obj._value_ = value
167  obj._recoverable = recoverable # noqa: SLF001
168  return obj
169 
170  @property
171  def recoverable(self) -> bool:
172  """Get if the state is recoverable.
173 
174  If the entry state is recoverable, unloads
175  and reloads are allowed.
176  """
177  return self._recoverable
178 
179 
180 DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id"
181 DISCOVERY_NOTIFICATION_ID = "config_entry_discovery"
182 DISCOVERY_SOURCES = {
183  SOURCE_BLUETOOTH,
184  SOURCE_DHCP,
185  SOURCE_DISCOVERY,
186  SOURCE_HARDWARE,
187  SOURCE_HASSIO,
188  SOURCE_HOMEKIT,
189  SOURCE_IMPORT,
190  SOURCE_INTEGRATION_DISCOVERY,
191  SOURCE_MQTT,
192  SOURCE_SSDP,
193  SOURCE_SYSTEM,
194  SOURCE_USB,
195  SOURCE_ZEROCONF,
196 }
197 
198 RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure"
199 
200 EVENT_FLOW_DISCOVERED = "config_entry_discovered"
201 
202 SIGNAL_CONFIG_ENTRY_CHANGED = SignalType["ConfigEntryChange", "ConfigEntry"](
203  "config_entry_changed"
204 )
205 
206 
207 @cache
209  discovery_domain: str,
210 ) -> SignalType[ConfigEntry]:
211  """Format signal."""
212  return SignalType(f"{discovery_domain}_discovered_config_entry_removed")
213 
214 
215 NO_RESET_TRIES_STATES = {
216  ConfigEntryState.SETUP_RETRY,
217  ConfigEntryState.SETUP_IN_PROGRESS,
218 }
219 
220 
221 class ConfigEntryChange(StrEnum):
222  """What was changed in a config entry."""
223 
224  ADDED = "added"
225  REMOVED = "removed"
226  UPDATED = "updated"
227 
228 
229 class ConfigEntryDisabler(StrEnum):
230  """What disabled a config entry."""
231 
232  USER = "user"
233 
234 
235 # DISABLED_* is deprecated, to be removed in 2022.3
236 DISABLED_USER = ConfigEntryDisabler.USER.value
237 
238 RELOAD_AFTER_UPDATE_DELAY = 30
239 
240 # Deprecated: Connection classes
241 # These aren't used anymore since 2021.6.0
242 # Mainly here not to break custom integrations.
243 CONN_CLASS_CLOUD_PUSH = "cloud_push"
244 CONN_CLASS_CLOUD_POLL = "cloud_poll"
245 CONN_CLASS_LOCAL_PUSH = "local_push"
246 CONN_CLASS_LOCAL_POLL = "local_poll"
247 CONN_CLASS_ASSUMED = "assumed"
248 CONN_CLASS_UNKNOWN = "unknown"
249 
250 
252  """Error while configuring an account."""
253 
254 
255 class UnknownEntry(ConfigError):
256  """Unknown entry specified."""
257 
258 
260  """Raised when a config entry operation is not allowed."""
261 
262 
263 type UpdateListenerType = Callable[
264  [HomeAssistant, ConfigEntry], Coroutine[Any, Any, None]
265 ]
266 
267 STATE_KEYS = {
268  "state",
269  "reason",
270  "error_reason_translation_key",
271  "error_reason_translation_placeholders",
272 }
273 FROZEN_CONFIG_ENTRY_ATTRS = {"entry_id", "domain", *STATE_KEYS}
274 UPDATE_ENTRY_CONFIG_ENTRY_ATTRS = {
275  "unique_id",
276  "title",
277  "data",
278  "options",
279  "pref_disable_new_entities",
280  "pref_disable_polling",
281  "minor_version",
282  "version",
283 }
284 
285 
287  """Typed context dict for config flow."""
288 
289  alternative_domain: str
290  configuration_url: str
291  confirm_only: bool
292  discovery_key: DiscoveryKey
293  entry_id: str
294  title_placeholders: Mapping[str, str]
295  unique_id: str | None
296 
297 
298 class ConfigFlowResult(FlowResult[ConfigFlowContext, str], total=False):
299  """Typed result dict for config flow."""
300 
301  minor_version: int
302  options: Mapping[str, Any]
303  version: int
304 
305 
306 def _validate_item(*, disabled_by: ConfigEntryDisabler | Any | None = None) -> None:
307  """Validate config entry item."""
308 
309  # Deprecated in 2022.1, stopped working in 2024.10
310  if disabled_by is not None and not isinstance(disabled_by, ConfigEntryDisabler):
311  raise TypeError(
312  f"disabled_by must be a ConfigEntryDisabler value, got {disabled_by}"
313  )
314 
315 
316 class ConfigEntry(Generic[_DataT]):
317  """Hold a configuration entry."""
318 
319  entry_id: str
320  domain: str
321  title: str
322  data: MappingProxyType[str, Any]
323  runtime_data: _DataT
324  options: MappingProxyType[str, Any]
325  unique_id: str | None
326  state: ConfigEntryState
327  reason: str | None
328  error_reason_translation_key: str | None
329  error_reason_translation_placeholders: dict[str, Any] | None
330  pref_disable_new_entities: bool
331  pref_disable_polling: bool
332  version: int
333  source: str
334  minor_version: int
335  disabled_by: ConfigEntryDisabler | None
336  supports_unload: bool | None
337  supports_remove_device: bool | None
338  _supports_options: bool | None
339  _supports_reconfigure: bool | None
340  update_listeners: list[UpdateListenerType]
341  _async_cancel_retry_setup: Callable[[], Any] | None
342  _on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None
343  setup_lock: asyncio.Lock
344  _reauth_lock: asyncio.Lock
345  _tasks: set[asyncio.Future[Any]]
346  _background_tasks: set[asyncio.Future[Any]]
347  _integration_for_domain: loader.Integration | None
348  _tries: int
349  created_at: datetime
350  modified_at: datetime
351  discovery_keys: MappingProxyType[str, tuple[DiscoveryKey, ...]]
352 
353  def __init__(
354  self,
355  *,
356  created_at: datetime | None = None,
357  data: Mapping[str, Any],
358  disabled_by: ConfigEntryDisabler | None = None,
359  discovery_keys: MappingProxyType[str, tuple[DiscoveryKey, ...]],
360  domain: str,
361  entry_id: str | None = None,
362  minor_version: int,
363  modified_at: datetime | None = None,
364  options: Mapping[str, Any] | None,
365  pref_disable_new_entities: bool | None = None,
366  pref_disable_polling: bool | None = None,
367  source: str,
368  state: ConfigEntryState = ConfigEntryState.NOT_LOADED,
369  title: str,
370  unique_id: str | None,
371  version: int,
372  ) -> None:
373  """Initialize a config entry."""
374  _setter = object.__setattr__
375  # Unique id of the config entry
376  _setter(self, "entry_id", entry_id or ulid_util.ulid_now())
377 
378  # Version of the configuration.
379  _setter(self, "version", version)
380  _setter(self, "minor_version", minor_version)
381 
382  # Domain the configuration belongs to
383  _setter(self, "domain", domain)
384 
385  # Title of the configuration
386  _setter(self, "title", title)
387 
388  # Config data
389  _setter(self, "data", MappingProxyType(data))
390 
391  # Entry options
392  _setter(self, "options", MappingProxyType(options or {}))
393 
394  # Entry system options
395  if pref_disable_new_entities is None:
396  pref_disable_new_entities = False
397 
398  _setter(self, "pref_disable_new_entities", pref_disable_new_entities)
399 
400  if pref_disable_polling is None:
401  pref_disable_polling = False
402 
403  _setter(self, "pref_disable_polling", pref_disable_polling)
404 
405  # Source of the configuration (user, discovery, cloud)
406  _setter(self, "source", source)
407 
408  # State of the entry (LOADED, NOT_LOADED)
409  _setter(self, "state", state)
410 
411  # Unique ID of this entry.
412  _setter(self, "unique_id", unique_id)
413 
414  # Config entry is disabled
415  _validate_item(disabled_by=disabled_by)
416  _setter(self, "disabled_by", disabled_by)
417 
418  # Supports unload
419  _setter(self, "supports_unload", None)
420 
421  # Supports remove device
422  _setter(self, "supports_remove_device", None)
423 
424  # Supports options
425  _setter(self, "_supports_options", None)
426 
427  # Supports reconfigure
428  _setter(self, "_supports_reconfigure", None)
429 
430  # Listeners to call on update
431  _setter(self, "update_listeners", [])
432 
433  # Reason why config entry is in a failed state
434  _setter(self, "reason", None)
435  _setter(self, "error_reason_translation_key", None)
436  _setter(self, "error_reason_translation_placeholders", None)
437 
438  # Function to cancel a scheduled retry
439  _setter(self, "_async_cancel_retry_setup", None)
440 
441  # Hold list for actions to call on unload.
442  _setter(self, "_on_unload", None)
443 
444  # Reload lock to prevent conflicting reloads
445  _setter(self, "setup_lock", asyncio.Lock())
446  # Reauth lock to prevent concurrent reauth flows
447  _setter(self, "_reauth_lock", asyncio.Lock())
448 
449  _setter(self, "_tasks", set())
450  _setter(self, "_background_tasks", set())
451 
452  _setter(self, "_integration_for_domain", None)
453  _setter(self, "_tries", 0)
454  _setter(self, "created_at", created_at or utcnow())
455  _setter(self, "modified_at", modified_at or utcnow())
456  _setter(self, "discovery_keys", discovery_keys)
457 
458  def __repr__(self) -> str:
459  """Representation of ConfigEntry."""
460  return (
461  f"<ConfigEntry entry_id={self.entry_id} version={self.version} domain={self.domain} "
462  f"title={self.title} state={self.state} unique_id={self.unique_id}>"
463  )
464 
465  def __setattr__(self, key: str, value: Any) -> None:
466  """Set an attribute."""
467  if key in UPDATE_ENTRY_CONFIG_ENTRY_ATTRS:
468  raise AttributeError(
469  f"{key} cannot be changed directly, use async_update_entry instead"
470  )
471  if key in FROZEN_CONFIG_ENTRY_ATTRS:
472  raise AttributeError(f"{key} cannot be changed")
473 
474  super().__setattr__(key, value)
475  self.clear_state_cacheclear_state_cache()
476  self.clear_storage_cacheclear_storage_cache()
477 
478  @property
479  def supports_options(self) -> bool:
480  """Return if entry supports config options."""
481  if self._supports_options is None and (handler := HANDLERS.get(self.domaindomain)):
482  # work out if handler has support for options flow
483  object.__setattr__(
484  self, "_supports_options", handler.async_supports_options_flow(self)
485  )
486  return self._supports_options or False
487 
488  @property
489  def supports_reconfigure(self) -> bool:
490  """Return if entry supports reconfigure step."""
491  if self._supports_reconfigure is None and (
492  handler := HANDLERS.get(self.domaindomain)
493  ):
494  # work out if handler has support for reconfigure step
495  object.__setattr__(
496  self,
497  "_supports_reconfigure",
498  hasattr(handler, "async_step_reconfigure"),
499  )
500  return self._supports_reconfigure or False
501 
502  def clear_state_cache(self) -> None:
503  """Clear cached properties that are included in as_json_fragment."""
504  self.__dict__.pop("as_json_fragment", None)
505 
506  @cached_property
507  def as_json_fragment(self) -> json_fragment:
508  """Return JSON fragment of a config entry that is used for the API."""
509  json_repr = {
510  "created_at": self.created_at.timestamp(),
511  "entry_id": self.entry_id,
512  "domain": self.domaindomain,
513  "modified_at": self.modified_at.timestamp(),
514  "title": self.title,
515  "source": self.sourcesource,
516  "state": self.statestate.value,
517  "supports_options": self.supports_optionssupports_options,
518  "supports_remove_device": self.supports_remove_devicesupports_remove_device or False,
519  "supports_unload": self.supports_unloadsupports_unload or False,
520  "supports_reconfigure": self.supports_reconfiguresupports_reconfigure,
521  "pref_disable_new_entities": self.pref_disable_new_entities,
522  "pref_disable_polling": self.pref_disable_polling,
523  "disabled_by": self.disabled_by,
524  "reason": self.reason,
525  "error_reason_translation_key": self.error_reason_translation_key,
526  "error_reason_translation_placeholders": self.error_reason_translation_placeholders,
527  }
528  return json_fragment(json_bytes(json_repr))
529 
530  def clear_storage_cache(self) -> None:
531  """Clear cached properties that are included in as_storage_fragment."""
532  self.__dict__.pop("as_storage_fragment", None)
533 
534  @cached_property
535  def as_storage_fragment(self) -> json_fragment:
536  """Return a storage fragment for this entry."""
537  return json_fragment(json_bytes_sorted(self.as_dictas_dict()))
538 
539  async def async_setup(
540  self,
541  hass: HomeAssistant,
542  *,
543  integration: loader.Integration | None = None,
544  ) -> None:
545  """Set up an entry."""
546  if self.sourcesource == SOURCE_IGNORE or self.disabled_by:
547  return
548 
549  current_entry.set(self)
550  try:
551  await self.__async_setup_with_context__async_setup_with_context(hass, integration)
552  finally:
553  current_entry.set(None)
554 
556  self,
557  hass: HomeAssistant,
558  integration: loader.Integration | None,
559  ) -> None:
560  """Set up an entry, with current_entry set."""
561  if integration is None and not (integration := self._integration_for_domain_integration_for_domain):
562  integration = await loader.async_get_integration(hass, self.domaindomain)
563  self._integration_for_domain_integration_for_domain = integration
564 
565  # Only store setup result as state if it was not forwarded.
566  if domain_is_integration := self.domaindomain == integration.domain:
567  if self.statestate in (
568  ConfigEntryState.LOADED,
569  ConfigEntryState.SETUP_IN_PROGRESS,
570  ):
571  raise OperationNotAllowed(
572  f"The config entry {self.title} ({self.domain}) with entry_id"
573  f" {self.entry_id} cannot be set up because it is already loaded "
574  f"in the {self.state} state"
575  )
576  if not self.setup_lock.locked():
577  raise OperationNotAllowed(
578  f"The config entry {self.title} ({self.domain}) with entry_id"
579  f" {self.entry_id} cannot be set up because it does not hold "
580  "the setup lock"
581  )
582  self._async_set_state_async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None)
583 
584  if self.supports_unloadsupports_unload is None:
585  self.supports_unloadsupports_unload = await support_entry_unload(hass, self.domaindomain)
586  if self.supports_remove_devicesupports_remove_device is None:
587  self.supports_remove_devicesupports_remove_device = await support_remove_from_device(
588  hass, self.domaindomain
589  )
590  try:
591  component = await integration.async_get_component()
592  except ImportError as err:
593  _LOGGER.error(
594  "Error importing integration %s to set up %s configuration entry: %s",
595  integration.domain,
596  self.domaindomain,
597  err,
598  )
599  if domain_is_integration:
600  self._async_set_state_async_set_state(
601  hass, ConfigEntryState.SETUP_ERROR, "Import error"
602  )
603  return
604 
605  if domain_is_integration:
606  try:
607  await integration.async_get_platform("config_flow")
608  except ImportError as err:
609  _LOGGER.error(
610  (
611  "Error importing platform config_flow from integration %s to"
612  " set up %s configuration entry: %s"
613  ),
614  integration.domain,
615  self.domaindomain,
616  err,
617  )
618  self._async_set_state_async_set_state(
619  hass, ConfigEntryState.SETUP_ERROR, "Import error"
620  )
621  return
622 
623  # Perform migration
624  if not await self.async_migrateasync_migrate(hass):
625  self._async_set_state_async_set_state(hass, ConfigEntryState.MIGRATION_ERROR, None)
626  return
627 
628  setup_phase = SetupPhases.CONFIG_ENTRY_SETUP
629  else:
630  setup_phase = SetupPhases.CONFIG_ENTRY_PLATFORM_SETUP
631 
632  error_reason = None
633  error_reason_translation_key = None
634  error_reason_translation_placeholders = None
635 
636  try:
637  with async_start_setup(
638  hass, integration=self.domaindomain, group=self.entry_id, phase=setup_phase
639  ):
640  result = await component.async_setup_entry(hass, self)
641 
642  if not isinstance(result, bool):
643  _LOGGER.error( # type: ignore[unreachable]
644  "%s.async_setup_entry did not return boolean", integration.domain
645  )
646  result = False
647  except ConfigEntryError as exc:
648  error_reason = str(exc) or "Unknown fatal config entry error"
649  error_reason_translation_key = exc.translation_key
650  error_reason_translation_placeholders = exc.translation_placeholders
651  _LOGGER.exception(
652  "Error setting up entry %s for %s: %s",
653  self.title,
654  self.domaindomain,
655  error_reason,
656  )
657  await self._async_process_on_unload_async_process_on_unload(hass)
658  result = False
659  except ConfigEntryAuthFailed as exc:
660  message = str(exc)
661  auth_base_message = "could not authenticate"
662  error_reason = message or auth_base_message
663  error_reason_translation_key = exc.translation_key
664  error_reason_translation_placeholders = exc.translation_placeholders
665  auth_message = (
666  f"{auth_base_message}: {message}" if message else auth_base_message
667  )
668  _LOGGER.warning(
669  "Config entry '%s' for %s integration %s",
670  self.title,
671  self.domaindomain,
672  auth_message,
673  )
674  await self._async_process_on_unload_async_process_on_unload(hass)
675  self.async_start_reauthasync_start_reauth(hass)
676  result = False
677  except ConfigEntryNotReady as exc:
678  message = str(exc)
679  error_reason_translation_key = exc.translation_key
680  error_reason_translation_placeholders = exc.translation_placeholders
681  self._async_set_state_async_set_state(
682  hass,
683  ConfigEntryState.SETUP_RETRY,
684  message or None,
685  error_reason_translation_key,
686  error_reason_translation_placeholders,
687  )
688  wait_time = 2 ** min(self._tries_tries, 4) * 5 + (
689  randint(RANDOM_MICROSECOND_MIN, RANDOM_MICROSECOND_MAX) / 1000000
690  )
691  self._tries_tries += 1
692  ready_message = f"ready yet: {message}" if message else "ready yet"
693  _LOGGER.debug(
694  (
695  "Config entry '%s' for %s integration not %s; Retrying in %d"
696  " seconds"
697  ),
698  self.title,
699  self.domaindomain,
700  ready_message,
701  wait_time,
702  )
703 
704  if hass.state is CoreState.running:
705  self._async_cancel_retry_setup_async_cancel_retry_setup = async_call_later(
706  hass,
707  wait_time,
708  HassJob(
709  functools.partial(self._async_setup_again_async_setup_again, hass),
710  job_type=HassJobType.Callback,
711  cancel_on_shutdown=True,
712  ),
713  )
714  else:
715  self._async_cancel_retry_setup_async_cancel_retry_setup = hass.bus.async_listen(
716  EVENT_HOMEASSISTANT_STARTED,
717  functools.partial(self._async_setup_again_async_setup_again, hass),
718  )
719 
720  await self._async_process_on_unload_async_process_on_unload(hass)
721  return
722  # pylint: disable-next=broad-except
723  except (asyncio.CancelledError, SystemExit, Exception):
724  _LOGGER.exception(
725  "Error setting up entry %s for %s", self.title, integration.domain
726  )
727  result = False
728 
729  #
730  # After successfully calling async_setup_entry, it is important that this function
731  # does not yield to the event loop by using `await` or `async with` or
732  # similar until after the state has been set by calling self._async_set_state.
733  #
734  # Otherwise we risk that any `call_soon`s
735  # created by an integration will be executed before the state is set.
736  #
737 
738  # Only store setup result as state if it was not forwarded.
739  if not domain_is_integration:
740  return
741 
742  self.async_cancel_retry_setupasync_cancel_retry_setup()
743 
744  if result:
745  self._async_set_state_async_set_state(hass, ConfigEntryState.LOADED, None)
746  else:
747  self._async_set_state_async_set_state(
748  hass,
749  ConfigEntryState.SETUP_ERROR,
750  error_reason,
751  error_reason_translation_key,
752  error_reason_translation_placeholders,
753  )
754 
755  @callback
756  def _async_setup_again(self, hass: HomeAssistant, *_: Any) -> None:
757  """Schedule setup again.
758 
759  This method is a callback to ensure that _async_cancel_retry_setup
760  is unset as soon as its callback is called.
761  """
762  self._async_cancel_retry_setup_async_cancel_retry_setup = None
763  # Check again when we fire in case shutdown
764  # has started so we do not block shutdown
765  if not hass.is_stopping:
766  hass.async_create_background_task(
767  self.async_setup_lockedasync_setup_locked(hass),
768  f"config entry retry {self.domain} {self.title}",
769  eager_start=True,
770  )
771 
773  self, hass: HomeAssistant, integration: loader.Integration | None = None
774  ) -> None:
775  """Set up while holding the setup lock."""
776  async with self.setup_lock:
777  if self.statestate is ConfigEntryState.LOADED:
778  # If something loaded the config entry while
779  # we were waiting for the lock, we should not
780  # set it up again.
781  _LOGGER.debug(
782  "Not setting up %s (%s %s) again, already loaded",
783  self.title,
784  self.domaindomain,
785  self.entry_id,
786  )
787  return
788  await self.async_setupasync_setup(hass, integration=integration)
789 
790  @callback
791  def async_shutdown(self) -> None:
792  """Call when Home Assistant is stopping."""
793  self.async_cancel_retry_setupasync_cancel_retry_setup()
794 
795  @callback
796  def async_cancel_retry_setup(self) -> None:
797  """Cancel retry setup."""
798  if self._async_cancel_retry_setup_async_cancel_retry_setup is not None:
799  self._async_cancel_retry_setup_async_cancel_retry_setup()
800  self._async_cancel_retry_setup_async_cancel_retry_setup = None
801 
802  async def async_unload(
803  self, hass: HomeAssistant, *, integration: loader.Integration | None = None
804  ) -> bool:
805  """Unload an entry.
806 
807  Returns if unload is possible and was successful.
808  """
809  if self.sourcesource == SOURCE_IGNORE:
810  self._async_set_state_async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
811  return True
812 
813  if self.statestate == ConfigEntryState.NOT_LOADED:
814  return True
815 
816  if not integration and (integration := self._integration_for_domain_integration_for_domain) is None:
817  try:
818  integration = await loader.async_get_integration(hass, self.domaindomain)
820  # The integration was likely a custom_component
821  # that was uninstalled, or an integration
822  # that has been renamed without removing the config
823  # entry.
824  self._async_set_state_async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
825  return True
826 
827  component = await integration.async_get_component()
828 
829  if domain_is_integration := self.domaindomain == integration.domain:
830  if not self.setup_lock.locked():
831  raise OperationNotAllowed(
832  f"The config entry {self.title} ({self.domain}) with entry_id"
833  f" {self.entry_id} cannot be unloaded because it does not hold "
834  "the setup lock"
835  )
836 
837  if not self.statestate.recoverable:
838  return False
839 
840  if self.statestate is not ConfigEntryState.LOADED:
841  self.async_cancel_retry_setupasync_cancel_retry_setup()
842  self._async_set_state_async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
843  return True
844 
845  supports_unload = hasattr(component, "async_unload_entry")
846 
847  if not supports_unload:
848  if domain_is_integration:
849  self._async_set_state_async_set_state(
850  hass, ConfigEntryState.FAILED_UNLOAD, "Unload not supported"
851  )
852  return False
853 
854  try:
855  result = await component.async_unload_entry(hass, self)
856 
857  assert isinstance(result, bool)
858 
859  # Only adjust state if we unloaded the component
860  if domain_is_integration and result:
861  await self._async_process_on_unload_async_process_on_unload(hass)
862  if hasattr(self, "runtime_data"):
863  object.__delattr__(self, "runtime_data")
864 
865  self._async_set_state_async_set_state(hass, ConfigEntryState.NOT_LOADED, None)
866 
867  except Exception as exc:
868  _LOGGER.exception(
869  "Error unloading entry %s for %s", self.title, integration.domain
870  )
871  if domain_is_integration:
872  self._async_set_state_async_set_state(
873  hass, ConfigEntryState.FAILED_UNLOAD, str(exc) or "Unknown error"
874  )
875  return False
876  return result
877 
878  async def async_remove(self, hass: HomeAssistant) -> None:
879  """Invoke remove callback on component."""
880  old_modified_at = self.modified_at
881  object.__setattr__(self, "modified_at", utcnow())
882  self.clear_state_cacheclear_state_cache()
883  self.clear_storage_cacheclear_storage_cache()
884 
885  if self.sourcesource == SOURCE_IGNORE:
886  return
887 
888  if not self.setup_lock.locked():
889  raise OperationNotAllowed(
890  f"The config entry {self.title} ({self.domain}) with entry_id"
891  f" {self.entry_id} cannot be removed because it does not hold "
892  "the setup lock"
893  )
894 
895  if not (integration := self._integration_for_domain_integration_for_domain):
896  try:
897  integration = await loader.async_get_integration(hass, self.domaindomain)
899  # The integration was likely a custom_component
900  # that was uninstalled, or an integration
901  # that has been renamed without removing the config
902  # entry.
903  return
904 
905  component = await integration.async_get_component()
906  if not hasattr(component, "async_remove_entry"):
907  return
908  try:
909  await component.async_remove_entry(hass, self)
910  except Exception:
911  _LOGGER.exception(
912  "Error calling entry remove callback %s for %s",
913  self.title,
914  integration.domain,
915  )
916  # Restore modified_at
917  object.__setattr__(self, "modified_at", old_modified_at)
918 
919  @callback
921  self,
922  hass: HomeAssistant,
923  state: ConfigEntryState,
924  reason: str | None,
925  error_reason_translation_key: str | None = None,
926  error_reason_translation_placeholders: dict[str, str] | None = None,
927  ) -> None:
928  """Set the state of the config entry."""
929  if state not in NO_RESET_TRIES_STATES:
930  self._tries_tries = 0
931  _setter = object.__setattr__
932  _setter(self, "state", state)
933  _setter(self, "reason", reason)
934  _setter(self, "error_reason_translation_key", error_reason_translation_key)
935  _setter(
936  self,
937  "error_reason_translation_placeholders",
938  error_reason_translation_placeholders,
939  )
940  self.clear_state_cacheclear_state_cache()
941  # Storage cache is not cleared here because the state is not stored
942  # in storage and we do not want to clear the cache on every state change
943  # since state changes are frequent.
944  async_dispatcher_send_internal(
945  hass, SIGNAL_CONFIG_ENTRY_CHANGED, ConfigEntryChange.UPDATED, self
946  )
947 
948  async def async_migrate(self, hass: HomeAssistant) -> bool:
949  """Migrate an entry.
950 
951  Returns True if config entry is up-to-date or has been migrated.
952  """
953  if (handler := HANDLERS.get(self.domaindomain)) is None:
954  _LOGGER.error(
955  "Flow handler not found for entry %s for %s", self.title, self.domaindomain
956  )
957  return False
958  # Handler may be a partial
959  # Keep for backwards compatibility
960  # https://github.com/home-assistant/core/pull/67087#discussion_r812559950
961  while isinstance(handler, functools.partial):
962  handler = handler.func # type: ignore[unreachable]
963 
964  same_major_version = self.versionversion == handler.VERSION
965  if same_major_version and self.minor_versionminor_version == handler.MINOR_VERSION:
966  return True
967 
968  if not (integration := self._integration_for_domain_integration_for_domain):
969  integration = await loader.async_get_integration(hass, self.domaindomain)
970  component = await integration.async_get_component()
971  supports_migrate = hasattr(component, "async_migrate_entry")
972  if not supports_migrate:
973  if same_major_version:
974  return True
975  _LOGGER.error(
976  "Migration handler not found for entry %s for %s",
977  self.title,
978  self.domaindomain,
979  )
980  return False
981 
982  try:
983  result = await component.async_migrate_entry(hass, self)
984  if not isinstance(result, bool):
985  _LOGGER.error( # type: ignore[unreachable]
986  "%s.async_migrate_entry did not return boolean", self.domaindomain
987  )
988  return False
989  if result:
990  hass.config_entries._async_schedule_save() # noqa: SLF001
991  except Exception:
992  _LOGGER.exception(
993  "Error migrating entry %s for %s", self.title, self.domaindomain
994  )
995  return False
996  return result
997 
998  def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE:
999  """Listen for when entry is updated.
1000 
1001  Returns function to unlisten.
1002  """
1003  self.update_listeners.append(listener)
1004  return lambda: self.update_listeners.remove(listener)
1005 
1006  def as_dict(self) -> dict[str, Any]:
1007  """Return dictionary version of this entry."""
1008  return {
1009  "created_at": self.created_at.isoformat(),
1010  "data": dict(self.data),
1011  "discovery_keys": dict(self.discovery_keys),
1012  "disabled_by": self.disabled_by,
1013  "domain": self.domaindomain,
1014  "entry_id": self.entry_id,
1015  "minor_version": self.minor_versionminor_version,
1016  "modified_at": self.modified_at.isoformat(),
1017  "options": dict(self.options),
1018  "pref_disable_new_entities": self.pref_disable_new_entities,
1019  "pref_disable_polling": self.pref_disable_polling,
1020  "source": self.sourcesource,
1021  "title": self.title,
1022  "unique_id": self.unique_id,
1023  "version": self.versionversion,
1024  }
1025 
1026  @callback
1028  self, func: Callable[[], Coroutine[Any, Any, None] | None]
1029  ) -> None:
1030  """Add a function to call when config entry is unloaded."""
1031  if self._on_unload_on_unload is None:
1032  self._on_unload_on_unload = []
1033  self._on_unload_on_unload.append(func)
1034 
1035  async def _async_process_on_unload(self, hass: HomeAssistant) -> None:
1036  """Process the on_unload callbacks and wait for pending tasks."""
1037  if self._on_unload_on_unload is not None:
1038  while self._on_unload_on_unload:
1039  if job := self._on_unload_on_unload.pop()():
1040  self.async_create_task(hass, job, eager_start=True)
1041 
1042  if not self._tasks and not self._background_tasks:
1043  return
1044 
1045  cancel_message = f"Config entry {self.title} with {self.domain} unloading"
1046  for task in self._background_tasks:
1047  task.cancel(cancel_message)
1048 
1049  _, pending = await asyncio.wait(
1050  [*self._tasks, *self._background_tasks], timeout=10
1051  )
1052 
1053  for task in pending:
1054  _LOGGER.warning(
1055  "Unloading %s (%s) config entry. Task %s did not complete in time",
1056  self.title,
1057  self.domaindomain,
1058  task,
1059  )
1060 
1061  @callback
1063  self,
1064  hass: HomeAssistant,
1065  context: ConfigFlowContext | None = None,
1066  data: dict[str, Any] | None = None,
1067  ) -> None:
1068  """Start a reauth flow."""
1069  # We will check this again in the task when we hold the lock,
1070  # but we also check it now to try to avoid creating the task.
1071  if any(self.async_get_active_flowsasync_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})):
1072  # Reauth or Reconfigure flow already in progress for this entry
1073  return
1074  hass.async_create_task(
1075  self._async_init_reauth_async_init_reauth(hass, context, data),
1076  f"config entry reauth {self.title} {self.domain} {self.entry_id}",
1077  eager_start=True,
1078  )
1079 
1081  self,
1082  hass: HomeAssistant,
1083  context: ConfigFlowContext | None = None,
1084  data: dict[str, Any] | None = None,
1085  ) -> None:
1086  """Start a reauth flow."""
1087  async with self._reauth_lock:
1088  if any(
1089  self.async_get_active_flowsasync_get_active_flows(hass, {SOURCE_RECONFIGURE, SOURCE_REAUTH})
1090  ):
1091  # Reauth or Reconfigure flow already in progress for this entry
1092  return
1093  result = await hass.config_entries.flow.async_init(
1094  self.domaindomain,
1095  context=ConfigFlowContext(
1096  source=SOURCE_REAUTH,
1097  entry_id=self.entry_id,
1098  title_placeholders={"name": self.title},
1099  unique_id=self.unique_id,
1100  )
1101  | (context or {}),
1102  data=self.data | (data or {}),
1103  )
1104  if result["type"] not in FLOW_NOT_COMPLETE_STEPS:
1105  return
1106 
1107  # Create an issue, there's no need to hold the lock when doing that
1108  issue_id = f"config_entry_reauth_{self.domain}_{self.entry_id}"
1109  ir.async_create_issue(
1110  hass,
1111  HOMEASSISTANT_DOMAIN,
1112  issue_id,
1113  data={"flow_id": result["flow_id"]},
1114  is_fixable=False,
1115  issue_domain=self.domaindomain,
1116  severity=ir.IssueSeverity.ERROR,
1117  translation_key="config_entry_reauth",
1118  translation_placeholders={"name": self.title},
1119  )
1120 
1121  @callback
1123  self, hass: HomeAssistant, sources: set[str]
1124  ) -> Generator[ConfigFlowResult]:
1125  """Get any active flows of certain sources for this entry."""
1126  return (
1127  flow
1128  for flow in hass.config_entries.flow.async_progress_by_handler(
1129  self.domaindomain,
1130  match_context={"entry_id": self.entry_id},
1131  include_uninitialized=True,
1132  )
1133  if flow["context"].get("source") in sources
1134  )
1135 
1136  @callback
1137  def async_create_task[_R](
1138  self,
1139  hass: HomeAssistant,
1140  target: Coroutine[Any, Any, _R],
1141  name: str | None = None,
1142  eager_start: bool = True,
1143  ) -> asyncio.Task[_R]:
1144  """Create a task from within the event loop.
1145 
1146  This method must be run in the event loop.
1147 
1148  target: target to call.
1149  """
1150  task = hass.async_create_task_internal(
1151  target, f"{name} {self.title} {self.domain} {self.entry_id}", eager_start
1152  )
1153  if eager_start and task.done():
1154  return task
1155  self._tasks.add(task)
1156  task.add_done_callback(self._tasks.remove)
1157 
1158  return task
1159 
1160  @callback
1161  def async_create_background_task[_R](
1162  self,
1163  hass: HomeAssistant,
1164  target: Coroutine[Any, Any, _R],
1165  name: str,
1166  eager_start: bool = True,
1167  ) -> asyncio.Task[_R]:
1168  """Create a background task tied to the config entry lifecycle.
1169 
1170  Background tasks are automatically canceled when config entry is unloaded.
1171 
1172  A background task is different from a normal task:
1173 
1174  - Will not block startup
1175  - Will be automatically cancelled on shutdown
1176  - Calls to async_block_till_done will not wait for completion
1177 
1178  This method must be run in the event loop.
1179  """
1180  task = hass.async_create_background_task(target, name, eager_start)
1181  if task.done():
1182  return task
1183  self._background_tasks.add(task)
1184  task.add_done_callback(self._background_tasks.remove)
1185  return task
1186 
1187 
1188 current_entry: ContextVar[ConfigEntry | None] = ContextVar(
1189  "current_entry", default=None
1190 )
1191 
1192 
1193 class FlowCancelledError(Exception):
1194  """Error to indicate that a flow has been cancelled."""
1195 
1196 
1197 def _report_non_awaited_platform_forwards(entry: ConfigEntry, what: str) -> None:
1198  """Report non awaited platform forwards."""
1199  report_usage(
1200  f"calls {what} for integration {entry.domain} with "
1201  f"title: {entry.title} and entry_id: {entry.entry_id}, "
1202  f"during setup without awaiting {what}, which can cause "
1203  "the setup lock to be released before the setup is done",
1204  core_behavior=ReportBehavior.LOG,
1205  breaks_in_ha_version="2025.1",
1206  )
1207 
1208 
1210  data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult]
1211 ):
1212  """Manage all the config entry flows that are in progress."""
1213 
1214  _flow_result = ConfigFlowResult
1215 
1217  self,
1218  hass: HomeAssistant,
1219  config_entries: ConfigEntries,
1220  hass_config: ConfigType,
1221  ) -> None:
1222  """Initialize the config entry flow manager."""
1223  super().__init__(hass)
1224  self.config_entriesconfig_entries = config_entries
1225  self._hass_config_hass_config = hass_config
1226  self._pending_import_flows: defaultdict[
1227  str, dict[str, asyncio.Future[None]]
1228  ] = defaultdict(dict)
1229  self._initialize_futures: defaultdict[str, set[asyncio.Future[None]]] = (
1230  defaultdict(set)
1231  )
1232  self._discovery_debouncer_discovery_debouncer = Debouncer[None](
1233  hass,
1234  _LOGGER,
1235  cooldown=DISCOVERY_COOLDOWN,
1236  immediate=True,
1237  function=self._async_discovery_async_discovery,
1238  background=True,
1239  )
1240 
1241  async def async_wait_import_flow_initialized(self, handler: str) -> None:
1242  """Wait till all import flows in progress are initialized."""
1243  if not (current := self._pending_import_flows.get(handler)):
1244  return
1245 
1246  await asyncio.wait(current.values())
1247 
1248  @callback
1249  def _async_has_other_discovery_flows(self, flow_id: str) -> bool:
1250  """Check if there are any other discovery flows in progress."""
1251  for flow in self._progress.values():
1252  if flow.flow_id != flow_id and flow.context["source"] in DISCOVERY_SOURCES:
1253  return True
1254  return False
1255 
1256  async def async_init(
1257  self,
1258  handler: str,
1259  *,
1260  context: ConfigFlowContext | None = None,
1261  data: Any = None,
1262  ) -> ConfigFlowResult:
1263  """Start a configuration flow."""
1264  if not context or "source" not in context:
1265  raise KeyError("Context not set or doesn't have a source set")
1266 
1267  # reauth/reconfigure flows should be linked to a config entry
1268  if (source := context["source"]) in {
1269  SOURCE_REAUTH,
1270  SOURCE_RECONFIGURE,
1271  } and "entry_id" not in context:
1272  # Deprecated in 2024.12, should fail in 2025.12
1273  report_usage(
1274  f"initialises a {source} flow without a link to the config entry",
1275  breaks_in_ha_version="2025.12",
1276  )
1277 
1278  flow_id = ulid_util.ulid_now()
1279 
1280  # Avoid starting a config flow on an integration that only supports
1281  # a single config entry, but which already has an entry
1282  if (
1283  source not in {SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE}
1284  and (
1285  self.config_entriesconfig_entries.async_has_entries(handler, include_ignore=False)
1286  or (
1287  self.config_entriesconfig_entries.async_has_entries(handler, include_ignore=True)
1288  and source != SOURCE_USER
1289  )
1290  )
1291  and await _support_single_config_entry_only(self.hasshass, handler)
1292  ):
1293  return ConfigFlowResult(
1294  type=data_entry_flow.FlowResultType.ABORT,
1295  flow_id=flow_id,
1296  handler=handler,
1297  reason="single_instance_allowed",
1298  translation_domain=HOMEASSISTANT_DOMAIN,
1299  )
1300 
1301  loop = self.hasshass.loop
1302 
1303  if source == SOURCE_IMPORT:
1304  self._pending_import_flows[handler][flow_id] = loop.create_future()
1305 
1306  cancel_init_future = loop.create_future()
1307  handler_init_futures = self._initialize_futures[handler]
1308  handler_init_futures.add(cancel_init_future)
1309  try:
1310  async with interrupt(
1311  cancel_init_future,
1312  FlowCancelledError,
1313  "Config entry initialize canceled: Home Assistant is shutting down",
1314  ):
1315  flow, result = await self._async_init_async_init(flow_id, handler, context, data)
1316  except FlowCancelledError as ex:
1317  raise asyncio.CancelledError from ex
1318  finally:
1319  handler_init_futures.remove(cancel_init_future)
1320  if not handler_init_futures:
1321  del self._initialize_futures[handler]
1322  if handler in self._pending_import_flows:
1323  self._pending_import_flows[handler].pop(flow_id, None)
1324  if not self._pending_import_flows[handler]:
1325  del self._pending_import_flows[handler]
1326 
1327  if result["type"] != data_entry_flow.FlowResultType.ABORT:
1328  await self.async_post_initasync_post_initasync_post_init(flow, result)
1329 
1330  return result
1331 
1332  async def _async_init(
1333  self,
1334  flow_id: str,
1335  handler: str,
1336  context: ConfigFlowContext,
1337  data: Any,
1338  ) -> tuple[ConfigFlow, ConfigFlowResult]:
1339  """Run the init in a task to allow it to be canceled at shutdown."""
1340  flow = await self.async_create_flowasync_create_flowasync_create_flow(handler, context=context, data=data)
1341  if not flow:
1342  raise data_entry_flow.UnknownFlow("Flow was not created")
1343  flow.hass = self.hasshass
1344  flow.handler = handler
1345  flow.flow_id = flow_id
1346  flow.context = context
1347  flow.init_data = data
1348  self._async_add_flow_progress_async_add_flow_progress(flow)
1349  try:
1350  result = await self._async_handle_step_async_handle_step(flow, flow.init_step, data)
1351  finally:
1352  self._set_pending_import_done_set_pending_import_done(flow)
1353  return flow, result
1354 
1355  def _set_pending_import_done(self, flow: ConfigFlow) -> None:
1356  """Set pending import flow as done."""
1357  if (
1358  (handler_import_flows := self._pending_import_flows.get(flow.handler))
1359  and (init_done := handler_import_flows.get(flow.flow_id))
1360  and not init_done.done()
1361  ):
1362  init_done.set_result(None)
1363 
1364  @callback
1365  def async_shutdown(self) -> None:
1366  """Cancel any initializing flows."""
1367  for future_list in self._initialize_futures.values():
1368  for future in future_list:
1369  future.set_result(None)
1370  self._discovery_debouncer_discovery_debouncer.async_shutdown()
1371 
1373  self,
1374  flow: data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult],
1375  result: ConfigFlowResult,
1376  ) -> ConfigFlowResult:
1377  """Finish a config flow and add an entry.
1378 
1379  This method is called when a flow step returns FlowResultType.ABORT or
1380  FlowResultType.CREATE_ENTRY.
1381  """
1382  flow = cast(ConfigFlow, flow)
1383 
1384  # Mark the step as done.
1385  # We do this to avoid a circular dependency where async_finish_flow sets up a
1386  # new entry, which needs the integration to be set up, which is waiting for
1387  # init to be done.
1388  self._set_pending_import_done_set_pending_import_done(flow)
1389 
1390  # Remove notification if no other discovery config entries in progress
1391  if not self._async_has_other_discovery_flows_async_has_other_discovery_flows(flow.flow_id):
1392  persistent_notification.async_dismiss(self.hasshass, DISCOVERY_NOTIFICATION_ID)
1393 
1394  # Clean up issue if this is a reauth flow
1395  if flow.context["source"] == SOURCE_REAUTH:
1396  if (entry_id := flow.context.get("entry_id")) is not None and (
1397  entry := self.config_entriesconfig_entries.async_get_entry(entry_id)
1398  ) is not None:
1399  issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}"
1400  ir.async_delete_issue(self.hasshass, HOMEASSISTANT_DOMAIN, issue_id)
1401 
1402  if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
1403  # If there's an ignored config entry with a matching unique ID,
1404  # update the discovery key.
1405  if (
1406  (discovery_key := flow.context.get("discovery_key"))
1407  and (unique_id := flow.unique_id) is not None
1408  and (
1409  entry := self.config_entriesconfig_entries.async_entry_for_domain_unique_id(
1410  result["handler"], unique_id
1411  )
1412  )
1413  and discovery_key
1414  not in (
1415  known_discovery_keys := entry.discovery_keys.get(
1416  discovery_key.domain, ()
1417  )
1418  )
1419  ):
1420  new_discovery_keys = MappingProxyType(
1421  entry.discovery_keys
1422  | {
1423  discovery_key.domain: tuple(
1424  [*known_discovery_keys, discovery_key][-10:]
1425  )
1426  }
1427  )
1428  _LOGGER.debug(
1429  "Updating discovery keys for %s entry %s %s -> %s",
1430  entry.domain,
1431  unique_id,
1432  entry.discovery_keys,
1433  new_discovery_keys,
1434  )
1435  self.config_entriesconfig_entries.async_update_entry(
1436  entry, discovery_keys=new_discovery_keys
1437  )
1438  return result
1439 
1440  # Avoid adding a config entry for a integration
1441  # that only supports a single config entry, but already has an entry
1442  if (
1443  self.config_entriesconfig_entries.async_has_entries(flow.handler, include_ignore=False)
1444  and await _support_single_config_entry_only(self.hasshass, flow.handler)
1445  and flow.context["source"] != SOURCE_IGNORE
1446  ):
1447  return ConfigFlowResult(
1448  type=data_entry_flow.FlowResultType.ABORT,
1449  flow_id=flow.flow_id,
1450  handler=flow.handler,
1451  reason="single_instance_allowed",
1452  translation_domain=HOMEASSISTANT_DOMAIN,
1453  )
1454 
1455  # Check if config entry exists with unique ID. Unload it.
1456  existing_entry = None
1457 
1458  # Abort all flows in progress with same unique ID
1459  # or the default discovery ID
1460  for progress_flow in self.async_progress_by_handlerasync_progress_by_handler(flow.handler):
1461  progress_unique_id = progress_flow["context"].get("unique_id")
1462  progress_flow_id = progress_flow["flow_id"]
1463 
1464  if progress_flow_id != flow.flow_id and (
1465  (flow.unique_id and progress_unique_id == flow.unique_id)
1466  or progress_unique_id == DEFAULT_DISCOVERY_UNIQUE_ID
1467  ):
1468  self.async_abortasync_abort(progress_flow_id)
1469  continue
1470 
1471  # Abort any flows in progress for the same handler
1472  # when integration allows only one config entry
1473  if (
1474  progress_flow_id != flow.flow_id
1475  and await _support_single_config_entry_only(self.hasshass, flow.handler)
1476  ):
1477  self.async_abortasync_abort(progress_flow_id)
1478 
1479  if flow.unique_id is not None:
1480  # Reset unique ID when the default discovery ID has been used
1481  if flow.unique_id == DEFAULT_DISCOVERY_UNIQUE_ID:
1482  await flow.async_set_unique_id(None)
1483 
1484  # Find existing entry.
1485  existing_entry = self.config_entriesconfig_entries.async_entry_for_domain_unique_id(
1486  result["handler"], flow.unique_id
1487  )
1488 
1489  # Unload the entry before setting up the new one.
1490  if existing_entry is not None and existing_entry.state.recoverable:
1491  await self.config_entriesconfig_entries.async_unload(existing_entry.entry_id)
1492 
1493  discovery_key = flow.context.get("discovery_key")
1494  discovery_keys = (
1495  MappingProxyType({discovery_key.domain: (discovery_key,)})
1496  if discovery_key
1497  else MappingProxyType({})
1498  )
1499  entry = ConfigEntry(
1500  data=result["data"],
1501  discovery_keys=discovery_keys,
1502  domain=result["handler"],
1503  minor_version=result["minor_version"],
1504  options=result["options"],
1505  source=flow.context["source"],
1506  title=result["title"],
1507  unique_id=flow.unique_id,
1508  version=result["version"],
1509  )
1510 
1511  if existing_entry is not None:
1512  # Unload and remove the existing entry, but don't clean up devices and
1513  # entities until the new entry is added
1514  await self.config_entriesconfig_entries._async_remove(existing_entry.entry_id) # noqa: SLF001
1515  await self.config_entriesconfig_entries.async_add(entry)
1516 
1517  if existing_entry is not None:
1518  # Clean up devices and entities belonging to the existing entry
1519  # which are not present in the new entry
1520  self.config_entriesconfig_entries._async_clean_up(existing_entry) # noqa: SLF001
1521 
1522  result["result"] = entry
1523  return result
1524 
1526  self,
1527  handler_key: str,
1528  *,
1529  context: ConfigFlowContext | None = None,
1530  data: Any = None,
1531  ) -> ConfigFlow:
1532  """Create a flow for specified handler.
1533 
1534  Handler key is the domain of the component that we want to set up.
1535  """
1536  handler = await _async_get_flow_handler(
1537  self.hasshass, handler_key, self._hass_config_hass_config
1538  )
1539  if not context or "source" not in context:
1540  raise KeyError("Context not set or doesn't have a source set")
1541 
1542  flow = handler()
1543  flow.init_step = context["source"]
1544  return flow
1545 
1546  async def async_post_init(
1547  self,
1548  flow: data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult],
1549  result: ConfigFlowResult,
1550  ) -> None:
1551  """After a flow is initialised trigger new flow notifications."""
1552  source = flow.context["source"]
1553 
1554  # Create notification.
1555  if source in DISCOVERY_SOURCES:
1556  await self._discovery_debouncer_discovery_debouncer.async_call()
1557  elif source == SOURCE_REAUTH:
1558  persistent_notification.async_create(
1559  self.hasshass,
1560  title="Integration requires reconfiguration",
1561  message=(
1562  "At least one of your integrations requires reconfiguration to "
1563  "continue functioning. [Check it out](/config/integrations)."
1564  ),
1565  notification_id=RECONFIGURE_NOTIFICATION_ID,
1566  )
1567 
1568  @callback
1569  def _async_discovery(self) -> None:
1570  """Handle discovery."""
1571  # async_fire_internal is used here because this is only
1572  # called from the Debouncer so we know the usage is safe
1573  self.hasshass.bus.async_fire_internal(EVENT_FLOW_DISCOVERED)
1574  persistent_notification.async_create(
1575  self.hasshass,
1576  title="New devices discovered",
1577  message=(
1578  "We have discovered new devices on your network. "
1579  "[Check it out](/config/integrations)."
1580  ),
1581  notification_id=DISCOVERY_NOTIFICATION_ID,
1582  )
1583 
1584  @callback
1586  self, handler: str, match_context: ConfigFlowContext, data: Any
1587  ) -> bool:
1588  """Check if an existing matching discovery flow is in progress.
1589 
1590  A flow with the same handler, context, and data.
1591 
1592  If match_context is passed, only return flows with a context that is a
1593  superset of match_context.
1594  """
1595  if not (flows := self._handler_progress_index.get(handler)):
1596  return False
1597  match_items = match_context.items()
1598  for progress in flows:
1599  if match_items <= progress.context.items() and progress.init_data == data:
1600  return True
1601  return False
1602 
1603  @callback
1604  def async_has_matching_flow(self, flow: ConfigFlow) -> bool:
1605  """Check if an existing matching flow is in progress."""
1606  if not (flows := self._handler_progress_index.get(flow.handler)):
1607  return False
1608  for other_flow in set(flows):
1609  if other_flow is not flow and flow.is_matching(other_flow): # type: ignore[arg-type]
1610  return True
1611  return False
1612 
1613 
1614 class ConfigEntryItems(UserDict[str, ConfigEntry]):
1615  """Container for config items, maps config_entry_id -> entry.
1616 
1617  Maintains two additional indexes:
1618  - domain -> list[ConfigEntry]
1619  - domain -> unique_id -> ConfigEntry
1620  """
1621 
1622  def __init__(self, hass: HomeAssistant) -> None:
1623  """Initialize the container."""
1624  super().__init__()
1625  self._hass_hass = hass
1626  self._domain_index: dict[str, list[ConfigEntry]] = {}
1627  self._domain_unique_id_index: dict[str, dict[str, list[ConfigEntry]]] = {}
1628 
1629  def values(self) -> ValuesView[ConfigEntry]:
1630  """Return the underlying values to avoid __iter__ overhead."""
1631  return self.data.values()
1632 
1633  def __setitem__(self, entry_id: str, entry: ConfigEntry) -> None:
1634  """Add an item."""
1635  data = self.data
1636  self.check_unique_idcheck_unique_id(entry)
1637  if entry_id in data:
1638  # This is likely a bug in a test that is adding the same entry twice.
1639  # In the future, once we have fixed the tests, this will raise HomeAssistantError.
1640  _LOGGER.error("An entry with the id %s already exists", entry_id)
1641  self._unindex_entry_unindex_entry(entry_id)
1642  data[entry_id] = entry
1643  self._index_entry_index_entry(entry)
1644 
1645  def check_unique_id(self, entry: ConfigEntry) -> None:
1646  """Check config entry unique id.
1647 
1648  For a string unique id (this is the correct case): return
1649  For a hashable non string unique id: log warning
1650  For a non-hashable unique id: raise error
1651  """
1652  if (unique_id := entry.unique_id) is None:
1653  return
1654  if isinstance(unique_id, str):
1655  # Unique id should be a string
1656  return
1657  if isinstance(unique_id, Hashable): # type: ignore[unreachable]
1658  # Checks for other non-string was added in HA Core 2024.10
1659  # In HA Core 2025.10, we should remove the error and instead fail
1660  report_issue = async_suggest_report_issue(
1661  self._hass_hass, integration_domain=entry.domain
1662  )
1663  _LOGGER.error(
1664  (
1665  "Config entry '%s' from integration %s has an invalid unique_id"
1666  " '%s' of type %s when a string is expected, please %s"
1667  ),
1668  entry.title,
1669  entry.domain,
1670  entry.unique_id,
1671  type(entry.unique_id).__name__,
1672  report_issue,
1673  )
1674  else:
1675  # Guard against integrations using unhashable unique_id
1676  # In HA Core 2024.11, the guard was changed from warning to failing
1677  raise HomeAssistantError(
1678  f"The entry unique id {unique_id} is not a string."
1679  )
1680 
1681  def _index_entry(self, entry: ConfigEntry) -> None:
1682  """Index an entry."""
1683  self.check_unique_idcheck_unique_id(entry)
1684  self._domain_index.setdefault(entry.domain, []).append(entry)
1685  if entry.unique_id is not None:
1686  self._domain_unique_id_index.setdefault(entry.domain, {}).setdefault(
1687  entry.unique_id, []
1688  ).append(entry)
1689 
1690  def _unindex_entry(self, entry_id: str) -> None:
1691  """Unindex an entry."""
1692  entry = self.data[entry_id]
1693  domain = entry.domain
1694  self._domain_index[domain].remove(entry)
1695  if not self._domain_index[domain]:
1696  del self._domain_index[domain]
1697  if (unique_id := entry.unique_id) is not None:
1698  self._domain_unique_id_index[domain][unique_id].remove(entry)
1699  if not self._domain_unique_id_index[domain][unique_id]:
1700  del self._domain_unique_id_index[domain][unique_id]
1701  if not self._domain_unique_id_index[domain]:
1702  del self._domain_unique_id_index[domain]
1703 
1704  def __delitem__(self, entry_id: str) -> None:
1705  """Remove an item."""
1706  self._unindex_entry_unindex_entry(entry_id)
1707  super().__delitem__(entry_id)
1708 
1709  def update_unique_id(self, entry: ConfigEntry, new_unique_id: str | None) -> None:
1710  """Update unique id for an entry.
1711 
1712  This method mutates the entry with the new unique id and updates the indexes.
1713  """
1714  entry_id = entry.entry_id
1715  self._unindex_entry_unindex_entry(entry_id)
1716  self.check_unique_idcheck_unique_id(entry)
1717  object.__setattr__(entry, "unique_id", new_unique_id)
1718  self._index_entry_index_entry(entry)
1719  entry.clear_state_cache()
1720  entry.clear_storage_cache()
1721 
1722  def get_entries_for_domain(self, domain: str) -> list[ConfigEntry]:
1723  """Get entries for a domain."""
1724  return self._domain_index.get(domain, [])
1725 
1727  self, domain: str, unique_id: str
1728  ) -> ConfigEntry | None:
1729  """Get entry by domain and unique id."""
1730  if unique_id is None:
1731  return None # type: ignore[unreachable]
1732  if not isinstance(unique_id, Hashable):
1733  raise HomeAssistantError(
1734  f"The entry unique id {unique_id} is not a string."
1735  )
1736  entries = self._domain_unique_id_index.get(domain, {}).get(unique_id)
1737  if not entries:
1738  return None
1739  return entries[0]
1740 
1741 
1742 class ConfigEntryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
1743  """Class to help storing config entry data."""
1744 
1745  def __init__(self, hass: HomeAssistant) -> None:
1746  """Initialize storage class."""
1747  super().__init__(
1748  hass,
1749  STORAGE_VERSION,
1750  STORAGE_KEY,
1751  minor_version=STORAGE_VERSION_MINOR,
1752  )
1753 
1755  self,
1756  old_major_version: int,
1757  old_minor_version: int,
1758  old_data: dict[str, Any],
1759  ) -> dict[str, Any]:
1760  """Migrate to the new version."""
1761  data = old_data
1762  if old_major_version == 1:
1763  if old_minor_version < 2:
1764  # Version 1.2 implements migration and freezes the available keys
1765  for entry in data["entries"]:
1766  # Populate keys which were introduced before version 1.2
1767 
1768  pref_disable_new_entities = entry.get("pref_disable_new_entities")
1769  if pref_disable_new_entities is None and "system_options" in entry:
1770  pref_disable_new_entities = entry.get("system_options", {}).get(
1771  "disable_new_entities"
1772  )
1773 
1774  entry.setdefault("disabled_by", entry.get("disabled_by"))
1775  entry.setdefault("minor_version", entry.get("minor_version", 1))
1776  entry.setdefault("options", entry.get("options", {}))
1777  entry.setdefault(
1778  "pref_disable_new_entities", pref_disable_new_entities
1779  )
1780  entry.setdefault(
1781  "pref_disable_polling", entry.get("pref_disable_polling")
1782  )
1783  entry.setdefault("unique_id", entry.get("unique_id"))
1784 
1785  if old_minor_version < 3:
1786  # Version 1.3 adds the created_at and modified_at fields
1787  created_at = utc_from_timestamp(0).isoformat()
1788  for entry in data["entries"]:
1789  entry["created_at"] = entry["modified_at"] = created_at
1790 
1791  if old_minor_version < 4:
1792  # Version 1.4 adds discovery_keys
1793  for entry in data["entries"]:
1794  entry["discovery_keys"] = {}
1795 
1796  if old_major_version > 1:
1797  raise NotImplementedError
1798  return data
1799 
1800 
1802  """Manage the configuration entries.
1803 
1804  An instance of this object is available via `hass.config_entries`.
1805  """
1806 
1807  def __init__(self, hass: HomeAssistant, hass_config: ConfigType) -> None:
1808  """Initialize the entry manager."""
1809  self.hasshass = hass
1810  self.flowflow = ConfigEntriesFlowManager(hass, self, hass_config)
1811  self.optionsoptions = OptionsFlowManager(hass)
1812  self._hass_config_hass_config = hass_config
1813  self._entries_entries = ConfigEntryItems(hass)
1814  self._store_store = ConfigEntryStore(hass)
1816 
1817  @callback
1819  self, include_ignore: bool = False, include_disabled: bool = False
1820  ) -> list[str]:
1821  """Return domains for which we have entries."""
1822  return list(
1823  {
1824  entry.domain: None
1825  for entry in self._entries_entries.values()
1826  if (include_ignore or entry.source != SOURCE_IGNORE)
1827  and (include_disabled or not entry.disabled_by)
1828  }
1829  )
1830 
1831  @callback
1832  def async_get_entry(self, entry_id: str) -> ConfigEntry | None:
1833  """Return entry with matching entry_id."""
1834  return self._entries_entries.data.get(entry_id)
1835 
1836  @callback
1837  def async_get_known_entry(self, entry_id: str) -> ConfigEntry:
1838  """Return entry with matching entry_id.
1839 
1840  Raises UnknownEntry if entry is not found.
1841  """
1842  if (entry := self.async_get_entryasync_get_entry(entry_id)) is None:
1843  raise UnknownEntry
1844  return entry
1845 
1846  @callback
1847  def async_entry_ids(self) -> list[str]:
1848  """Return entry ids."""
1849  return list(self._entries_entries.data)
1850 
1851  @callback
1853  self, domain: str, include_ignore: bool = True, include_disabled: bool = True
1854  ) -> bool:
1855  """Return if there are entries for a domain."""
1856  entries = self._entries_entries.get_entries_for_domain(domain)
1857  if include_ignore and include_disabled:
1858  return bool(entries)
1859  for entry in entries:
1860  if (include_ignore or entry.source != SOURCE_IGNORE) and (
1861  include_disabled or not entry.disabled_by
1862  ):
1863  return True
1864  return False
1865 
1866  @callback
1868  self,
1869  domain: str | None = None,
1870  include_ignore: bool = True,
1871  include_disabled: bool = True,
1872  ) -> list[ConfigEntry]:
1873  """Return all entries or entries for a specific domain."""
1874  if domain is None:
1875  entries: Iterable[ConfigEntry] = self._entries_entries.values()
1876  else:
1877  entries = self._entries_entries.get_entries_for_domain(domain)
1878 
1879  if include_ignore and include_disabled:
1880  return list(entries)
1881 
1882  return [
1883  entry
1884  for entry in entries
1885  if (include_ignore or entry.source != SOURCE_IGNORE)
1886  and (include_disabled or not entry.disabled_by)
1887  ]
1888 
1889  @callback
1890  def async_loaded_entries(self, domain: str) -> list[ConfigEntry]:
1891  """Return loaded entries for a specific domain.
1892 
1893  This will exclude ignored or disabled config entruis.
1894  """
1895  entries = self._entries_entries.get_entries_for_domain(domain)
1896 
1897  return [entry for entry in entries if entry.state == ConfigEntryState.LOADED]
1898 
1899  @callback
1901  self, domain: str, unique_id: str
1902  ) -> ConfigEntry | None:
1903  """Return entry for a domain with a matching unique id."""
1904  return self._entries_entries.get_entry_by_domain_and_unique_id(domain, unique_id)
1905 
1906  async def async_add(self, entry: ConfigEntry) -> None:
1907  """Add and setup an entry."""
1908  if entry.entry_id in self._entries_entries.data:
1909  raise HomeAssistantError(
1910  f"An entry with the id {entry.entry_id} already exists."
1911  )
1912 
1913  self._entries_entries[entry.entry_id] = entry
1914  self.async_update_issuesasync_update_issues()
1915  self._async_dispatch_async_dispatch(ConfigEntryChange.ADDED, entry)
1916  await self.async_setupasync_setup(entry.entry_id)
1917  self._async_schedule_save_async_schedule_save()
1918 
1919  async def async_remove(self, entry_id: str) -> dict[str, Any]:
1920  """Remove, unload and clean up after an entry."""
1921  unload_success, entry = await self._async_remove_async_remove(entry_id)
1922  self._async_clean_up_async_clean_up(entry)
1923 
1924  for discovery_domain in entry.discovery_keys:
1925  async_dispatcher_send_internal(
1926  self.hasshass,
1927  signal_discovered_config_entry_removed(discovery_domain),
1928  entry,
1929  )
1930 
1931  return {"require_restart": not unload_success}
1932 
1933  async def _async_remove(self, entry_id: str) -> tuple[bool, ConfigEntry]:
1934  """Remove and unload an entry."""
1935  entry = self.async_get_known_entryasync_get_known_entry(entry_id)
1936 
1937  async with entry.setup_lock:
1938  if not entry.state.recoverable:
1939  unload_success = entry.state is not ConfigEntryState.FAILED_UNLOAD
1940  else:
1941  unload_success = await self.async_unloadasync_unload(entry_id, _lock=False)
1942 
1943  await entry.async_remove(self.hasshass)
1944 
1945  del self._entries_entries[entry.entry_id]
1946  self.async_update_issuesasync_update_issues()
1947  self._async_schedule_save_async_schedule_save()
1948 
1949  return (unload_success, entry)
1950 
1951  @callback
1952  def _async_clean_up(self, entry: ConfigEntry) -> None:
1953  """Clean up after an entry."""
1954  entry_id = entry.entry_id
1955 
1956  dev_reg = dr.async_get(self.hasshass)
1957  ent_reg = er.async_get(self.hasshass)
1958 
1959  dev_reg.async_clear_config_entry(entry_id)
1960  ent_reg.async_clear_config_entry(entry_id)
1961 
1962  # If the configuration entry is removed during reauth, it should
1963  # abort any reauth flow that is active for the removed entry and
1964  # linked issues.
1965  for progress_flow in self.hasshass.config_entries.flow.async_progress_by_handler(
1966  entry.domain, match_context={"entry_id": entry_id, "source": SOURCE_REAUTH}
1967  ):
1968  if "flow_id" in progress_flow:
1969  self.hasshass.config_entries.flow.async_abort(progress_flow["flow_id"])
1970  issue_id = f"config_entry_reauth_{entry.domain}_{entry.entry_id}"
1971  ir.async_delete_issue(self.hasshass, HOMEASSISTANT_DOMAIN, issue_id)
1972 
1973  self._async_dispatch_async_dispatch(ConfigEntryChange.REMOVED, entry)
1974 
1975  @callback
1976  def _async_shutdown(self, event: Event) -> None:
1977  """Call when Home Assistant is stopping."""
1978  for entry in self._entries_entries.values():
1979  entry.async_shutdown()
1980  self.flowflow.async_shutdown()
1981 
1982  async def async_initialize(self) -> None:
1983  """Initialize config entry config."""
1984  config = await self._store_store.async_load()
1985 
1986  self.hasshass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown_async_shutdown)
1987 
1988  if config is None:
1989  self._entries_entries = ConfigEntryItems(self.hasshass)
1990  return
1991 
1992  entries: ConfigEntryItems = ConfigEntryItems(self.hasshass)
1993  for entry in config["entries"]:
1994  entry_id = entry["entry_id"]
1995 
1996  config_entry = ConfigEntry(
1997  created_at=datetime.fromisoformat(entry["created_at"]),
1998  data=entry["data"],
1999  disabled_by=try_parse_enum(ConfigEntryDisabler, entry["disabled_by"]),
2000  discovery_keys=MappingProxyType(
2001  {
2002  domain: tuple(DiscoveryKey.from_json_dict(key) for key in keys)
2003  for domain, keys in entry["discovery_keys"].items()
2004  }
2005  ),
2006  domain=entry["domain"],
2007  entry_id=entry_id,
2008  minor_version=entry["minor_version"],
2009  modified_at=datetime.fromisoformat(entry["modified_at"]),
2010  options=entry["options"],
2011  pref_disable_new_entities=entry["pref_disable_new_entities"],
2012  pref_disable_polling=entry["pref_disable_polling"],
2013  source=entry["source"],
2014  title=entry["title"],
2015  unique_id=entry["unique_id"],
2016  version=entry["version"],
2017  )
2018  entries[entry_id] = config_entry
2019 
2020  self._entries_entries = entries
2021  self.async_update_issuesasync_update_issues()
2022 
2023  async def async_setup(self, entry_id: str, _lock: bool = True) -> bool:
2024  """Set up a config entry.
2025 
2026  Return True if entry has been successfully loaded.
2027  """
2028  entry = self.async_get_known_entryasync_get_known_entry(entry_id)
2029 
2030  if entry.state is not ConfigEntryState.NOT_LOADED:
2031  raise OperationNotAllowed(
2032  f"The config entry '{entry.title}' ({entry.domain}) with entry_id"
2033  f" '{entry.entry_id}' cannot be set up because it is in state "
2034  f"{entry.state}, but needs to be in the {ConfigEntryState.NOT_LOADED} state"
2035  )
2036 
2037  # Setup Component if not set up yet
2038  if entry.domain in self.hasshass.config.components:
2039  if _lock:
2040  async with entry.setup_lock:
2041  await entry.async_setup(self.hasshass)
2042  else:
2043  await entry.async_setup(self.hasshass)
2044  else:
2045  # Setting up the component will set up all its config entries
2046  result = await async_setup_component(
2047  self.hasshass, entry.domain, self._hass_config_hass_config
2048  )
2049 
2050  if not result:
2051  return result
2052 
2053  return (
2054  entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap]
2055  )
2056 
2057  async def async_unload(self, entry_id: str, _lock: bool = True) -> bool:
2058  """Unload a config entry."""
2059  entry = self.async_get_known_entryasync_get_known_entry(entry_id)
2060 
2061  if not entry.state.recoverable:
2062  raise OperationNotAllowed(
2063  f"The config entry '{entry.title}' ({entry.domain}) with entry_id"
2064  f" '{entry.entry_id}' cannot be unloaded because it is in the non"
2065  f" recoverable state {entry.state}"
2066  )
2067 
2068  if _lock:
2069  async with entry.setup_lock:
2070  return await entry.async_unload(self.hasshass)
2071 
2072  return await entry.async_unload(self.hasshass)
2073 
2074  @callback
2075  def async_schedule_reload(self, entry_id: str) -> None:
2076  """Schedule a config entry to be reloaded."""
2077  entry = self.async_get_known_entryasync_get_known_entry(entry_id)
2078  entry.async_cancel_retry_setup()
2079  self.hasshass.async_create_task(
2080  self.async_reloadasync_reload(entry_id),
2081  f"config entry reload {entry.title} {entry.domain} {entry.entry_id}",
2082  )
2083 
2084  async def async_reload(self, entry_id: str) -> bool:
2085  """Reload an entry.
2086 
2087  When reloading from an integration is is preferable to
2088  call async_schedule_reload instead of this method since
2089  it will cancel setup retry before starting this method
2090  in a task which eliminates a race condition where the
2091  setup retry can fire during the reload.
2092 
2093  If an entry was not loaded, will just load.
2094  """
2095  entry = self.async_get_known_entryasync_get_known_entry(entry_id)
2096 
2097  # Cancel the setup retry task before waiting for the
2098  # reload lock to reduce the chance of concurrent reload
2099  # attempts.
2100  entry.async_cancel_retry_setup()
2101 
2102  if entry.domain not in self.hasshass.config.components:
2103  # If the component is not loaded, just load it as
2104  # the config entry will be loaded as well. We need
2105  # to do this before holding the lock to avoid a
2106  # deadlock.
2107  await async_setup_component(self.hasshass, entry.domain, self._hass_config_hass_config)
2108  return entry.state is ConfigEntryState.LOADED
2109 
2110  async with entry.setup_lock:
2111  unload_result = await self.async_unloadasync_unload(entry_id, _lock=False)
2112 
2113  if not unload_result or entry.disabled_by:
2114  return unload_result
2115 
2116  return await self.async_setupasync_setup(entry_id, _lock=False)
2117 
2119  self, entry_id: str, disabled_by: ConfigEntryDisabler | None
2120  ) -> bool:
2121  """Disable an entry.
2122 
2123  If disabled_by is changed, the config entry will be reloaded.
2124  """
2125  entry = self.async_get_known_entryasync_get_known_entry(entry_id)
2126 
2127  _validate_item(disabled_by=disabled_by)
2128  if entry.disabled_by is disabled_by:
2129  return True
2130 
2131  entry.disabled_by = disabled_by
2132  self._async_schedule_save_async_schedule_save()
2133 
2134  dev_reg = dr.async_get(self.hasshass)
2135  ent_reg = er.async_get(self.hasshass)
2136 
2137  if not entry.disabled_by:
2138  # The config entry will no longer be disabled, enable devices and entities
2139  dr.async_config_entry_disabled_by_changed(dev_reg, entry)
2140  er.async_config_entry_disabled_by_changed(ent_reg, entry)
2141 
2142  # Load or unload the config entry
2143  reload_result = await self.async_reloadasync_reload(entry_id)
2144 
2145  if entry.disabled_by:
2146  # The config entry has been disabled, disable devices and entities
2147  dr.async_config_entry_disabled_by_changed(dev_reg, entry)
2148  er.async_config_entry_disabled_by_changed(ent_reg, entry)
2149 
2150  return reload_result
2151 
2152  @callback
2154  self,
2155  entry: ConfigEntry,
2156  *,
2157  data: Mapping[str, Any] | UndefinedType = UNDEFINED,
2158  discovery_keys: MappingProxyType[str, tuple[DiscoveryKey, ...]]
2159  | UndefinedType = UNDEFINED,
2160  minor_version: int | UndefinedType = UNDEFINED,
2161  options: Mapping[str, Any] | UndefinedType = UNDEFINED,
2162  pref_disable_new_entities: bool | UndefinedType = UNDEFINED,
2163  pref_disable_polling: bool | UndefinedType = UNDEFINED,
2164  title: str | UndefinedType = UNDEFINED,
2165  unique_id: str | None | UndefinedType = UNDEFINED,
2166  version: int | UndefinedType = UNDEFINED,
2167  ) -> bool:
2168  """Update a config entry.
2169 
2170  If the entry was changed, the update_listeners are
2171  fired and this function returns True
2172 
2173  If the entry was not changed, the update_listeners are
2174  not fired and this function returns False
2175  """
2176  if entry.entry_id not in self._entries_entries:
2177  raise UnknownEntry(entry.entry_id)
2178 
2179  self.hasshass.verify_event_loop_thread("hass.config_entries.async_update_entry")
2180  changed = False
2181  _setter = object.__setattr__
2182 
2183  if unique_id is not UNDEFINED and entry.unique_id != unique_id:
2184  # Deprecated in 2024.11, should fail in 2025.11
2185  if (
2186  # flipr creates duplicates during migration, and asks users to
2187  # remove the duplicate. We don't need warn about it here too.
2188  # We should remove the special case for "flipr" in HA Core 2025.4,
2189  # when the flipr migration period ends
2190  entry.domain != "flipr"
2191  and unique_id is not None
2192  and self.async_entry_for_domain_unique_idasync_entry_for_domain_unique_id(entry.domain, unique_id)
2193  is not None
2194  ):
2195  report_issue = async_suggest_report_issue(
2196  self.hasshass, integration_domain=entry.domain
2197  )
2198  _LOGGER.error(
2199  (
2200  "Unique id of config entry '%s' from integration %s changed to"
2201  " '%s' which is already in use, please %s"
2202  ),
2203  entry.title,
2204  entry.domain,
2205  unique_id,
2206  report_issue,
2207  )
2208  # Reindex the entry if the unique_id has changed
2209  self._entries_entries.update_unique_id(entry, unique_id)
2210  self.async_update_issuesasync_update_issues()
2211  changed = True
2212 
2213  for attr, value in (
2214  ("discovery_keys", discovery_keys),
2215  ("minor_version", minor_version),
2216  ("pref_disable_new_entities", pref_disable_new_entities),
2217  ("pref_disable_polling", pref_disable_polling),
2218  ("title", title),
2219  ("version", version),
2220  ):
2221  if value is UNDEFINED or getattr(entry, attr) == value:
2222  continue
2223 
2224  _setter(entry, attr, value)
2225  changed = True
2226 
2227  if data is not UNDEFINED and entry.data != data:
2228  changed = True
2229  _setter(entry, "data", MappingProxyType(data))
2230 
2231  if options is not UNDEFINED and entry.options != options:
2232  changed = True
2233  _setter(entry, "options", MappingProxyType(options))
2234 
2235  if not changed:
2236  return False
2237 
2238  _setter(entry, "modified_at", utcnow())
2239 
2240  for listener in entry.update_listeners:
2241  self.hasshass.async_create_task(
2242  listener(self.hasshass, entry),
2243  f"config entry update listener {entry.title} {entry.domain} {entry.domain}",
2244  )
2245 
2246  self._async_schedule_save_async_schedule_save()
2247  entry.clear_state_cache()
2248  entry.clear_storage_cache()
2249  self._async_dispatch_async_dispatch(ConfigEntryChange.UPDATED, entry)
2250  return True
2251 
2252  @callback
2254  self, change_type: ConfigEntryChange, entry: ConfigEntry
2255  ) -> None:
2256  """Dispatch a config entry change."""
2257  async_dispatcher_send_internal(
2258  self.hasshass, SIGNAL_CONFIG_ENTRY_CHANGED, change_type, entry
2259  )
2260 
2262  self, entry: ConfigEntry, platforms: Iterable[Platform | str]
2263  ) -> None:
2264  """Forward the setup of an entry to platforms.
2265 
2266  This method should be awaited before async_setup_entry is finished
2267  in each integration. This is to ensure that all platforms are loaded
2268  before the entry is set up. This ensures that the config entry cannot
2269  be unloaded before all platforms are loaded.
2270 
2271  This method is more efficient than async_forward_entry_setup as
2272  it can load multiple platforms at once and does not require a separate
2273  import executor job for each platform.
2274  """
2275  integration = await loader.async_get_integration(self.hasshass, entry.domain)
2276  if not integration.platforms_are_loaded(platforms):
2277  with async_pause_setup(self.hasshass, SetupPhases.WAIT_IMPORT_PLATFORMS):
2278  await integration.async_get_platforms(platforms)
2279 
2280  if not entry.setup_lock.locked():
2281  async with entry.setup_lock:
2282  if entry.state is not ConfigEntryState.LOADED:
2283  raise OperationNotAllowed(
2284  f"The config entry '{entry.title}' ({entry.domain}) with "
2285  f"entry_id '{entry.entry_id}' cannot forward setup for "
2286  f"{platforms} because it is in state {entry.state}, but needs "
2287  f"to be in the {ConfigEntryState.LOADED} state"
2288  )
2289  await self._async_forward_entry_setups_locked_async_forward_entry_setups_locked(entry, platforms)
2290  else:
2291  await self._async_forward_entry_setups_locked_async_forward_entry_setups_locked(entry, platforms)
2292  # If the lock was held when we stated, and it was released during
2293  # the platform setup, it means they did not await the setup call.
2294  if not entry.setup_lock.locked():
2296  entry, "async_forward_entry_setups"
2297  )
2298 
2300  self, entry: ConfigEntry, platforms: Iterable[Platform | str]
2301  ) -> None:
2302  await asyncio.gather(
2303  *(
2304  create_eager_task(
2305  self._async_forward_entry_setup_async_forward_entry_setup(entry, platform, False),
2306  name=(
2307  f"config entry forward setup {entry.title} "
2308  f"{entry.domain} {entry.entry_id} {platform}"
2309  ),
2310  loop=self.hasshass.loop,
2311  )
2312  for platform in platforms
2313  )
2314  )
2315 
2317  self, entry: ConfigEntry, domain: Platform | str
2318  ) -> bool:
2319  """Forward the setup of an entry to a different component.
2320 
2321  By default an entry is setup with the component it belongs to. If that
2322  component also has related platforms, the component will have to
2323  forward the entry to be setup by that component.
2324 
2325  This method is deprecated and will stop working in Home Assistant 2025.6.
2326 
2327  Instead, await async_forward_entry_setups as it can load
2328  multiple platforms at once and is more efficient since it
2329  does not require a separate import executor job for each platform.
2330  """
2331  report_usage(
2332  "calls async_forward_entry_setup for "
2333  f"integration, {entry.domain} with title: {entry.title} "
2334  f"and entry_id: {entry.entry_id}, which is deprecated, "
2335  "await async_forward_entry_setups instead",
2336  core_behavior=ReportBehavior.LOG,
2337  breaks_in_ha_version="2025.6",
2338  )
2339  if not entry.setup_lock.locked():
2340  async with entry.setup_lock:
2341  if entry.state is not ConfigEntryState.LOADED:
2342  raise OperationNotAllowed(
2343  f"The config entry '{entry.title}' ({entry.domain}) with "
2344  f"entry_id '{entry.entry_id}' cannot forward setup for "
2345  f"{domain} because it is in state {entry.state}, but needs "
2346  f"to be in the {ConfigEntryState.LOADED} state"
2347  )
2348  return await self._async_forward_entry_setup_async_forward_entry_setup(entry, domain, True)
2349  result = await self._async_forward_entry_setup_async_forward_entry_setup(entry, domain, True)
2350  # If the lock was held when we stated, and it was released during
2351  # the platform setup, it means they did not await the setup call.
2352  if not entry.setup_lock.locked():
2353  _report_non_awaited_platform_forwards(entry, "async_forward_entry_setup")
2354  return result
2355 
2357  self,
2358  entry: ConfigEntry,
2359  domain: Platform | str,
2360  preload_platform: bool,
2361  ) -> bool:
2362  """Forward the setup of an entry to a different component."""
2363  # Setup Component if not set up yet
2364  if domain not in self.hasshass.config.components:
2365  with async_pause_setup(self.hasshass, SetupPhases.WAIT_BASE_PLATFORM_SETUP):
2366  result = await async_setup_component(
2367  self.hasshass, domain, self._hass_config_hass_config
2368  )
2369 
2370  if not result:
2371  return False
2372 
2373  if preload_platform:
2374  # If this is a late setup, we need to make sure the platform is loaded
2375  # so we do not end up waiting for when the EntityComponent calls
2376  # async_prepare_setup_platform
2377  integration = await loader.async_get_integration(self.hasshass, entry.domain)
2378  if not integration.platforms_are_loaded((domain,)):
2379  with async_pause_setup(self.hasshass, SetupPhases.WAIT_IMPORT_PLATFORMS):
2380  await integration.async_get_platform(domain)
2381 
2382  integration = loader.async_get_loaded_integration(self.hasshass, domain)
2383  await entry.async_setup(self.hasshass, integration=integration)
2384  return True
2385 
2387  self, entry: ConfigEntry, platforms: Iterable[Platform | str]
2388  ) -> bool:
2389  """Forward the unloading of an entry to platforms."""
2390  return all(
2391  await asyncio.gather(
2392  *(
2393  create_eager_task(
2394  self.async_forward_entry_unloadasync_forward_entry_unload(entry, platform),
2395  name=(
2396  f"config entry forward unload {entry.title} "
2397  f"{entry.domain} {entry.entry_id} {platform}"
2398  ),
2399  loop=self.hasshass.loop,
2400  )
2401  for platform in platforms
2402  )
2403  )
2404  )
2405 
2407  self, entry: ConfigEntry, domain: Platform | str
2408  ) -> bool:
2409  """Forward the unloading of an entry to a different component.
2410 
2411  Its is preferred to call async_unload_platforms instead
2412  of directly calling this method.
2413  """
2414  # It was never loaded.
2415  if domain not in self.hasshass.config.components:
2416  return True
2417 
2418  integration = loader.async_get_loaded_integration(self.hasshass, domain)
2419 
2420  return await entry.async_unload(self.hasshass, integration=integration)
2421 
2422  @callback
2423  def _async_schedule_save(self) -> None:
2424  """Save the entity registry to a file."""
2425  self._store_store.async_delay_save(self._data_to_save_data_to_save, SAVE_DELAY)
2426 
2427  @callback
2428  def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
2429  """Return data to save."""
2430  # typing does not know that the storage fragment will serialize to a dict
2431  return {
2432  "entries": [entry.as_storage_fragment for entry in self._entries_entries.values()] # type: ignore[misc]
2433  }
2434 
2435  async def async_wait_component(self, entry: ConfigEntry) -> bool:
2436  """Wait for an entry's component to load and return if the entry is loaded.
2437 
2438  This is primarily intended for existing config entries which are loaded at
2439  startup, awaiting this function will block until the component and all its
2440  config entries are loaded.
2441  Config entries which are created after Home Assistant is started can't be waited
2442  for, the function will just return if the config entry is loaded or not.
2443  """
2444  setup_done = self.hasshass.data.get(DATA_SETUP_DONE, {})
2445  if setup_future := setup_done.get(entry.domain):
2446  await setup_future
2447  # The component was not loaded.
2448  if entry.domain not in self.hasshass.config.components:
2449  return False
2450  return entry.state is ConfigEntryState.LOADED
2451 
2452  @callback
2453  def async_update_issues(self) -> None:
2454  """Update unique id collision issues."""
2455  issue_registry = ir.async_get(self.hasshass)
2456  issues: set[str] = set()
2457 
2458  for issue in issue_registry.issues.values():
2459  if (
2460  issue.domain != HOMEASSISTANT_DOMAIN
2461  or not (issue_data := issue.data)
2462  or issue_data.get("issue_type") != ISSUE_UNIQUE_ID_COLLISION
2463  ):
2464  continue
2465  issues.add(issue.issue_id)
2466 
2467  for domain, unique_ids in self._entries_entries._domain_unique_id_index.items(): # noqa: SLF001
2468  # flipr creates duplicates during migration, and asks users to
2469  # remove the duplicate. We don't need warn about it here too.
2470  # We should remove the special case for "flipr" in HA Core 2025.4,
2471  # when the flipr migration period ends
2472  if domain == "flipr":
2473  continue
2474  for unique_id, entries in unique_ids.items():
2475  # We might mutate the list of entries, so we need a copy to not mess up
2476  # the index
2477  entries = list(entries)
2478 
2479  # There's no need to raise an issue for ignored entries, we can
2480  # safely remove them once we no longer allow unique id collisions.
2481  # Iterate over a copy of the copy to allow mutating while iterating
2482  for entry in list(entries):
2483  if entry.source == SOURCE_IGNORE:
2484  entries.remove(entry)
2485 
2486  if len(entries) < 2:
2487  continue
2488  issue_id = f"{ISSUE_UNIQUE_ID_COLLISION}_{domain}_{unique_id}"
2489  issues.discard(issue_id)
2490  titles = [f"'{entry.title}'" for entry in entries]
2491  translation_placeholders = {
2492  "domain": domain,
2493  "configure_url": f"/config/integrations/integration/{domain}",
2494  "unique_id": str(unique_id),
2495  }
2496  if len(titles) <= UNIQUE_ID_COLLISION_TITLE_LIMIT:
2497  translation_key = "config_entry_unique_id_collision"
2498  translation_placeholders["titles"] = ", ".join(titles)
2499  else:
2500  translation_key = "config_entry_unique_id_collision_many"
2501  translation_placeholders["number_of_entries"] = str(len(titles))
2502  translation_placeholders["titles"] = ", ".join(
2503  titles[:UNIQUE_ID_COLLISION_TITLE_LIMIT]
2504  )
2505  translation_placeholders["title_limit"] = str(
2506  UNIQUE_ID_COLLISION_TITLE_LIMIT
2507  )
2508 
2509  ir.async_create_issue(
2510  self.hasshass,
2511  HOMEASSISTANT_DOMAIN,
2512  issue_id,
2513  breaks_in_ha_version="2025.11.0",
2514  data={
2515  "issue_type": ISSUE_UNIQUE_ID_COLLISION,
2516  "unique_id": unique_id,
2517  },
2518  is_fixable=False,
2519  issue_domain=domain,
2520  severity=ir.IssueSeverity.ERROR,
2521  translation_key=translation_key,
2522  translation_placeholders=translation_placeholders,
2523  )
2524 
2525  break # Only create one issue per domain
2526 
2527  for issue_id in issues:
2528  ir.async_delete_issue(self.hasshass, HOMEASSISTANT_DOMAIN, issue_id)
2529 
2530 
2531 @callback
2533  other_entries: list[ConfigEntry], match_dict: dict[str, Any] | None = None
2534 ) -> None:
2535  """Abort if current entries match all data.
2536 
2537  Requires `already_configured` in strings.json in user visible flows.
2538  """
2539  if match_dict is None:
2540  match_dict = {} # Match any entry
2541  for entry in other_entries:
2542  options_items = entry.options.items()
2543  data_items = entry.data.items()
2544  for kv in match_dict.items():
2545  if kv not in options_items and kv not in data_items:
2546  break
2547  else:
2548  raise data_entry_flow.AbortFlow("already_configured")
2549 
2550 
2552  data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult]
2553 ):
2554  """Base class for config and option flows."""
2555 
2556  _flow_result = ConfigFlowResult
2557 
2558 
2560  """Base class for config flows with some helpers."""
2561 
2562  def __init_subclass__(cls, *, domain: str | None = None, **kwargs: Any) -> None:
2563  """Initialize a subclass, register if possible."""
2564  super().__init_subclass__(**kwargs)
2565  if domain is not None:
2566  HANDLERS.register(domain)(cls)
2567 
2568  @property
2569  def unique_id(self) -> str | None:
2570  """Return unique ID if available."""
2571  if not self.context:
2572  return None
2573 
2574  return self.context.get("unique_id")
2575 
2576  @staticmethod
2577  @callback
2578  def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
2579  """Get the options flow for this handler."""
2581 
2582  @classmethod
2583  @callback
2584  def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
2585  """Return options flow support for this handler."""
2586  return cls.async_get_options_flowasync_get_options_flow is not ConfigFlow.async_get_options_flow
2587 
2588  @callback
2590  self, match_dict: dict[str, Any] | None = None
2591  ) -> None:
2592  """Abort if current entries match all data.
2593 
2594  Requires `already_configured` in strings.json in user visible flows.
2595  """
2597  self._async_current_entries_async_current_entries(include_ignore=False), match_dict
2598  )
2599 
2600  @callback
2602  self,
2603  *,
2604  reason: str = "unique_id_mismatch",
2605  description_placeholders: Mapping[str, str] | None = None,
2606  ) -> None:
2607  """Abort if the unique ID does not match the reauth/reconfigure context.
2608 
2609  Requires strings.json entry corresponding to the `reason` parameter
2610  in user visible flows.
2611  """
2612  if (
2613  self.sourcesourcesource == SOURCE_REAUTH
2614  and self._get_reauth_entry_get_reauth_entry().unique_id != self.unique_idunique_id
2615  ) or (
2616  self.sourcesourcesource == SOURCE_RECONFIGURE
2617  and self._get_reconfigure_entry_get_reconfigure_entry().unique_id != self.unique_idunique_id
2618  ):
2619  raise data_entry_flow.AbortFlow(reason, description_placeholders)
2620 
2621  @callback
2623  self,
2624  updates: dict[str, Any] | None = None,
2625  reload_on_update: bool = True,
2626  *,
2627  error: str = "already_configured",
2628  ) -> None:
2629  """Abort if the unique ID is already configured.
2630 
2631  Requires strings.json entry corresponding to the `error` parameter
2632  in user visible flows.
2633  """
2634  if self.unique_idunique_id is None:
2635  return
2636 
2637  if not (
2638  entry := self.hass.config_entries.async_entry_for_domain_unique_id(
2639  self.handler, self.unique_idunique_id
2640  )
2641  ):
2642  return
2643 
2644  should_reload = False
2645  if (
2646  updates is not None
2647  and self.hass.config_entries.async_update_entry(
2648  entry, data={**entry.data, **updates}
2649  )
2650  and reload_on_update
2651  and entry.state in (ConfigEntryState.LOADED, ConfigEntryState.SETUP_RETRY)
2652  ):
2653  # Existing config entry present, and the
2654  # entry data just changed
2655  should_reload = True
2656  elif (
2657  self.sourcesourcesource in DISCOVERY_SOURCES
2658  and entry.state is ConfigEntryState.SETUP_RETRY
2659  ):
2660  # Existing config entry present in retry state, and we
2661  # just discovered the unique id so we know its online
2662  should_reload = True
2663  # Allow ignored entries to be configured on manual user step
2664  if entry.source == SOURCE_IGNORE and self.sourcesourcesource == SOURCE_USER:
2665  return
2666  if should_reload:
2667  self.hass.config_entries.async_schedule_reload(entry.entry_id)
2668  raise data_entry_flow.AbortFlow(error)
2669 
2671  self, unique_id: str | None = None, *, raise_on_progress: bool = True
2672  ) -> ConfigEntry | None:
2673  """Set a unique ID for the config flow.
2674 
2675  Returns optionally existing config entry with same ID.
2676  """
2677  if unique_id is None:
2678  self.context["unique_id"] = None
2679  return None
2680 
2681  if raise_on_progress:
2682  if self._async_in_progress_async_in_progress(
2683  include_uninitialized=True, match_context={"unique_id": unique_id}
2684  ):
2685  raise data_entry_flow.AbortFlow("already_in_progress")
2686 
2687  self.context["unique_id"] = unique_id
2688 
2689  # Abort discoveries done using the default discovery unique id
2690  if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID:
2691  for progress in self._async_in_progress_async_in_progress(
2692  include_uninitialized=True,
2693  match_context={"unique_id": DEFAULT_DISCOVERY_UNIQUE_ID},
2694  ):
2695  self.hass.config_entries.flow.async_abort(progress["flow_id"])
2696 
2697  return self.hass.config_entries.async_entry_for_domain_unique_id(
2698  self.handler, unique_id
2699  )
2700 
2701  @callback
2703  self,
2704  ) -> None:
2705  """Mark the config flow as only needing user confirmation to finish flow."""
2706  self.context["confirm_only"] = True
2707 
2708  @callback
2710  self, include_ignore: bool | None = None
2711  ) -> list[ConfigEntry]:
2712  """Return current entries.
2713 
2714  If the flow is user initiated, filter out ignored entries,
2715  unless include_ignore is True.
2716  """
2717  return self.hass.config_entries.async_entries(
2718  self.handler,
2719  include_ignore or (include_ignore is None and self.sourcesourcesource != SOURCE_USER),
2720  )
2721 
2722  @callback
2723  def _async_current_ids(self, include_ignore: bool = True) -> set[str | None]:
2724  """Return current unique IDs."""
2725  return {
2726  entry.unique_id
2727  for entry in self.hass.config_entries.async_entries(self.handler)
2728  if include_ignore or entry.source != SOURCE_IGNORE
2729  }
2730 
2731  @callback
2733  self,
2734  include_uninitialized: bool = False,
2735  match_context: dict[str, Any] | None = None,
2736  ) -> list[ConfigFlowResult]:
2737  """Return other in progress flows for current domain."""
2738  return [
2739  flw
2740  for flw in self.hass.config_entries.flow.async_progress_by_handler(
2741  self.handler,
2742  include_uninitialized=include_uninitialized,
2743  match_context=match_context,
2744  )
2745  if flw["flow_id"] != self.flow_id
2746  ]
2747 
2748  async def async_step_ignore(self, user_input: dict[str, Any]) -> ConfigFlowResult:
2749  """Ignore this config flow.
2750 
2751  Ignoring a config flow works by creating a config entry with source set to
2752  SOURCE_IGNORE.
2753 
2754  There will only be a single active discovery flow per device, also when the
2755  integration has multiple discovery sources for the same device. This method
2756  is called when the user ignores a discovered device or service, we then store
2757  the key for the flow being ignored.
2758 
2759  Once the ignore config entry is created, ConfigEntriesFlowManager.async_finish_flow
2760  will make sure the discovery key is kept up to date since it may not be stable
2761  unlike the unique id.
2762  """
2763  await self.async_set_unique_idasync_set_unique_id(user_input["unique_id"], raise_on_progress=False)
2764  return self.async_create_entryasync_create_entryasync_create_entry(title=user_input["title"], data={})
2765 
2766  async def async_step_user(
2767  self, user_input: dict[str, Any] | None = None
2768  ) -> ConfigFlowResult:
2769  """Handle a flow initiated by the user."""
2770  return self.async_abortasync_abortasync_abort(reason="not_implemented")
2771 
2773  """Mark this flow discovered, without a unique identifier.
2774 
2775  If a flow initiated by discovery, doesn't have a unique ID, this can
2776  be used alternatively. It will ensure only 1 flow is started and only
2777  when the handler has no existing config entries.
2778 
2779  It ensures that the discovery can be ignored by the user.
2780 
2781  Requires `already_configured` and `already_in_progress` in strings.json
2782  in user visible flows.
2783  """
2784  if self.unique_idunique_id is not None:
2785  return
2786 
2787  # Abort if the handler has config entries already
2788  if self._async_current_entries_async_current_entries():
2789  raise data_entry_flow.AbortFlow("already_configured")
2790 
2791  # Use an special unique id to differentiate
2792  await self.async_set_unique_idasync_set_unique_id(DEFAULT_DISCOVERY_UNIQUE_ID)
2793  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
2794 
2795  # Abort if any other flow for this handler is already in progress
2796  if self._async_in_progress_async_in_progress(include_uninitialized=True):
2797  raise data_entry_flow.AbortFlow("already_in_progress")
2798 
2800  self,
2801  ) -> ConfigFlowResult:
2802  """Handle a flow initialized by discovery."""
2803  await self._async_handle_discovery_without_unique_id_async_handle_discovery_without_unique_id()
2804  return await self.async_step_userasync_step_user()
2805 
2807  self, discovery_info: DiscoveryInfoType
2808  ) -> ConfigFlowResult:
2809  """Handle a flow initialized by discovery."""
2810  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2811 
2812  @callback
2814  self,
2815  *,
2816  reason: str,
2817  description_placeholders: Mapping[str, str] | None = None,
2818  ) -> ConfigFlowResult:
2819  """Abort the config flow."""
2820  # Remove reauth notification if no reauth flows are in progress
2821  if self.sourcesourcesource == SOURCE_REAUTH and not any(
2822  ent["flow_id"] != self.flow_id
2823  for ent in self.hass.config_entries.flow.async_progress_by_handler(
2824  self.handler, match_context={"source": SOURCE_REAUTH}
2825  )
2826  ):
2827  persistent_notification.async_dismiss(
2828  self.hass, RECONFIGURE_NOTIFICATION_ID
2829  )
2830 
2831  return super().async_abort(
2832  reason=reason, description_placeholders=description_placeholders
2833  )
2834 
2836  self, discovery_info: BluetoothServiceInfoBleak
2837  ) -> ConfigFlowResult:
2838  """Handle a flow initialized by Bluetooth discovery."""
2839  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2840 
2841  async def async_step_dhcp(
2842  self, discovery_info: DhcpServiceInfo
2843  ) -> ConfigFlowResult:
2844  """Handle a flow initialized by DHCP discovery."""
2845  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2846 
2848  self, discovery_info: HassioServiceInfo
2849  ) -> ConfigFlowResult:
2850  """Handle a flow initialized by HASS IO discovery."""
2851  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2852 
2854  self, discovery_info: DiscoveryInfoType
2855  ) -> ConfigFlowResult:
2856  """Handle a flow initialized by integration specific discovery."""
2857  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2858 
2860  self, discovery_info: ZeroconfServiceInfo
2861  ) -> ConfigFlowResult:
2862  """Handle a flow initialized by Homekit discovery."""
2863  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2864 
2865  async def async_step_mqtt(
2866  self, discovery_info: MqttServiceInfo
2867  ) -> ConfigFlowResult:
2868  """Handle a flow initialized by MQTT discovery."""
2869  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2870 
2871  async def async_step_ssdp(
2872  self, discovery_info: SsdpServiceInfo
2873  ) -> ConfigFlowResult:
2874  """Handle a flow initialized by SSDP discovery."""
2875  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2876 
2877  async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResult:
2878  """Handle a flow initialized by USB discovery."""
2879  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2880 
2882  self, discovery_info: ZeroconfServiceInfo
2883  ) -> ConfigFlowResult:
2884  """Handle a flow initialized by Zeroconf discovery."""
2885  return await self._async_step_discovery_without_unique_id_async_step_discovery_without_unique_id()
2886 
2887  @callback
2888  def async_create_entry( # type: ignore[override]
2889  self,
2890  *,
2891  title: str,
2892  data: Mapping[str, Any],
2893  description: str | None = None,
2894  description_placeholders: Mapping[str, str] | None = None,
2895  options: Mapping[str, Any] | None = None,
2896  ) -> ConfigFlowResult:
2897  """Finish config flow and create a config entry."""
2898  if self.sourcesourcesource in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
2899  report_usage(
2900  f"creates a new entry in a '{self.source}' flow, "
2901  "when it is expected to update an existing entry and abort",
2902  core_behavior=ReportBehavior.LOG,
2903  breaks_in_ha_version="2025.11",
2904  integration_domain=self.handler,
2905  )
2906  result = super().async_create_entry(
2907  title=title,
2908  data=data,
2909  description=description,
2910  description_placeholders=description_placeholders,
2911  )
2912 
2913  result["minor_version"] = self.MINOR_VERSIONMINOR_VERSION
2914  result["options"] = options or {}
2915  result["version"] = self.VERSIONVERSION
2916 
2917  return result
2918 
2919  @callback
2921  self,
2922  entry: ConfigEntry,
2923  *,
2924  unique_id: str | None | UndefinedType = UNDEFINED,
2925  title: str | UndefinedType = UNDEFINED,
2926  data: Mapping[str, Any] | UndefinedType = UNDEFINED,
2927  data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED,
2928  options: Mapping[str, Any] | UndefinedType = UNDEFINED,
2929  reason: str | UndefinedType = UNDEFINED,
2930  reload_even_if_entry_is_unchanged: bool = True,
2931  ) -> ConfigFlowResult:
2932  """Update config entry, reload config entry and finish config flow.
2933 
2934  :param data: replace the entry data with new data
2935  :param data_updates: add items from data_updates to entry data - existing keys
2936  are overridden
2937  :param options: replace the entry options with new options
2938  :param title: replace the title of the entry
2939  :param unique_id: replace the unique_id of the entry
2940 
2941  :param reason: set the reason for the abort, defaults to
2942  `reauth_successful` or `reconfigure_successful` based on flow source
2943 
2944  :param reload_even_if_entry_is_unchanged: set this to `False` if the entry
2945  should not be reloaded if it is unchanged
2946  """
2947  if data_updates is not UNDEFINED:
2948  if data is not UNDEFINED:
2949  raise ValueError("Cannot set both data and data_updates")
2950  data = entry.data | data_updates
2951  result = self.hass.config_entries.async_update_entry(
2952  entry=entry,
2953  unique_id=unique_id,
2954  title=title,
2955  data=data,
2956  options=options,
2957  )
2958  if reload_even_if_entry_is_unchanged or result:
2959  self.hass.config_entries.async_schedule_reload(entry.entry_id)
2960  if reason is UNDEFINED:
2961  reason = "reauth_successful"
2962  if self.sourcesourcesource == SOURCE_RECONFIGURE:
2963  reason = "reconfigure_successful"
2964  return self.async_abortasync_abortasync_abort(reason=reason)
2965 
2966  @callback
2968  self,
2969  *,
2970  step_id: str | None = None,
2971  data_schema: vol.Schema | None = None,
2972  errors: dict[str, str] | None = None,
2973  description_placeholders: Mapping[str, str] | None = None,
2974  last_step: bool | None = None,
2975  preview: str | None = None,
2976  ) -> ConfigFlowResult:
2977  """Return the definition of a form to gather user input.
2978 
2979  The step_id parameter is deprecated and will be removed in a future release.
2980  """
2981  if self.sourcesourcesource == SOURCE_REAUTH and "entry_id" in self.context:
2982  # If the integration does not provide a name for the reauth title,
2983  # we append it to the description placeholders.
2984  # We also need to check entry_id as some integrations bypass the
2985  # reauth helpers and create a flow without it.
2986  description_placeholders = dict(description_placeholders or {})
2987  if description_placeholders.get(CONF_NAME) is None:
2988  description_placeholders[CONF_NAME] = self._get_reauth_entry_get_reauth_entry().title
2989  return super().async_show_form(
2990  step_id=step_id,
2991  data_schema=data_schema,
2992  errors=errors,
2993  description_placeholders=description_placeholders,
2994  last_step=last_step,
2995  preview=preview,
2996  )
2997 
2998  def is_matching(self, other_flow: Self) -> bool:
2999  """Return True if other_flow is matching this flow."""
3000  raise NotImplementedError
3001 
3002  @property
3003  def _reauth_entry_id(self) -> str:
3004  """Return reauth entry id."""
3005  if self.sourcesourcesource != SOURCE_REAUTH:
3006  raise ValueError(f"Source is {self.source}, expected {SOURCE_REAUTH}")
3007  return self.context["entry_id"]
3008 
3009  @callback
3010  def _get_reauth_entry(self) -> ConfigEntry:
3011  """Return the reauth config entry linked to the current context."""
3012  return self.hass.config_entries.async_get_known_entry(self._reauth_entry_id_reauth_entry_id)
3013 
3014  @property
3015  def _reconfigure_entry_id(self) -> str:
3016  """Return reconfigure entry id."""
3017  if self.sourcesourcesource != SOURCE_RECONFIGURE:
3018  raise ValueError(f"Source is {self.source}, expected {SOURCE_RECONFIGURE}")
3019  return self.context["entry_id"]
3020 
3021  @callback
3022  def _get_reconfigure_entry(self) -> ConfigEntry:
3023  """Return the reconfigure config entry linked to the current context."""
3024  return self.hass.config_entries.async_get_known_entry(
3025  self._reconfigure_entry_id_reconfigure_entry_id
3026  )
3027 
3028 
3030  data_entry_flow.FlowManager[ConfigFlowContext, ConfigFlowResult]
3031 ):
3032  """Flow to set options for a configuration entry."""
3033 
3034  _flow_result = ConfigFlowResult
3035 
3036  def _async_get_config_entry(self, config_entry_id: str) -> ConfigEntry:
3037  """Return config entry or raise if not found."""
3038  return self.hasshass.config_entries.async_get_known_entry(config_entry_id)
3039 
3041  self,
3042  handler_key: str,
3043  *,
3044  context: ConfigFlowContext | None = None,
3045  data: dict[str, Any] | None = None,
3046  ) -> OptionsFlow:
3047  """Create an options flow for a config entry.
3048 
3049  Entry_id and flow.handler is the same thing to map entry with flow.
3050  """
3051  entry = self._async_get_config_entry_async_get_config_entry(handler_key)
3052  handler = await _async_get_flow_handler(self.hasshass, entry.domain, {})
3053  return handler.async_get_options_flow(entry)
3054 
3056  self,
3057  flow: data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult],
3058  result: ConfigFlowResult,
3059  ) -> ConfigFlowResult:
3060  """Finish an options flow and update options for configuration entry.
3061 
3062  This method is called when a flow step returns FlowResultType.ABORT or
3063  FlowResultType.CREATE_ENTRY.
3064 
3065  Flow.handler and entry_id is the same thing to map flow with entry.
3066  """
3067  flow = cast(OptionsFlow, flow)
3068 
3069  if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
3070  return result
3071 
3072  entry = self.hasshass.config_entries.async_get_known_entry(flow.handler)
3073 
3074  if result["data"] is not None:
3075  self.hasshass.config_entries.async_update_entry(entry, options=result["data"])
3076 
3077  result["result"] = True
3078  return result
3079 
3081  self, flow: data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult]
3082  ) -> None:
3083  """Set up preview for an option flow handler."""
3084  entry = self._async_get_config_entry_async_get_config_entry(flow.handler)
3085  await _load_integration(self.hasshass, entry.domain, {})
3086  if entry.domain not in self._preview:
3087  self._preview.add(entry.domain)
3088  await flow.async_setup_preview(self.hasshass)
3089 
3090 
3092  """Base class for config options flows."""
3093 
3094  handler: str
3095 
3096  _config_entry: ConfigEntry
3097  """For compatibility only - to be removed in 2025.12"""
3098 
3099  @callback
3101  self, match_dict: dict[str, Any] | None = None
3102  ) -> None:
3103  """Abort if another current entry matches all data.
3104 
3105  Requires `already_configured` in strings.json in user visible flows.
3106  """
3108  [
3109  entry
3110  for entry in self.hass.config_entries.async_entries(
3111  self.config_entryconfig_entryconfig_entry.domain
3112  )
3113  if entry is not self.config_entryconfig_entryconfig_entry and entry.source != SOURCE_IGNORE
3114  ],
3115  match_dict,
3116  )
3117 
3118  @property
3119  def _config_entry_id(self) -> str:
3120  """Return config entry id.
3121 
3122  Please note that this is not available inside `__init__` method, and
3123  can only be referenced after initialisation.
3124  """
3125  # This is the same as handler, but that's an implementation detail
3126  if self.handler is None:
3127  raise ValueError(
3128  "The config entry id is not available during initialisation"
3129  )
3130  return self.handler
3131 
3132  @property
3133  def config_entry(self) -> ConfigEntry:
3134  """Return the config entry linked to the current options flow.
3135 
3136  Please note that this is not available inside `__init__` method, and
3137  can only be referenced after initialisation.
3138  """
3139  # For compatibility only - to be removed in 2025.12
3140  if hasattr(self, "_config_entry"):
3141  return self._config_entry_config_entry
3142 
3143  if self.hass is None:
3144  raise ValueError("The config entry is not available during initialisation")
3145  return self.hass.config_entries.async_get_known_entry(self._config_entry_id_config_entry_id)
3146 
3147  @config_entry.setter
3148  def config_entry(self, value: ConfigEntry) -> None:
3149  """Set the config entry value."""
3150  report_usage(
3151  "sets option flow config_entry explicitly, which is deprecated",
3152  core_behavior=ReportBehavior.ERROR,
3153  core_integration_behavior=ReportBehavior.ERROR,
3154  custom_integration_behavior=ReportBehavior.LOG,
3155  breaks_in_ha_version="2025.12",
3156  )
3157  self._config_entry_config_entry = value
3158 
3159 
3161  """Base class for options flows with config entry and options.
3162 
3163  This class is being phased out, and should not be referenced in new code.
3164  It is kept only for backward compatibility, and only for custom integrations.
3165  """
3166 
3167  def __init__(self, config_entry: ConfigEntry) -> None:
3168  """Initialize options flow."""
3169  self._config_entry_config_entry_config_entry = config_entry
3170  self._options_options = deepcopy(dict(config_entry.options))
3171  report_usage(
3172  "inherits from OptionsFlowWithConfigEntry",
3173  core_behavior=ReportBehavior.ERROR,
3174  core_integration_behavior=ReportBehavior.ERROR,
3175  custom_integration_behavior=ReportBehavior.IGNORE,
3176  )
3177 
3178  @property
3179  def options(self) -> dict[str, Any]:
3180  """Return a mutable copy of the config entry options."""
3181  return self._options_options
3182 
3183 
3185  """Handler when entities related to config entries updated disabled_by."""
3186 
3187  def __init__(self, hass: HomeAssistant) -> None:
3188  """Initialize the handler."""
3189  self.hasshass = hass
3190  self.registryregistry: er.EntityRegistry | None = None
3191  self.changedchanged: set[str] = set()
3192  self._remove_call_later_remove_call_later: Callable[[], None] | None = None
3193 
3194  @callback
3195  def async_setup(self) -> None:
3196  """Set up the disable handler."""
3197  self.hasshass.bus.async_listen(
3198  er.EVENT_ENTITY_REGISTRY_UPDATED,
3199  self._handle_entry_updated_handle_entry_updated,
3200  event_filter=_handle_entry_updated_filter,
3201  )
3202 
3203  @callback
3205  self, event: Event[er.EventEntityRegistryUpdatedData]
3206  ) -> None:
3207  """Handle entity registry entry update."""
3208  if self.registryregistry is None:
3209  self.registryregistry = er.async_get(self.hasshass)
3210 
3211  entity_entry = self.registryregistry.async_get(event.data["entity_id"])
3212 
3213  if (
3214  # Stop if no entry found
3215  entity_entry is None
3216  # Stop if entry not connected to config entry
3217  or entity_entry.config_entry_id is None
3218  # Stop if the entry got disabled. In that case the entity handles it
3219  # themselves.
3220  or entity_entry.disabled_by
3221  ):
3222  return
3223 
3224  config_entry = self.hasshass.config_entries.async_get_known_entry(
3225  entity_entry.config_entry_id
3226  )
3227 
3228  if config_entry.entry_id not in self.changedchanged and config_entry.supports_unload:
3229  self.changedchanged.add(config_entry.entry_id)
3230 
3231  if not self.changedchanged:
3232  return
3233 
3234  # We are going to delay reloading on *every* entity registry change so that
3235  # if a user is happily clicking along, it will only reload at the end.
3236 
3237  if self._remove_call_later_remove_call_later:
3238  self._remove_call_later_remove_call_later()
3239 
3240  self._remove_call_later_remove_call_later = async_call_later(
3241  self.hasshass,
3242  RELOAD_AFTER_UPDATE_DELAY,
3243  HassJob(self._async_handle_reload_async_handle_reload, cancel_on_shutdown=True),
3244  )
3245 
3246  @callback
3247  def _async_handle_reload(self, _now: Any) -> None:
3248  """Handle a reload."""
3249  self._remove_call_later_remove_call_later = None
3250  to_reload = self.changedchanged
3251  self.changedchanged = set()
3252 
3253  _LOGGER.info(
3254  (
3255  "Reloading configuration entries because disabled_by changed in entity"
3256  " registry: %s"
3257  ),
3258  ", ".join(to_reload),
3259  )
3260  for entry_id in to_reload:
3261  self.hasshass.config_entries.async_schedule_reload(entry_id)
3262 
3263 
3264 @callback
3266  event_data: er.EventEntityRegistryUpdatedData,
3267 ) -> bool:
3268  """Handle entity registry entry update filter.
3269 
3270  Only handle changes to "disabled_by".
3271  If "disabled_by" was CONFIG_ENTRY, reload is not needed.
3272  """
3273  return not (
3274  event_data["action"] != "update"
3275  or "disabled_by" not in event_data["changes"]
3276  or event_data["changes"]["disabled_by"] is er.RegistryEntryDisabler.CONFIG_ENTRY
3277  )
3278 
3279 
3280 async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool:
3281  """Test if a domain supports entry unloading."""
3282  integration = await loader.async_get_integration(hass, domain)
3283  component = await integration.async_get_component()
3284  return hasattr(component, "async_unload_entry")
3285 
3286 
3287 async def support_remove_from_device(hass: HomeAssistant, domain: str) -> bool:
3288  """Test if a domain supports being removed from a device."""
3289  integration = await loader.async_get_integration(hass, domain)
3290  component = await integration.async_get_component()
3291  return hasattr(component, "async_remove_config_entry_device")
3292 
3293 
3294 async def _support_single_config_entry_only(hass: HomeAssistant, domain: str) -> bool:
3295  """Test if a domain supports only a single config entry."""
3296  integration = await loader.async_get_integration(hass, domain)
3297  return integration.single_config_entry
3298 
3299 
3301  hass: HomeAssistant, domain: str, hass_config: ConfigType
3302 ) -> None:
3303  try:
3304  integration = await loader.async_get_integration(hass, domain)
3305  except loader.IntegrationNotFound as err:
3306  _LOGGER.error("Cannot find integration %s", domain)
3307  raise data_entry_flow.UnknownHandler from err
3308 
3309  # Make sure requirements and dependencies of component are resolved
3310  await async_process_deps_reqs(hass, hass_config, integration)
3311  try:
3312  await integration.async_get_platform("config_flow")
3313  except ImportError as err:
3314  _LOGGER.error(
3315  "Error occurred loading flow for integration %s: %s",
3316  domain,
3317  err,
3318  )
3319  raise data_entry_flow.UnknownHandler from err
3320 
3321 
3323  hass: HomeAssistant, domain: str, hass_config: ConfigType
3324 ) -> type[ConfigFlow]:
3325  """Get a flow handler for specified domain."""
3326 
3327  # First check if there is a handler registered for the domain
3328  if loader.is_component_module_loaded(hass, f"{domain}.config_flow") and (
3329  handler := HANDLERS.get(domain)
3330  ):
3331  return handler
3332 
3333  await _load_integration(hass, domain, hass_config)
3334 
3335  if handler := HANDLERS.get(domain):
3336  return handler
3337 
ConfigFlowResult async_finish_flow(self, data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult] flow, ConfigFlowResult result)
None async_post_init(self, data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult] flow, ConfigFlowResult result)
None __init__(self, HomeAssistant hass, ConfigEntries config_entries, ConfigType hass_config)
ConfigFlowResult async_init(self, str handler, *ConfigFlowContext|None context=None, Any data=None)
tuple[ConfigFlow, ConfigFlowResult] _async_init(self, str flow_id, str handler, ConfigFlowContext context, Any data)
ConfigFlow async_create_flow(self, str handler_key, *ConfigFlowContext|None context=None, Any data=None)
bool async_has_matching_discovery_flow(self, str handler, ConfigFlowContext match_context, Any data)
dict[str, Any] async_remove(self, str entry_id)
ConfigEntry|None async_entry_for_domain_unique_id(self, str domain, str unique_id)
None _async_forward_entry_setups_locked(self, ConfigEntry entry, Iterable[Platform|str] platforms)
bool async_unload(self, str entry_id, bool _lock=True)
list[str] async_domains(self, bool include_ignore=False, bool include_disabled=False)
None async_schedule_reload(self, str entry_id)
ConfigEntry|None async_get_entry(self, str entry_id)
None async_forward_entry_setups(self, ConfigEntry entry, Iterable[Platform|str] platforms)
bool async_setup(self, str entry_id, bool _lock=True)
bool async_has_entries(self, str domain, bool include_ignore=True, bool include_disabled=True)
dict[str, list[dict[str, Any]]] _data_to_save(self)
None _async_clean_up(self, ConfigEntry entry)
bool async_forward_entry_unload(self, ConfigEntry entry, Platform|str domain)
bool async_set_disabled_by(self, str entry_id, ConfigEntryDisabler|None disabled_by)
list[ConfigEntry] async_entries(self, str|None domain=None, bool include_ignore=True, bool include_disabled=True)
tuple[bool, ConfigEntry] _async_remove(self, str entry_id)
bool async_forward_entry_setup(self, ConfigEntry entry, Platform|str domain)
bool async_wait_component(self, ConfigEntry entry)
None async_add(self, ConfigEntry entry)
ConfigEntry async_get_known_entry(self, str entry_id)
None __init__(self, HomeAssistant hass, ConfigType hass_config)
None _async_dispatch(self, ConfigEntryChange change_type, ConfigEntry entry)
bool async_unload_platforms(self, ConfigEntry entry, Iterable[Platform|str] platforms)
bool async_update_entry(self, ConfigEntry entry, *Mapping[str, Any]|UndefinedType data=UNDEFINED, MappingProxyType[str, tuple[DiscoveryKey,...]]|UndefinedType discovery_keys=UNDEFINED, int|UndefinedType minor_version=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, bool|UndefinedType pref_disable_new_entities=UNDEFINED, bool|UndefinedType pref_disable_polling=UNDEFINED, str|UndefinedType title=UNDEFINED, str|None|UndefinedType unique_id=UNDEFINED, int|UndefinedType version=UNDEFINED)
list[ConfigEntry] async_loaded_entries(self, str domain)
bool _async_forward_entry_setup(self, ConfigEntry entry, Platform|str domain, bool preload_platform)
ConfigEntry|None get_entry_by_domain_and_unique_id(self, str domain, str unique_id)
None update_unique_id(self, ConfigEntry entry, str|None new_unique_id)
None __init__(self, HomeAssistant hass)
None __setitem__(self, str entry_id, ConfigEntry entry)
None check_unique_id(self, ConfigEntry entry)
list[ConfigEntry] get_entries_for_domain(self, str domain)
None _index_entry(self, ConfigEntry entry)
Self __new__(cls, str value, bool recoverable)
None __init__(self, HomeAssistant hass)
dict[str, Any] _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, Any] old_data)
None async_setup(self, HomeAssistant hass, *loader.Integration|None integration=None)
None async_start_reauth(self, HomeAssistant hass, ConfigFlowContext|None context=None, dict[str, Any]|None data=None)
None async_remove(self, HomeAssistant hass)
CALLBACK_TYPE add_update_listener(self, UpdateListenerType listener)
Generator[ConfigFlowResult] async_get_active_flows(self, HomeAssistant hass, set[str] sources)
None __init__(self, *datetime|None created_at=None, Mapping[str, Any] data, ConfigEntryDisabler|None disabled_by=None, MappingProxyType[str, tuple[DiscoveryKey,...]] discovery_keys, str domain, str|None entry_id=None, int minor_version, datetime|None modified_at=None, Mapping[str, Any]|None options, bool|None pref_disable_new_entities=None, bool|None pref_disable_polling=None, str source, ConfigEntryState state=ConfigEntryState.NOT_LOADED, str title, str|None unique_id, int version)
None _async_setup_again(self, HomeAssistant hass, *Any _)
bool async_migrate(self, HomeAssistant hass)
bool async_unload(self, HomeAssistant hass, *loader.Integration|None integration=None)
None _async_set_state(self, HomeAssistant hass, ConfigEntryState state, str|None reason, str|None error_reason_translation_key=None, dict[str, str]|None error_reason_translation_placeholders=None)
None _async_init_reauth(self, HomeAssistant hass, ConfigFlowContext|None context=None, dict[str, Any]|None data=None)
None async_on_unload(self, Callable[[], Coroutine[Any, Any, None]|None] func)
None async_setup_locked(self, HomeAssistant hass, loader.Integration|None integration=None)
None _async_process_on_unload(self, HomeAssistant hass)
None __async_setup_with_context(self, HomeAssistant hass, loader.Integration|None integration)
None __setattr__(self, str key, Any value)
bool async_supports_options_flow(cls, ConfigEntry config_entry)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigFlowResult async_step_dhcp(self, DhcpServiceInfo discovery_info)
ConfigFlowResult async_step_usb(self, UsbServiceInfo discovery_info)
set[str|None] _async_current_ids(self, bool include_ignore=True)
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_step_ssdp(self, SsdpServiceInfo discovery_info)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_step_ignore(self, dict[str, Any] user_input)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
list[ConfigFlowResult] _async_in_progress(self, bool include_uninitialized=False, dict[str, Any]|None match_context=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_step_mqtt(self, MqttServiceInfo discovery_info)
ConfigFlowResult async_step_hassio(self, HassioServiceInfo discovery_info)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult _async_step_discovery_without_unique_id(self)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_step_discovery(self, DiscoveryInfoType discovery_info)
ConfigFlowResult async_step_zeroconf(self, ZeroconfServiceInfo discovery_info)
ConfigFlowResult async_step_integration_discovery(self, DiscoveryInfoType discovery_info)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
bool is_matching(self, Self other_flow)
ConfigFlowResult async_step_bluetooth(self, BluetoothServiceInfoBleak discovery_info)
ConfigFlowResult async_step_homekit(self, ZeroconfServiceInfo discovery_info)
None __init_subclass__(cls, *str|None domain=None, **Any kwargs)
None _abort_if_unique_id_mismatch(self, *str reason="unique_id_mismatch", Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult 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)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
None _handle_entry_updated(self, Event[er.EventEntityRegistryUpdatedData] event)
OptionsFlow async_create_flow(self, str handler_key, *ConfigFlowContext|None context=None, dict[str, Any]|None data=None)
None _async_setup_preview(self, data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult] flow)
ConfigFlowResult async_finish_flow(self, data_entry_flow.FlowHandler[ConfigFlowContext, ConfigFlowResult] flow, ConfigFlowResult result)
ConfigEntry _async_get_config_entry(self, str config_entry_id)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
None config_entry(self, ConfigEntry value)
int VERSION
_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)
int MINOR_VERSION
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)
hass
_FlowResultT _async_handle_step(self, FlowHandler[_FlowContextT, _FlowResultT, _HandlerT] flow, str step_id, dict|BaseServiceInfo|None user_input)
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)
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
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
Definition: __init__.py:168
None async_update_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:216
MealieConfigEntry async_get_entry(HomeAssistant hass, str config_entry_id)
Definition: services.py:101
bool _support_single_config_entry_only(HomeAssistant hass, str domain)
None _report_non_awaited_platform_forwards(ConfigEntry entry, str what)
None _async_abort_entries_match(list[ConfigEntry] other_entries, dict[str, Any]|None match_dict=None)
bool _handle_entry_updated_filter(er.EventEntityRegistryUpdatedData event_data)
bool support_entry_unload(HomeAssistant hass, str domain)
type[ConfigFlow] _async_get_flow_handler(HomeAssistant hass, str domain, ConfigType hass_config)
None _load_integration(HomeAssistant hass, str domain, ConfigType hass_config)
bool support_remove_from_device(HomeAssistant hass, str domain)
SignalType[ConfigEntry] signal_discovered_config_entry_removed(str discovery_domain)
None _validate_item(*ConfigEntryDisabler|Any|None disabled_by=None)
None async_load(HomeAssistant hass)
AreaRegistry async_get(HomeAssistant hass)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597
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
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
Definition: storage.py:444
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
Generator[None] async_pause_setup(core.HomeAssistant hass, SetupPhases phase)
Definition: setup.py:691
Generator[None] async_start_setup(core.HomeAssistant hass, str integration, SetupPhases phase, str|None group=None)
Definition: setup.py:739
None async_process_deps_reqs(core.HomeAssistant hass, ConfigType config, loader.Integration integration)
Definition: setup.py:561
bool async_setup_component(core.HomeAssistant hass, str domain, ConfigType config)
Definition: setup.py:147