1609 lines
		
	
	
		
			56 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			1609 lines
		
	
	
		
			56 KiB
		
	
	
	
		
			Python
		
	
	
"""Manage config entries in Home Assistant."""
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import asyncio
 | 
						|
from collections.abc import Awaitable, Callable, Iterable, Mapping
 | 
						|
from contextvars import ContextVar
 | 
						|
import dataclasses
 | 
						|
from enum import Enum
 | 
						|
import functools
 | 
						|
import logging
 | 
						|
from types import MappingProxyType, MethodType
 | 
						|
from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast
 | 
						|
import weakref
 | 
						|
 | 
						|
from . import data_entry_flow, loader
 | 
						|
from .backports.enum import StrEnum
 | 
						|
from .components import persistent_notification
 | 
						|
from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform
 | 
						|
from .core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
 | 
						|
from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError
 | 
						|
from .helpers import device_registry, entity_registry
 | 
						|
from .helpers.event import async_call_later
 | 
						|
from .helpers.frame import report
 | 
						|
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
 | 
						|
from .setup import async_process_deps_reqs, async_setup_component
 | 
						|
from .util import uuid as uuid_util
 | 
						|
from .util.decorator import Registry
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from .components.dhcp import DhcpServiceInfo
 | 
						|
    from .components.hassio import HassioServiceInfo
 | 
						|
    from .components.mqtt import MqttServiceInfo
 | 
						|
    from .components.ssdp import SsdpServiceInfo
 | 
						|
    from .components.usb import UsbServiceInfo
 | 
						|
    from .components.zeroconf import ZeroconfServiceInfo
 | 
						|
 | 
						|
_LOGGER = logging.getLogger(__name__)
 | 
						|
 | 
						|
SOURCE_DISCOVERY = "discovery"
 | 
						|
SOURCE_HASSIO = "hassio"
 | 
						|
SOURCE_HOMEKIT = "homekit"
 | 
						|
SOURCE_IMPORT = "import"
 | 
						|
SOURCE_INTEGRATION_DISCOVERY = "integration_discovery"
 | 
						|
SOURCE_MQTT = "mqtt"
 | 
						|
SOURCE_SSDP = "ssdp"
 | 
						|
SOURCE_USB = "usb"
 | 
						|
SOURCE_USER = "user"
 | 
						|
SOURCE_ZEROCONF = "zeroconf"
 | 
						|
SOURCE_DHCP = "dhcp"
 | 
						|
 | 
						|
# If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow
 | 
						|
# websocket command creates a config entry with this source and while it exists normal discoveries
 | 
						|
# with the same unique id are ignored.
 | 
						|
SOURCE_IGNORE = "ignore"
 | 
						|
 | 
						|
# This is used when a user uses the "Stop Ignoring" button in the UI (the
 | 
						|
# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has
 | 
						|
# been removed and unloaded.
 | 
						|
SOURCE_UNIGNORE = "unignore"
 | 
						|
 | 
						|
# This is used to signal that re-authentication is required by the user.
 | 
						|
SOURCE_REAUTH = "reauth"
 | 
						|
 | 
						|
HANDLERS = Registry()
 | 
						|
 | 
						|
STORAGE_KEY = "core.config_entries"
 | 
						|
STORAGE_VERSION = 1
 | 
						|
 | 
						|
# Deprecated since 0.73
 | 
						|
PATH_CONFIG = ".config_entries.json"
 | 
						|
 | 
						|
SAVE_DELAY = 1
 | 
						|
 | 
						|
_T = TypeVar("_T", bound="ConfigEntryState")
 | 
						|
 | 
						|
 | 
						|
class ConfigEntryState(Enum):
 | 
						|
    """Config entry state."""
 | 
						|
 | 
						|
    LOADED = "loaded", True
 | 
						|
    """The config entry has been set up successfully"""
 | 
						|
    SETUP_ERROR = "setup_error", True
 | 
						|
    """There was an error while trying to set up this config entry"""
 | 
						|
    MIGRATION_ERROR = "migration_error", False
 | 
						|
    """There was an error while trying to migrate the config entry to a new version"""
 | 
						|
    SETUP_RETRY = "setup_retry", True
 | 
						|
    """The config entry was not ready to be set up yet, but might be later"""
 | 
						|
    NOT_LOADED = "not_loaded", True
 | 
						|
    """The config entry has not been loaded"""
 | 
						|
    FAILED_UNLOAD = "failed_unload", False
 | 
						|
    """An error occurred when trying to unload the entry"""
 | 
						|
 | 
						|
    _recoverable: bool
 | 
						|
 | 
						|
    def __new__(cls: type[_T], value: str, recoverable: bool) -> _T:
 | 
						|
        """Create new ConfigEntryState."""
 | 
						|
        obj = object.__new__(cls)
 | 
						|
        obj._value_ = value
 | 
						|
        obj._recoverable = recoverable
 | 
						|
        return obj
 | 
						|
 | 
						|
    @property
 | 
						|
    def recoverable(self) -> bool:
 | 
						|
        """Get if the state is recoverable."""
 | 
						|
        return self._recoverable
 | 
						|
 | 
						|
 | 
						|
DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id"
 | 
						|
DISCOVERY_NOTIFICATION_ID = "config_entry_discovery"
 | 
						|
DISCOVERY_SOURCES = (
 | 
						|
    SOURCE_SSDP,
 | 
						|
    SOURCE_USB,
 | 
						|
    SOURCE_DHCP,
 | 
						|
    SOURCE_HOMEKIT,
 | 
						|
    SOURCE_ZEROCONF,
 | 
						|
    SOURCE_HOMEKIT,
 | 
						|
    SOURCE_DHCP,
 | 
						|
    SOURCE_DISCOVERY,
 | 
						|
    SOURCE_IMPORT,
 | 
						|
    SOURCE_INTEGRATION_DISCOVERY,
 | 
						|
    SOURCE_UNIGNORE,
 | 
						|
)
 | 
						|
 | 
						|
RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure"
 | 
						|
 | 
						|
EVENT_FLOW_DISCOVERED = "config_entry_discovered"
 | 
						|
 | 
						|
 | 
						|
class ConfigEntryDisabler(StrEnum):
 | 
						|
    """What disabled a config entry."""
 | 
						|
 | 
						|
    USER = "user"
 | 
						|
 | 
						|
 | 
						|
# DISABLED_* is deprecated, to be removed in 2022.3
 | 
						|
DISABLED_USER = ConfigEntryDisabler.USER.value
 | 
						|
 | 
						|
RELOAD_AFTER_UPDATE_DELAY = 30
 | 
						|
 | 
						|
# Deprecated: Connection classes
 | 
						|
# These aren't used anymore since 2021.6.0
 | 
						|
# Mainly here not to break custom integrations.
 | 
						|
CONN_CLASS_CLOUD_PUSH = "cloud_push"
 | 
						|
CONN_CLASS_CLOUD_POLL = "cloud_poll"
 | 
						|
CONN_CLASS_LOCAL_PUSH = "local_push"
 | 
						|
CONN_CLASS_LOCAL_POLL = "local_poll"
 | 
						|
CONN_CLASS_ASSUMED = "assumed"
 | 
						|
CONN_CLASS_UNKNOWN = "unknown"
 | 
						|
 | 
						|
 | 
						|
class ConfigError(HomeAssistantError):
 | 
						|
    """Error while configuring an account."""
 | 
						|
 | 
						|
 | 
						|
class UnknownEntry(ConfigError):
 | 
						|
    """Unknown entry specified."""
 | 
						|
 | 
						|
 | 
						|
class OperationNotAllowed(ConfigError):
 | 
						|
    """Raised when a config entry operation is not allowed."""
 | 
						|
 | 
						|
 | 
						|
UpdateListenerType = Callable[[HomeAssistant, "ConfigEntry"], Awaitable[None]]
 | 
						|
 | 
						|
 | 
						|
class ConfigEntry:
 | 
						|
    """Hold a configuration entry."""
 | 
						|
 | 
						|
    __slots__ = (
 | 
						|
        "entry_id",
 | 
						|
        "version",
 | 
						|
        "domain",
 | 
						|
        "title",
 | 
						|
        "data",
 | 
						|
        "options",
 | 
						|
        "unique_id",
 | 
						|
        "supports_unload",
 | 
						|
        "pref_disable_new_entities",
 | 
						|
        "pref_disable_polling",
 | 
						|
        "source",
 | 
						|
        "state",
 | 
						|
        "disabled_by",
 | 
						|
        "_setup_lock",
 | 
						|
        "update_listeners",
 | 
						|
        "reason",
 | 
						|
        "_async_cancel_retry_setup",
 | 
						|
        "_on_unload",
 | 
						|
    )
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        version: int,
 | 
						|
        domain: str,
 | 
						|
        title: str,
 | 
						|
        data: Mapping[str, Any],
 | 
						|
        source: str,
 | 
						|
        pref_disable_new_entities: bool | None = None,
 | 
						|
        pref_disable_polling: bool | None = None,
 | 
						|
        options: Mapping[str, Any] | None = None,
 | 
						|
        unique_id: str | None = None,
 | 
						|
        entry_id: str | None = None,
 | 
						|
        state: ConfigEntryState = ConfigEntryState.NOT_LOADED,
 | 
						|
        disabled_by: ConfigEntryDisabler | None = None,
 | 
						|
    ) -> None:
 | 
						|
        """Initialize a config entry."""
 | 
						|
        # Unique id of the config entry
 | 
						|
        self.entry_id = entry_id or uuid_util.random_uuid_hex()
 | 
						|
 | 
						|
        # Version of the configuration.
 | 
						|
        self.version = version
 | 
						|
 | 
						|
        # Domain the configuration belongs to
 | 
						|
        self.domain = domain
 | 
						|
 | 
						|
        # Title of the configuration
 | 
						|
        self.title = title
 | 
						|
 | 
						|
        # Config data
 | 
						|
        self.data = MappingProxyType(data)
 | 
						|
 | 
						|
        # Entry options
 | 
						|
        self.options = MappingProxyType(options or {})
 | 
						|
 | 
						|
        # Entry system options
 | 
						|
        if pref_disable_new_entities is None:
 | 
						|
            pref_disable_new_entities = False
 | 
						|
 | 
						|
        self.pref_disable_new_entities = pref_disable_new_entities
 | 
						|
 | 
						|
        if pref_disable_polling is None:
 | 
						|
            pref_disable_polling = False
 | 
						|
 | 
						|
        self.pref_disable_polling = pref_disable_polling
 | 
						|
 | 
						|
        # Source of the configuration (user, discovery, cloud)
 | 
						|
        self.source = source
 | 
						|
 | 
						|
        # State of the entry (LOADED, NOT_LOADED)
 | 
						|
        self.state = state
 | 
						|
 | 
						|
        # Unique ID of this entry.
 | 
						|
        self.unique_id = unique_id
 | 
						|
 | 
						|
        # Config entry is disabled
 | 
						|
        if isinstance(disabled_by, str) and not isinstance(
 | 
						|
            disabled_by, ConfigEntryDisabler
 | 
						|
        ):
 | 
						|
            report(  # type: ignore[unreachable]
 | 
						|
                "uses str for config entry disabled_by. This is deprecated and will "
 | 
						|
                "stop working in Home Assistant 2022.3, it should be updated to use "
 | 
						|
                "ConfigEntryDisabler instead",
 | 
						|
                error_if_core=False,
 | 
						|
            )
 | 
						|
            disabled_by = ConfigEntryDisabler(disabled_by)
 | 
						|
        self.disabled_by = disabled_by
 | 
						|
 | 
						|
        # Supports unload
 | 
						|
        self.supports_unload = False
 | 
						|
 | 
						|
        # Listeners to call on update
 | 
						|
        self.update_listeners: list[
 | 
						|
            weakref.ReferenceType[UpdateListenerType] | weakref.WeakMethod
 | 
						|
        ] = []
 | 
						|
 | 
						|
        # Reason why config entry is in a failed state
 | 
						|
        self.reason: str | None = None
 | 
						|
 | 
						|
        # Function to cancel a scheduled retry
 | 
						|
        self._async_cancel_retry_setup: Callable[[], Any] | None = None
 | 
						|
 | 
						|
        # Hold list for functions to call on unload.
 | 
						|
        self._on_unload: list[CALLBACK_TYPE] | None = None
 | 
						|
 | 
						|
    async def async_setup(
 | 
						|
        self,
 | 
						|
        hass: HomeAssistant,
 | 
						|
        *,
 | 
						|
        integration: loader.Integration | None = None,
 | 
						|
        tries: int = 0,
 | 
						|
    ) -> None:
 | 
						|
        """Set up an entry."""
 | 
						|
        current_entry.set(self)
 | 
						|
        if self.source == SOURCE_IGNORE or self.disabled_by:
 | 
						|
            return
 | 
						|
 | 
						|
        if integration is None:
 | 
						|
            integration = await loader.async_get_integration(hass, self.domain)
 | 
						|
 | 
						|
        self.supports_unload = await support_entry_unload(hass, self.domain)
 | 
						|
 | 
						|
        try:
 | 
						|
            component = integration.get_component()
 | 
						|
        except ImportError as err:
 | 
						|
            _LOGGER.error(
 | 
						|
                "Error importing integration %s to set up %s configuration entry: %s",
 | 
						|
                integration.domain,
 | 
						|
                self.domain,
 | 
						|
                err,
 | 
						|
            )
 | 
						|
            if self.domain == integration.domain:
 | 
						|
                self.state = ConfigEntryState.SETUP_ERROR
 | 
						|
                self.reason = "Import error"
 | 
						|
            return
 | 
						|
 | 
						|
        if self.domain == integration.domain:
 | 
						|
            try:
 | 
						|
                integration.get_platform("config_flow")
 | 
						|
            except ImportError as err:
 | 
						|
                _LOGGER.error(
 | 
						|
                    "Error importing platform config_flow from integration %s to set up %s configuration entry: %s",
 | 
						|
                    integration.domain,
 | 
						|
                    self.domain,
 | 
						|
                    err,
 | 
						|
                )
 | 
						|
                self.state = ConfigEntryState.SETUP_ERROR
 | 
						|
                self.reason = "Import error"
 | 
						|
                return
 | 
						|
 | 
						|
            # Perform migration
 | 
						|
            if not await self.async_migrate(hass):
 | 
						|
                self.state = ConfigEntryState.MIGRATION_ERROR
 | 
						|
                self.reason = None
 | 
						|
                return
 | 
						|
 | 
						|
        error_reason = None
 | 
						|
 | 
						|
        try:
 | 
						|
            result = await component.async_setup_entry(hass, self)
 | 
						|
 | 
						|
            if not isinstance(result, bool):
 | 
						|
                _LOGGER.error(
 | 
						|
                    "%s.async_setup_entry did not return boolean", integration.domain
 | 
						|
                )
 | 
						|
                result = False
 | 
						|
        except ConfigEntryAuthFailed as ex:
 | 
						|
            message = str(ex)
 | 
						|
            auth_base_message = "could not authenticate"
 | 
						|
            error_reason = message or auth_base_message
 | 
						|
            auth_message = (
 | 
						|
                f"{auth_base_message}: {message}" if message else auth_base_message
 | 
						|
            )
 | 
						|
            _LOGGER.warning(
 | 
						|
                "Config entry '%s' for %s integration %s",
 | 
						|
                self.title,
 | 
						|
                self.domain,
 | 
						|
                auth_message,
 | 
						|
            )
 | 
						|
            self._async_process_on_unload()
 | 
						|
            self.async_start_reauth(hass)
 | 
						|
            result = False
 | 
						|
        except ConfigEntryNotReady as ex:
 | 
						|
            self.state = ConfigEntryState.SETUP_RETRY
 | 
						|
            self.reason = str(ex) or None
 | 
						|
            wait_time = 2 ** min(tries, 4) * 5
 | 
						|
            tries += 1
 | 
						|
            message = str(ex)
 | 
						|
            ready_message = f"ready yet: {message}" if message else "ready yet"
 | 
						|
            if tries == 1:
 | 
						|
                _LOGGER.warning(
 | 
						|
                    "Config entry '%s' for %s integration not %s; Retrying in background",
 | 
						|
                    self.title,
 | 
						|
                    self.domain,
 | 
						|
                    ready_message,
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                _LOGGER.debug(
 | 
						|
                    "Config entry '%s' for %s integration not %s; Retrying in %d seconds",
 | 
						|
                    self.title,
 | 
						|
                    self.domain,
 | 
						|
                    ready_message,
 | 
						|
                    wait_time,
 | 
						|
                )
 | 
						|
 | 
						|
            async def setup_again(*_: Any) -> None:
 | 
						|
                """Run setup again."""
 | 
						|
                self._async_cancel_retry_setup = None
 | 
						|
                await self.async_setup(hass, integration=integration, tries=tries)
 | 
						|
 | 
						|
            if hass.state == CoreState.running:
 | 
						|
                self._async_cancel_retry_setup = async_call_later(
 | 
						|
                    hass, wait_time, setup_again
 | 
						|
                )
 | 
						|
            else:
 | 
						|
                self._async_cancel_retry_setup = hass.bus.async_listen_once(
 | 
						|
                    EVENT_HOMEASSISTANT_STARTED, setup_again
 | 
						|
                )
 | 
						|
 | 
						|
            self._async_process_on_unload()
 | 
						|
            return
 | 
						|
        except Exception:  # pylint: disable=broad-except
 | 
						|
            _LOGGER.exception(
 | 
						|
                "Error setting up entry %s for %s", self.title, integration.domain
 | 
						|
            )
 | 
						|
            result = False
 | 
						|
 | 
						|
        # Only store setup result as state if it was not forwarded.
 | 
						|
        if self.domain != integration.domain:
 | 
						|
            return
 | 
						|
 | 
						|
        if result:
 | 
						|
            self.state = ConfigEntryState.LOADED
 | 
						|
            self.reason = None
 | 
						|
        else:
 | 
						|
            self.state = ConfigEntryState.SETUP_ERROR
 | 
						|
            self.reason = error_reason
 | 
						|
 | 
						|
    async def async_shutdown(self) -> None:
 | 
						|
        """Call when Home Assistant is stopping."""
 | 
						|
        self.async_cancel_retry_setup()
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_cancel_retry_setup(self) -> None:
 | 
						|
        """Cancel retry setup."""
 | 
						|
        if self._async_cancel_retry_setup is not None:
 | 
						|
            self._async_cancel_retry_setup()
 | 
						|
            self._async_cancel_retry_setup = None
 | 
						|
 | 
						|
    async def async_unload(
 | 
						|
        self, hass: HomeAssistant, *, integration: loader.Integration | None = None
 | 
						|
    ) -> bool:
 | 
						|
        """Unload an entry.
 | 
						|
 | 
						|
        Returns if unload is possible and was successful.
 | 
						|
        """
 | 
						|
        if self.source == SOURCE_IGNORE:
 | 
						|
            self.state = ConfigEntryState.NOT_LOADED
 | 
						|
            self.reason = None
 | 
						|
            return True
 | 
						|
 | 
						|
        if self.state == ConfigEntryState.NOT_LOADED:
 | 
						|
            return True
 | 
						|
 | 
						|
        if integration is None:
 | 
						|
            try:
 | 
						|
                integration = await loader.async_get_integration(hass, self.domain)
 | 
						|
            except loader.IntegrationNotFound:
 | 
						|
                # The integration was likely a custom_component
 | 
						|
                # that was uninstalled, or an integration
 | 
						|
                # that has been renamed without removing the config
 | 
						|
                # entry.
 | 
						|
                self.state = ConfigEntryState.NOT_LOADED
 | 
						|
                self.reason = None
 | 
						|
                return True
 | 
						|
 | 
						|
        component = integration.get_component()
 | 
						|
 | 
						|
        if integration.domain == self.domain:
 | 
						|
            if not self.state.recoverable:
 | 
						|
                return False
 | 
						|
 | 
						|
            if self.state is not ConfigEntryState.LOADED:
 | 
						|
                self.async_cancel_retry_setup()
 | 
						|
 | 
						|
                self.state = ConfigEntryState.NOT_LOADED
 | 
						|
                self.reason = None
 | 
						|
                return True
 | 
						|
 | 
						|
        supports_unload = hasattr(component, "async_unload_entry")
 | 
						|
 | 
						|
        if not supports_unload:
 | 
						|
            if integration.domain == self.domain:
 | 
						|
                self.state = ConfigEntryState.FAILED_UNLOAD
 | 
						|
                self.reason = "Unload not supported"
 | 
						|
            return False
 | 
						|
 | 
						|
        try:
 | 
						|
            result = await component.async_unload_entry(hass, self)
 | 
						|
 | 
						|
            assert isinstance(result, bool)
 | 
						|
 | 
						|
            # Only adjust state if we unloaded the component
 | 
						|
            if result and integration.domain == self.domain:
 | 
						|
                self.state = ConfigEntryState.NOT_LOADED
 | 
						|
                self.reason = None
 | 
						|
 | 
						|
            self._async_process_on_unload()
 | 
						|
 | 
						|
            # https://github.com/python/mypy/issues/11839
 | 
						|
            return result  # type: ignore[no-any-return]
 | 
						|
        except Exception:  # pylint: disable=broad-except
 | 
						|
            _LOGGER.exception(
 | 
						|
                "Error unloading entry %s for %s", self.title, integration.domain
 | 
						|
            )
 | 
						|
            if integration.domain == self.domain:
 | 
						|
                self.state = ConfigEntryState.FAILED_UNLOAD
 | 
						|
                self.reason = "Unknown error"
 | 
						|
            return False
 | 
						|
 | 
						|
    async def async_remove(self, hass: HomeAssistant) -> None:
 | 
						|
        """Invoke remove callback on component."""
 | 
						|
        if self.source == SOURCE_IGNORE:
 | 
						|
            return
 | 
						|
 | 
						|
        try:
 | 
						|
            integration = await loader.async_get_integration(hass, self.domain)
 | 
						|
        except loader.IntegrationNotFound:
 | 
						|
            # The integration was likely a custom_component
 | 
						|
            # that was uninstalled, or an integration
 | 
						|
            # that has been renamed without removing the config
 | 
						|
            # entry.
 | 
						|
            return
 | 
						|
 | 
						|
        component = integration.get_component()
 | 
						|
        if not hasattr(component, "async_remove_entry"):
 | 
						|
            return
 | 
						|
        try:
 | 
						|
            await component.async_remove_entry(hass, self)
 | 
						|
        except Exception:  # pylint: disable=broad-except
 | 
						|
            _LOGGER.exception(
 | 
						|
                "Error calling entry remove callback %s for %s",
 | 
						|
                self.title,
 | 
						|
                integration.domain,
 | 
						|
            )
 | 
						|
 | 
						|
    async def async_migrate(self, hass: HomeAssistant) -> bool:
 | 
						|
        """Migrate an entry.
 | 
						|
 | 
						|
        Returns True if config entry is up-to-date or has been migrated.
 | 
						|
        """
 | 
						|
        if (handler := HANDLERS.get(self.domain)) is None:
 | 
						|
            _LOGGER.error(
 | 
						|
                "Flow handler not found for entry %s for %s", self.title, self.domain
 | 
						|
            )
 | 
						|
            return False
 | 
						|
        # Handler may be a partial
 | 
						|
        while isinstance(handler, functools.partial):
 | 
						|
            handler = handler.func
 | 
						|
 | 
						|
        if self.version == handler.VERSION:
 | 
						|
            return True
 | 
						|
 | 
						|
        integration = await loader.async_get_integration(hass, self.domain)
 | 
						|
        component = integration.get_component()
 | 
						|
        supports_migrate = hasattr(component, "async_migrate_entry")
 | 
						|
        if not supports_migrate:
 | 
						|
            _LOGGER.error(
 | 
						|
                "Migration handler not found for entry %s for %s",
 | 
						|
                self.title,
 | 
						|
                self.domain,
 | 
						|
            )
 | 
						|
            return False
 | 
						|
 | 
						|
        try:
 | 
						|
            result = await component.async_migrate_entry(hass, self)
 | 
						|
            if not isinstance(result, bool):
 | 
						|
                _LOGGER.error(
 | 
						|
                    "%s.async_migrate_entry did not return boolean", self.domain
 | 
						|
                )
 | 
						|
                return False
 | 
						|
            if result:
 | 
						|
                # pylint: disable=protected-access
 | 
						|
                hass.config_entries._async_schedule_save()
 | 
						|
            # https://github.com/python/mypy/issues/11839
 | 
						|
            return result  # type: ignore[no-any-return]
 | 
						|
        except Exception:  # pylint: disable=broad-except
 | 
						|
            _LOGGER.exception(
 | 
						|
                "Error migrating entry %s for %s", self.title, self.domain
 | 
						|
            )
 | 
						|
            return False
 | 
						|
 | 
						|
    def add_update_listener(self, listener: UpdateListenerType) -> CALLBACK_TYPE:
 | 
						|
        """Listen for when entry is updated.
 | 
						|
 | 
						|
        Returns function to unlisten.
 | 
						|
        """
 | 
						|
        weak_listener: Any
 | 
						|
        # weakref.ref is not applicable to a bound method, e.g. method of a class instance, as reference will die immediately
 | 
						|
        if hasattr(listener, "__self__"):
 | 
						|
            weak_listener = weakref.WeakMethod(cast(MethodType, listener))
 | 
						|
        else:
 | 
						|
            weak_listener = weakref.ref(listener)
 | 
						|
        self.update_listeners.append(weak_listener)
 | 
						|
 | 
						|
        return lambda: self.update_listeners.remove(weak_listener)
 | 
						|
 | 
						|
    def as_dict(self) -> dict[str, Any]:
 | 
						|
        """Return dictionary version of this entry."""
 | 
						|
        return {
 | 
						|
            "entry_id": self.entry_id,
 | 
						|
            "version": self.version,
 | 
						|
            "domain": self.domain,
 | 
						|
            "title": self.title,
 | 
						|
            "data": dict(self.data),
 | 
						|
            "options": dict(self.options),
 | 
						|
            "pref_disable_new_entities": self.pref_disable_new_entities,
 | 
						|
            "pref_disable_polling": self.pref_disable_polling,
 | 
						|
            "source": self.source,
 | 
						|
            "unique_id": self.unique_id,
 | 
						|
            "disabled_by": self.disabled_by,
 | 
						|
        }
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_on_unload(self, func: CALLBACK_TYPE) -> None:
 | 
						|
        """Add a function to call when config entry is unloaded."""
 | 
						|
        if self._on_unload is None:
 | 
						|
            self._on_unload = []
 | 
						|
        self._on_unload.append(func)
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_process_on_unload(self) -> None:
 | 
						|
        """Process the on_unload callbacks."""
 | 
						|
        if self._on_unload is not None:
 | 
						|
            while self._on_unload:
 | 
						|
                self._on_unload.pop()()
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_start_reauth(self, hass: HomeAssistant) -> None:
 | 
						|
        """Start a reauth flow."""
 | 
						|
        flow_context = {
 | 
						|
            "source": SOURCE_REAUTH,
 | 
						|
            "entry_id": self.entry_id,
 | 
						|
            "title_placeholders": {"name": self.title},
 | 
						|
            "unique_id": self.unique_id,
 | 
						|
        }
 | 
						|
 | 
						|
        for flow in hass.config_entries.flow.async_progress_by_handler(self.domain):
 | 
						|
            if flow["context"] == flow_context:
 | 
						|
                return
 | 
						|
 | 
						|
        hass.async_create_task(
 | 
						|
            hass.config_entries.flow.async_init(
 | 
						|
                self.domain,
 | 
						|
                context=flow_context,
 | 
						|
                data=self.data,
 | 
						|
            )
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
current_entry: ContextVar[ConfigEntry | None] = ContextVar(
 | 
						|
    "current_entry", default=None
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
class ConfigEntriesFlowManager(data_entry_flow.FlowManager):
 | 
						|
    """Manage all the config entry flows that are in progress."""
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        hass: HomeAssistant,
 | 
						|
        config_entries: ConfigEntries,
 | 
						|
        hass_config: ConfigType,
 | 
						|
    ) -> None:
 | 
						|
        """Initialize the config entry flow manager."""
 | 
						|
        super().__init__(hass)
 | 
						|
        self.config_entries = config_entries
 | 
						|
        self._hass_config = hass_config
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_has_other_discovery_flows(self, flow_id: str) -> bool:
 | 
						|
        """Check if there are any other discovery flows in progress."""
 | 
						|
        return any(
 | 
						|
            flow.context["source"] in DISCOVERY_SOURCES and flow.flow_id != flow_id
 | 
						|
            for flow in self._progress.values()
 | 
						|
        )
 | 
						|
 | 
						|
    async def async_finish_flow(
 | 
						|
        self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Finish a config flow and add an entry."""
 | 
						|
        flow = cast(ConfigFlow, flow)
 | 
						|
 | 
						|
        # Remove notification if no other discovery config entries in progress
 | 
						|
        if not self._async_has_other_discovery_flows(flow.flow_id):
 | 
						|
            persistent_notification.async_dismiss(self.hass, DISCOVERY_NOTIFICATION_ID)
 | 
						|
 | 
						|
        if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
 | 
						|
            return result
 | 
						|
 | 
						|
        # Check if config entry exists with unique ID. Unload it.
 | 
						|
        existing_entry = None
 | 
						|
 | 
						|
        # Abort all flows in progress with same unique ID
 | 
						|
        # or the default discovery ID
 | 
						|
        for progress_flow in self.async_progress_by_handler(flow.handler):
 | 
						|
            progress_unique_id = progress_flow["context"].get("unique_id")
 | 
						|
            if progress_flow["flow_id"] != flow.flow_id and (
 | 
						|
                (flow.unique_id and progress_unique_id == flow.unique_id)
 | 
						|
                or progress_unique_id == DEFAULT_DISCOVERY_UNIQUE_ID
 | 
						|
            ):
 | 
						|
                self.async_abort(progress_flow["flow_id"])
 | 
						|
 | 
						|
        if flow.unique_id is not None:
 | 
						|
            # Reset unique ID when the default discovery ID has been used
 | 
						|
            if flow.unique_id == DEFAULT_DISCOVERY_UNIQUE_ID:
 | 
						|
                await flow.async_set_unique_id(None)
 | 
						|
 | 
						|
            # Find existing entry.
 | 
						|
            for check_entry in self.config_entries.async_entries(result["handler"]):
 | 
						|
                if check_entry.unique_id == flow.unique_id:
 | 
						|
                    existing_entry = check_entry
 | 
						|
                    break
 | 
						|
 | 
						|
        # Unload the entry before setting up the new one.
 | 
						|
        # We will remove it only after the other one is set up,
 | 
						|
        # so that device customizations are not getting lost.
 | 
						|
        if existing_entry is not None and existing_entry.state.recoverable:
 | 
						|
            await self.config_entries.async_unload(existing_entry.entry_id)
 | 
						|
 | 
						|
        entry = ConfigEntry(
 | 
						|
            version=result["version"],
 | 
						|
            domain=result["handler"],
 | 
						|
            title=result["title"],
 | 
						|
            data=result["data"],
 | 
						|
            options=result["options"],
 | 
						|
            source=flow.context["source"],
 | 
						|
            unique_id=flow.unique_id,
 | 
						|
        )
 | 
						|
 | 
						|
        await self.config_entries.async_add(entry)
 | 
						|
 | 
						|
        if existing_entry is not None:
 | 
						|
            await self.config_entries.async_remove(existing_entry.entry_id)
 | 
						|
 | 
						|
        result["result"] = entry
 | 
						|
        return result
 | 
						|
 | 
						|
    async def async_create_flow(
 | 
						|
        self, handler_key: Any, *, context: dict | None = None, data: Any = None
 | 
						|
    ) -> ConfigFlow:
 | 
						|
        """Create a flow for specified handler.
 | 
						|
 | 
						|
        Handler key is the domain of the component that we want to set up.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            integration = await loader.async_get_integration(self.hass, handler_key)
 | 
						|
        except loader.IntegrationNotFound as err:
 | 
						|
            _LOGGER.error("Cannot find integration %s", handler_key)
 | 
						|
            raise data_entry_flow.UnknownHandler from err
 | 
						|
 | 
						|
        # Make sure requirements and dependencies of component are resolved
 | 
						|
        await async_process_deps_reqs(self.hass, self._hass_config, integration)
 | 
						|
 | 
						|
        try:
 | 
						|
            integration.get_platform("config_flow")
 | 
						|
        except ImportError as err:
 | 
						|
            _LOGGER.error(
 | 
						|
                "Error occurred loading configuration flow for integration %s: %s",
 | 
						|
                handler_key,
 | 
						|
                err,
 | 
						|
            )
 | 
						|
            raise data_entry_flow.UnknownHandler
 | 
						|
 | 
						|
        if (handler := HANDLERS.get(handler_key)) is None:
 | 
						|
            raise data_entry_flow.UnknownHandler
 | 
						|
 | 
						|
        if not context or "source" not in context:
 | 
						|
            raise KeyError("Context not set or doesn't have a source set")
 | 
						|
 | 
						|
        flow = cast(ConfigFlow, handler())
 | 
						|
        flow.init_step = context["source"]
 | 
						|
        return flow
 | 
						|
 | 
						|
    async def async_post_init(
 | 
						|
        self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult
 | 
						|
    ) -> None:
 | 
						|
        """After a flow is initialised trigger new flow notifications."""
 | 
						|
        source = flow.context["source"]
 | 
						|
 | 
						|
        # Create notification.
 | 
						|
        if source in DISCOVERY_SOURCES:
 | 
						|
            self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED)
 | 
						|
            persistent_notification.async_create(
 | 
						|
                self.hass,
 | 
						|
                title="New devices discovered",
 | 
						|
                message=(
 | 
						|
                    "We have discovered new devices on your network. "
 | 
						|
                    "[Check it out](/config/integrations)."
 | 
						|
                ),
 | 
						|
                notification_id=DISCOVERY_NOTIFICATION_ID,
 | 
						|
            )
 | 
						|
        elif source == SOURCE_REAUTH:
 | 
						|
            persistent_notification.async_create(
 | 
						|
                self.hass,
 | 
						|
                title="Integration requires reconfiguration",
 | 
						|
                message=(
 | 
						|
                    "At least one of your integrations requires reconfiguration to "
 | 
						|
                    "continue functioning. [Check it out](/config/integrations)."
 | 
						|
                ),
 | 
						|
                notification_id=RECONFIGURE_NOTIFICATION_ID,
 | 
						|
            )
 | 
						|
 | 
						|
 | 
						|
class ConfigEntries:
 | 
						|
    """Manage the configuration entries.
 | 
						|
 | 
						|
    An instance of this object is available via `hass.config_entries`.
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, hass: HomeAssistant, hass_config: ConfigType) -> None:
 | 
						|
        """Initialize the entry manager."""
 | 
						|
        self.hass = hass
 | 
						|
        self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
 | 
						|
        self.options = OptionsFlowManager(hass)
 | 
						|
        self._hass_config = hass_config
 | 
						|
        self._entries: dict[str, ConfigEntry] = {}
 | 
						|
        self._domain_index: dict[str, list[str]] = {}
 | 
						|
        self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
 | 
						|
        EntityRegistryDisabledHandler(hass).async_setup()
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_domains(
 | 
						|
        self, include_ignore: bool = False, include_disabled: bool = False
 | 
						|
    ) -> list[str]:
 | 
						|
        """Return domains for which we have entries."""
 | 
						|
        return list(
 | 
						|
            {
 | 
						|
                entry.domain: None
 | 
						|
                for entry in self._entries.values()
 | 
						|
                if (include_ignore or entry.source != SOURCE_IGNORE)
 | 
						|
                and (include_disabled or not entry.disabled_by)
 | 
						|
            }
 | 
						|
        )
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_get_entry(self, entry_id: str) -> ConfigEntry | None:
 | 
						|
        """Return entry with matching entry_id."""
 | 
						|
        return self._entries.get(entry_id)
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_entries(self, domain: str | None = None) -> list[ConfigEntry]:
 | 
						|
        """Return all entries or entries for a specific domain."""
 | 
						|
        if domain is None:
 | 
						|
            return list(self._entries.values())
 | 
						|
        return [
 | 
						|
            self._entries[entry_id] for entry_id in self._domain_index.get(domain, [])
 | 
						|
        ]
 | 
						|
 | 
						|
    async def async_add(self, entry: ConfigEntry) -> None:
 | 
						|
        """Add and setup an entry."""
 | 
						|
        if entry.entry_id in self._entries:
 | 
						|
            raise HomeAssistantError(
 | 
						|
                f"An entry with the id {entry.entry_id} already exists."
 | 
						|
            )
 | 
						|
        self._entries[entry.entry_id] = entry
 | 
						|
        self._domain_index.setdefault(entry.domain, []).append(entry.entry_id)
 | 
						|
        await self.async_setup(entry.entry_id)
 | 
						|
        self._async_schedule_save()
 | 
						|
 | 
						|
    async def async_remove(self, entry_id: str) -> dict[str, Any]:
 | 
						|
        """Remove an entry."""
 | 
						|
        if (entry := self.async_get_entry(entry_id)) is None:
 | 
						|
            raise UnknownEntry
 | 
						|
 | 
						|
        if not entry.state.recoverable:
 | 
						|
            unload_success = entry.state is not ConfigEntryState.FAILED_UNLOAD
 | 
						|
        else:
 | 
						|
            unload_success = await self.async_unload(entry_id)
 | 
						|
 | 
						|
        await entry.async_remove(self.hass)
 | 
						|
 | 
						|
        del self._entries[entry.entry_id]
 | 
						|
        self._domain_index[entry.domain].remove(entry.entry_id)
 | 
						|
        if not self._domain_index[entry.domain]:
 | 
						|
            del self._domain_index[entry.domain]
 | 
						|
        self._async_schedule_save()
 | 
						|
 | 
						|
        dev_reg, ent_reg = await asyncio.gather(
 | 
						|
            self.hass.helpers.device_registry.async_get_registry(),
 | 
						|
            self.hass.helpers.entity_registry.async_get_registry(),
 | 
						|
        )
 | 
						|
 | 
						|
        dev_reg.async_clear_config_entry(entry_id)
 | 
						|
        ent_reg.async_clear_config_entry(entry_id)
 | 
						|
 | 
						|
        # If the configuration entry is removed during reauth, it should
 | 
						|
        # abort any reauth flow that is active for the removed entry.
 | 
						|
        for progress_flow in self.hass.config_entries.flow.async_progress_by_handler(
 | 
						|
            entry.domain
 | 
						|
        ):
 | 
						|
            context = progress_flow.get("context")
 | 
						|
            if (
 | 
						|
                context
 | 
						|
                and context["source"] == SOURCE_REAUTH
 | 
						|
                and "entry_id" in context
 | 
						|
                and context["entry_id"] == entry_id
 | 
						|
                and "flow_id" in progress_flow
 | 
						|
            ):
 | 
						|
                self.hass.config_entries.flow.async_abort(progress_flow["flow_id"])
 | 
						|
 | 
						|
        # After we have fully removed an "ignore" config entry we can try and rediscover it so that a
 | 
						|
        # user is able to immediately start configuring it. We do this by starting a new flow with
 | 
						|
        # the 'unignore' step. If the integration doesn't implement async_step_unignore then
 | 
						|
        # this will be a no-op.
 | 
						|
        if entry.source == SOURCE_IGNORE:
 | 
						|
            self.hass.async_create_task(
 | 
						|
                self.hass.config_entries.flow.async_init(
 | 
						|
                    entry.domain,
 | 
						|
                    context={"source": SOURCE_UNIGNORE},
 | 
						|
                    data={"unique_id": entry.unique_id},
 | 
						|
                )
 | 
						|
            )
 | 
						|
 | 
						|
        return {"require_restart": not unload_success}
 | 
						|
 | 
						|
    async def _async_shutdown(self, event: Event) -> None:
 | 
						|
        """Call when Home Assistant is stopping."""
 | 
						|
        await asyncio.gather(
 | 
						|
            *(entry.async_shutdown() for entry in self._entries.values())
 | 
						|
        )
 | 
						|
        await self.flow.async_shutdown()
 | 
						|
 | 
						|
    async def async_initialize(self) -> None:
 | 
						|
        """Initialize config entry config."""
 | 
						|
        # Migrating for config entries stored before 0.73
 | 
						|
        config = await self.hass.helpers.storage.async_migrator(
 | 
						|
            self.hass.config.path(PATH_CONFIG),
 | 
						|
            self._store,
 | 
						|
            old_conf_migrate_func=_old_conf_migrator,
 | 
						|
        )
 | 
						|
 | 
						|
        self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown)
 | 
						|
 | 
						|
        if config is None:
 | 
						|
            self._entries = {}
 | 
						|
            self._domain_index = {}
 | 
						|
            return
 | 
						|
 | 
						|
        entries = {}
 | 
						|
        domain_index: dict[str, list[str]] = {}
 | 
						|
 | 
						|
        for entry in config["entries"]:
 | 
						|
            pref_disable_new_entities = entry.get("pref_disable_new_entities")
 | 
						|
 | 
						|
            # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a system options dictionary
 | 
						|
            if pref_disable_new_entities is None and "system_options" in entry:
 | 
						|
                pref_disable_new_entities = entry.get("system_options", {}).get(
 | 
						|
                    "disable_new_entities"
 | 
						|
                )
 | 
						|
 | 
						|
            domain = entry["domain"]
 | 
						|
            entry_id = entry["entry_id"]
 | 
						|
 | 
						|
            entries[entry_id] = ConfigEntry(
 | 
						|
                version=entry["version"],
 | 
						|
                domain=domain,
 | 
						|
                entry_id=entry_id,
 | 
						|
                data=entry["data"],
 | 
						|
                source=entry["source"],
 | 
						|
                title=entry["title"],
 | 
						|
                # New in 0.89
 | 
						|
                options=entry.get("options"),
 | 
						|
                # New in 0.104
 | 
						|
                unique_id=entry.get("unique_id"),
 | 
						|
                # New in 2021.3
 | 
						|
                disabled_by=ConfigEntryDisabler(entry["disabled_by"])
 | 
						|
                if entry.get("disabled_by")
 | 
						|
                else None,
 | 
						|
                # New in 2021.6
 | 
						|
                pref_disable_new_entities=pref_disable_new_entities,
 | 
						|
                pref_disable_polling=entry.get("pref_disable_polling"),
 | 
						|
            )
 | 
						|
            domain_index.setdefault(domain, []).append(entry_id)
 | 
						|
 | 
						|
        self._domain_index = domain_index
 | 
						|
        self._entries = entries
 | 
						|
 | 
						|
    async def async_setup(self, entry_id: str) -> bool:
 | 
						|
        """Set up a config entry.
 | 
						|
 | 
						|
        Return True if entry has been successfully loaded.
 | 
						|
        """
 | 
						|
        if (entry := self.async_get_entry(entry_id)) is None:
 | 
						|
            raise UnknownEntry
 | 
						|
 | 
						|
        if entry.state is not ConfigEntryState.NOT_LOADED:
 | 
						|
            raise OperationNotAllowed
 | 
						|
 | 
						|
        # Setup Component if not set up yet
 | 
						|
        if entry.domain in self.hass.config.components:
 | 
						|
            await entry.async_setup(self.hass)
 | 
						|
        else:
 | 
						|
            # Setting up the component will set up all its config entries
 | 
						|
            result = await async_setup_component(
 | 
						|
                self.hass, entry.domain, self._hass_config
 | 
						|
            )
 | 
						|
 | 
						|
            if not result:
 | 
						|
                return result
 | 
						|
 | 
						|
        return entry.state is ConfigEntryState.LOADED  # type: ignore[comparison-overlap] # mypy bug?
 | 
						|
 | 
						|
    async def async_unload(self, entry_id: str) -> bool:
 | 
						|
        """Unload a config entry."""
 | 
						|
        if (entry := self.async_get_entry(entry_id)) is None:
 | 
						|
            raise UnknownEntry
 | 
						|
 | 
						|
        if not entry.state.recoverable:
 | 
						|
            raise OperationNotAllowed
 | 
						|
 | 
						|
        return await entry.async_unload(self.hass)
 | 
						|
 | 
						|
    async def async_reload(self, entry_id: str) -> bool:
 | 
						|
        """Reload an entry.
 | 
						|
 | 
						|
        If an entry was not loaded, will just load.
 | 
						|
        """
 | 
						|
        if (entry := self.async_get_entry(entry_id)) is None:
 | 
						|
            raise UnknownEntry
 | 
						|
 | 
						|
        unload_result = await self.async_unload(entry_id)
 | 
						|
 | 
						|
        if not unload_result or entry.disabled_by:
 | 
						|
            return unload_result
 | 
						|
 | 
						|
        return await self.async_setup(entry_id)
 | 
						|
 | 
						|
    async def async_set_disabled_by(
 | 
						|
        self, entry_id: str, disabled_by: ConfigEntryDisabler | None
 | 
						|
    ) -> bool:
 | 
						|
        """Disable an entry.
 | 
						|
 | 
						|
        If disabled_by is changed, the config entry will be reloaded.
 | 
						|
        """
 | 
						|
        if (entry := self.async_get_entry(entry_id)) is None:
 | 
						|
            raise UnknownEntry
 | 
						|
 | 
						|
        if isinstance(disabled_by, str) and not isinstance(
 | 
						|
            disabled_by, ConfigEntryDisabler
 | 
						|
        ):
 | 
						|
            report(  # type: ignore[unreachable]
 | 
						|
                "uses str for config entry disabled_by. This is deprecated and will "
 | 
						|
                "stop working in Home Assistant 2022.3, it should be updated to use "
 | 
						|
                "ConfigEntryDisabler instead",
 | 
						|
                error_if_core=False,
 | 
						|
            )
 | 
						|
            disabled_by = ConfigEntryDisabler(disabled_by)
 | 
						|
 | 
						|
        if entry.disabled_by is disabled_by:
 | 
						|
            return True
 | 
						|
 | 
						|
        entry.disabled_by = disabled_by
 | 
						|
        self._async_schedule_save()
 | 
						|
 | 
						|
        dev_reg = device_registry.async_get(self.hass)
 | 
						|
        ent_reg = entity_registry.async_get(self.hass)
 | 
						|
 | 
						|
        if not entry.disabled_by:
 | 
						|
            # The config entry will no longer be disabled, enable devices and entities
 | 
						|
            device_registry.async_config_entry_disabled_by_changed(dev_reg, entry)
 | 
						|
            entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry)
 | 
						|
 | 
						|
        # Load or unload the config entry
 | 
						|
        reload_result = await self.async_reload(entry_id)
 | 
						|
 | 
						|
        if entry.disabled_by:
 | 
						|
            # The config entry has been disabled, disable devices and entities
 | 
						|
            device_registry.async_config_entry_disabled_by_changed(dev_reg, entry)
 | 
						|
            entity_registry.async_config_entry_disabled_by_changed(ent_reg, entry)
 | 
						|
 | 
						|
        return reload_result
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_update_entry(
 | 
						|
        self,
 | 
						|
        entry: ConfigEntry,
 | 
						|
        *,
 | 
						|
        unique_id: str | None | UndefinedType = UNDEFINED,
 | 
						|
        title: str | UndefinedType = UNDEFINED,
 | 
						|
        data: Mapping[str, Any] | UndefinedType = UNDEFINED,
 | 
						|
        options: Mapping[str, Any] | UndefinedType = UNDEFINED,
 | 
						|
        pref_disable_new_entities: bool | UndefinedType = UNDEFINED,
 | 
						|
        pref_disable_polling: bool | UndefinedType = UNDEFINED,
 | 
						|
    ) -> bool:
 | 
						|
        """Update a config entry.
 | 
						|
 | 
						|
        If the entry was changed, the update_listeners are
 | 
						|
        fired and this function returns True
 | 
						|
 | 
						|
        If the entry was not changed, the update_listeners are
 | 
						|
        not fired and this function returns False
 | 
						|
        """
 | 
						|
        changed = False
 | 
						|
 | 
						|
        for attr, value in (
 | 
						|
            ("unique_id", unique_id),
 | 
						|
            ("title", title),
 | 
						|
            ("pref_disable_new_entities", pref_disable_new_entities),
 | 
						|
            ("pref_disable_polling", pref_disable_polling),
 | 
						|
        ):
 | 
						|
            if value == UNDEFINED or getattr(entry, attr) == value:
 | 
						|
                continue
 | 
						|
 | 
						|
            setattr(entry, attr, value)
 | 
						|
            changed = True
 | 
						|
 | 
						|
        if data is not UNDEFINED and entry.data != data:
 | 
						|
            changed = True
 | 
						|
            entry.data = MappingProxyType(data)
 | 
						|
 | 
						|
        if options is not UNDEFINED and entry.options != options:
 | 
						|
            changed = True
 | 
						|
            entry.options = MappingProxyType(options)
 | 
						|
 | 
						|
        if not changed:
 | 
						|
            return False
 | 
						|
 | 
						|
        for listener_ref in entry.update_listeners:
 | 
						|
            if (listener := listener_ref()) is not None:
 | 
						|
                self.hass.async_create_task(listener(self.hass, entry))
 | 
						|
 | 
						|
        self._async_schedule_save()
 | 
						|
 | 
						|
        return True
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_setup_platforms(
 | 
						|
        self, entry: ConfigEntry, platforms: Iterable[Platform | str]
 | 
						|
    ) -> None:
 | 
						|
        """Forward the setup of an entry to platforms."""
 | 
						|
        for platform in platforms:
 | 
						|
            self.hass.async_create_task(self.async_forward_entry_setup(entry, platform))
 | 
						|
 | 
						|
    async def async_forward_entry_setup(
 | 
						|
        self, entry: ConfigEntry, domain: Platform | str
 | 
						|
    ) -> bool:
 | 
						|
        """Forward the setup of an entry to a different component.
 | 
						|
 | 
						|
        By default an entry is setup with the component it belongs to. If that
 | 
						|
        component also has related platforms, the component will have to
 | 
						|
        forward the entry to be setup by that component.
 | 
						|
 | 
						|
        You don't want to await this coroutine if it is called as part of the
 | 
						|
        setup of a component, because it can cause a deadlock.
 | 
						|
        """
 | 
						|
        # Setup Component if not set up yet
 | 
						|
        if domain not in self.hass.config.components:
 | 
						|
            result = await async_setup_component(self.hass, domain, self._hass_config)
 | 
						|
 | 
						|
            if not result:
 | 
						|
                return False
 | 
						|
 | 
						|
        integration = await loader.async_get_integration(self.hass, domain)
 | 
						|
 | 
						|
        await entry.async_setup(self.hass, integration=integration)
 | 
						|
        return True
 | 
						|
 | 
						|
    async def async_unload_platforms(
 | 
						|
        self, entry: ConfigEntry, platforms: Iterable[Platform | str]
 | 
						|
    ) -> bool:
 | 
						|
        """Forward the unloading of an entry to platforms."""
 | 
						|
        return all(
 | 
						|
            await asyncio.gather(
 | 
						|
                *(
 | 
						|
                    self.async_forward_entry_unload(entry, platform)
 | 
						|
                    for platform in platforms
 | 
						|
                )
 | 
						|
            )
 | 
						|
        )
 | 
						|
 | 
						|
    async def async_forward_entry_unload(
 | 
						|
        self, entry: ConfigEntry, domain: Platform | str
 | 
						|
    ) -> bool:
 | 
						|
        """Forward the unloading of an entry to a different component."""
 | 
						|
        # It was never loaded.
 | 
						|
        if domain not in self.hass.config.components:
 | 
						|
            return True
 | 
						|
 | 
						|
        integration = await loader.async_get_integration(self.hass, domain)
 | 
						|
 | 
						|
        return await entry.async_unload(self.hass, integration=integration)
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_schedule_save(self) -> None:
 | 
						|
        """Save the entity registry to a file."""
 | 
						|
        self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
 | 
						|
        """Return data to save."""
 | 
						|
        return {"entries": [entry.as_dict() for entry in self._entries.values()]}
 | 
						|
 | 
						|
 | 
						|
async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]:
 | 
						|
    """Migrate the pre-0.73 config format to the latest version."""
 | 
						|
    return {"entries": old_config}
 | 
						|
 | 
						|
 | 
						|
class ConfigFlow(data_entry_flow.FlowHandler):
 | 
						|
    """Base class for config flows with some helpers."""
 | 
						|
 | 
						|
    def __init_subclass__(cls, domain: str | None = None, **kwargs: Any) -> None:
 | 
						|
        """Initialize a subclass, register if possible."""
 | 
						|
        super().__init_subclass__(**kwargs)
 | 
						|
        if domain is not None:
 | 
						|
            HANDLERS.register(domain)(cls)
 | 
						|
 | 
						|
    @property
 | 
						|
    def unique_id(self) -> str | None:
 | 
						|
        """Return unique ID if available."""
 | 
						|
        if not self.context:
 | 
						|
            return None
 | 
						|
 | 
						|
        return cast(Optional[str], self.context.get("unique_id"))
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    @callback
 | 
						|
    def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
 | 
						|
        """Get the options flow for this handler."""
 | 
						|
        raise data_entry_flow.UnknownHandler
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    @callback
 | 
						|
    def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
 | 
						|
        """Return options flow support for this handler."""
 | 
						|
        return cls.async_get_options_flow is not ConfigFlow.async_get_options_flow
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_abort_entries_match(
 | 
						|
        self, match_dict: dict[str, Any] | None = None
 | 
						|
    ) -> None:
 | 
						|
        """Abort if current entries match all data."""
 | 
						|
        if match_dict is None:
 | 
						|
            match_dict = {}  # Match any entry
 | 
						|
        for entry in self._async_current_entries(include_ignore=False):
 | 
						|
            if all(item in entry.data.items() for item in match_dict.items()):
 | 
						|
                raise data_entry_flow.AbortFlow("already_configured")
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _abort_if_unique_id_configured(
 | 
						|
        self,
 | 
						|
        updates: dict[Any, Any] | None = None,
 | 
						|
        reload_on_update: bool = True,
 | 
						|
    ) -> None:
 | 
						|
        """Abort if the unique ID is already configured."""
 | 
						|
        if self.unique_id is None:
 | 
						|
            return
 | 
						|
 | 
						|
        for entry in self._async_current_entries(include_ignore=True):
 | 
						|
            if entry.unique_id == self.unique_id:
 | 
						|
                if updates is not None:
 | 
						|
                    changed = self.hass.config_entries.async_update_entry(
 | 
						|
                        entry, data={**entry.data, **updates}
 | 
						|
                    )
 | 
						|
                    if (
 | 
						|
                        changed
 | 
						|
                        and reload_on_update
 | 
						|
                        and entry.state
 | 
						|
                        in (ConfigEntryState.LOADED, ConfigEntryState.SETUP_RETRY)
 | 
						|
                    ):
 | 
						|
                        self.hass.async_create_task(
 | 
						|
                            self.hass.config_entries.async_reload(entry.entry_id)
 | 
						|
                        )
 | 
						|
                # Allow ignored entries to be configured on manual user step
 | 
						|
                if entry.source == SOURCE_IGNORE and self.source == SOURCE_USER:
 | 
						|
                    continue
 | 
						|
                raise data_entry_flow.AbortFlow("already_configured")
 | 
						|
 | 
						|
    async def async_set_unique_id(
 | 
						|
        self, unique_id: str | None = None, *, raise_on_progress: bool = True
 | 
						|
    ) -> ConfigEntry | None:
 | 
						|
        """Set a unique ID for the config flow.
 | 
						|
 | 
						|
        Returns optionally existing config entry with same ID.
 | 
						|
        """
 | 
						|
        if unique_id is None:
 | 
						|
            self.context["unique_id"] = None
 | 
						|
            return None
 | 
						|
 | 
						|
        if raise_on_progress:
 | 
						|
            for progress in self._async_in_progress(include_uninitialized=True):
 | 
						|
                if progress["context"].get("unique_id") == unique_id:
 | 
						|
                    raise data_entry_flow.AbortFlow("already_in_progress")
 | 
						|
 | 
						|
        self.context["unique_id"] = unique_id
 | 
						|
 | 
						|
        # Abort discoveries done using the default discovery unique id
 | 
						|
        if unique_id != DEFAULT_DISCOVERY_UNIQUE_ID:
 | 
						|
            for progress in self._async_in_progress(include_uninitialized=True):
 | 
						|
                if progress["context"].get("unique_id") == DEFAULT_DISCOVERY_UNIQUE_ID:
 | 
						|
                    self.hass.config_entries.flow.async_abort(progress["flow_id"])
 | 
						|
 | 
						|
        for entry in self._async_current_entries(include_ignore=True):
 | 
						|
            if entry.unique_id == unique_id:
 | 
						|
                return entry
 | 
						|
 | 
						|
        return None
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _set_confirm_only(
 | 
						|
        self,
 | 
						|
    ) -> None:
 | 
						|
        """Mark the config flow as only needing user confirmation to finish flow."""
 | 
						|
        self.context["confirm_only"] = True
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_current_entries(
 | 
						|
        self, include_ignore: bool | None = None
 | 
						|
    ) -> list[ConfigEntry]:
 | 
						|
        """Return current entries.
 | 
						|
 | 
						|
        If the flow is user initiated, filter out ignored entries unless include_ignore is True.
 | 
						|
        """
 | 
						|
        config_entries = self.hass.config_entries.async_entries(self.handler)
 | 
						|
 | 
						|
        if (
 | 
						|
            include_ignore is True
 | 
						|
            or include_ignore is None
 | 
						|
            and self.source != SOURCE_USER
 | 
						|
        ):
 | 
						|
            return config_entries
 | 
						|
 | 
						|
        return [entry for entry in config_entries if entry.source != SOURCE_IGNORE]
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_current_ids(self, include_ignore: bool = True) -> set[str | None]:
 | 
						|
        """Return current unique IDs."""
 | 
						|
        return {
 | 
						|
            entry.unique_id
 | 
						|
            for entry in self.hass.config_entries.async_entries(self.handler)
 | 
						|
            if include_ignore or entry.source != SOURCE_IGNORE
 | 
						|
        }
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_in_progress(
 | 
						|
        self, include_uninitialized: bool = False
 | 
						|
    ) -> list[data_entry_flow.FlowResult]:
 | 
						|
        """Return other in progress flows for current domain."""
 | 
						|
        return [
 | 
						|
            flw
 | 
						|
            for flw in self.hass.config_entries.flow.async_progress_by_handler(
 | 
						|
                self.handler, include_uninitialized=include_uninitialized
 | 
						|
            )
 | 
						|
            if flw["flow_id"] != self.flow_id
 | 
						|
        ]
 | 
						|
 | 
						|
    async def async_step_ignore(
 | 
						|
        self, user_input: dict[str, Any]
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Ignore this config flow."""
 | 
						|
        await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
 | 
						|
        return self.async_create_entry(title=user_input["title"], data={})
 | 
						|
 | 
						|
    async def async_step_unignore(
 | 
						|
        self, user_input: dict[str, Any]
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Rediscover a config entry by it's unique_id."""
 | 
						|
        return self.async_abort(reason="not_implemented")
 | 
						|
 | 
						|
    async def async_step_user(
 | 
						|
        self, user_input: dict[str, Any] | None = None
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initiated by the user."""
 | 
						|
        return self.async_abort(reason="not_implemented")
 | 
						|
 | 
						|
    async def _async_handle_discovery_without_unique_id(self) -> None:
 | 
						|
        """Mark this flow discovered, without a unique identifier.
 | 
						|
 | 
						|
        If a flow initiated by discovery, doesn't have a unique ID, this can
 | 
						|
        be used alternatively. It will ensure only 1 flow is started and only
 | 
						|
        when the handler has no existing config entries.
 | 
						|
 | 
						|
        It ensures that the discovery can be ignored by the user.
 | 
						|
        """
 | 
						|
        if self.unique_id is not None:
 | 
						|
            return
 | 
						|
 | 
						|
        # Abort if the handler has config entries already
 | 
						|
        if self._async_current_entries():
 | 
						|
            raise data_entry_flow.AbortFlow("already_configured")
 | 
						|
 | 
						|
        # Use an special unique id to differentiate
 | 
						|
        await self.async_set_unique_id(DEFAULT_DISCOVERY_UNIQUE_ID)
 | 
						|
        self._abort_if_unique_id_configured()
 | 
						|
 | 
						|
        # Abort if any other flow for this handler is already in progress
 | 
						|
        if self._async_in_progress(include_uninitialized=True):
 | 
						|
            raise data_entry_flow.AbortFlow("already_in_progress")
 | 
						|
 | 
						|
    async def async_step_discovery(
 | 
						|
        self, discovery_info: DiscoveryInfoType
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by discovery."""
 | 
						|
        await self._async_handle_discovery_without_unique_id()
 | 
						|
        return await self.async_step_user()
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_abort(
 | 
						|
        self, *, reason: str, description_placeholders: dict | None = None
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Abort the config flow."""
 | 
						|
        # Remove reauth notification if no reauth flows are in progress
 | 
						|
        if self.source == SOURCE_REAUTH and not any(
 | 
						|
            ent["context"]["source"] == SOURCE_REAUTH
 | 
						|
            for ent in self.hass.config_entries.flow.async_progress_by_handler(
 | 
						|
                self.handler
 | 
						|
            )
 | 
						|
            if ent["flow_id"] != self.flow_id
 | 
						|
        ):
 | 
						|
            persistent_notification.async_dismiss(
 | 
						|
                self.hass, RECONFIGURE_NOTIFICATION_ID
 | 
						|
            )
 | 
						|
 | 
						|
        return super().async_abort(
 | 
						|
            reason=reason, description_placeholders=description_placeholders
 | 
						|
        )
 | 
						|
 | 
						|
    async def async_step_hassio(
 | 
						|
        self, discovery_info: HassioServiceInfo
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by HASS IO discovery."""
 | 
						|
        return await self.async_step_discovery(discovery_info.config)
 | 
						|
 | 
						|
    async def async_step_homekit(
 | 
						|
        self, discovery_info: ZeroconfServiceInfo
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by Homekit discovery."""
 | 
						|
        return await self.async_step_discovery(dataclasses.asdict(discovery_info))
 | 
						|
 | 
						|
    async def async_step_mqtt(
 | 
						|
        self, discovery_info: MqttServiceInfo
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by MQTT discovery."""
 | 
						|
        return await self.async_step_discovery(dataclasses.asdict(discovery_info))
 | 
						|
 | 
						|
    async def async_step_ssdp(
 | 
						|
        self, discovery_info: SsdpServiceInfo
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by SSDP discovery."""
 | 
						|
        return await self.async_step_discovery(dataclasses.asdict(discovery_info))
 | 
						|
 | 
						|
    async def async_step_zeroconf(
 | 
						|
        self, discovery_info: ZeroconfServiceInfo
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by Zeroconf discovery."""
 | 
						|
        return await self.async_step_discovery(dataclasses.asdict(discovery_info))
 | 
						|
 | 
						|
    async def async_step_dhcp(
 | 
						|
        self, discovery_info: DhcpServiceInfo
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by DHCP discovery."""
 | 
						|
        return await self.async_step_discovery(dataclasses.asdict(discovery_info))
 | 
						|
 | 
						|
    async def async_step_usb(
 | 
						|
        self, discovery_info: UsbServiceInfo
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Handle a flow initialized by USB discovery."""
 | 
						|
        return await self.async_step_discovery(dataclasses.asdict(discovery_info))
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_create_entry(  # pylint: disable=arguments-differ
 | 
						|
        self,
 | 
						|
        *,
 | 
						|
        title: str,
 | 
						|
        data: Mapping[str, Any],
 | 
						|
        description: str | None = None,
 | 
						|
        description_placeholders: dict | None = None,
 | 
						|
        options: Mapping[str, Any] | None = None,
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Finish config flow and create a config entry."""
 | 
						|
        result = super().async_create_entry(
 | 
						|
            title=title,
 | 
						|
            data=data,
 | 
						|
            description=description,
 | 
						|
            description_placeholders=description_placeholders,
 | 
						|
        )
 | 
						|
 | 
						|
        result["options"] = options or {}
 | 
						|
 | 
						|
        return result
 | 
						|
 | 
						|
 | 
						|
class OptionsFlowManager(data_entry_flow.FlowManager):
 | 
						|
    """Flow to set options for a configuration entry."""
 | 
						|
 | 
						|
    async def async_create_flow(
 | 
						|
        self,
 | 
						|
        handler_key: Any,
 | 
						|
        *,
 | 
						|
        context: dict[str, Any] | None = None,
 | 
						|
        data: dict[str, Any] | None = None,
 | 
						|
    ) -> OptionsFlow:
 | 
						|
        """Create an options flow for a config entry.
 | 
						|
 | 
						|
        Entry_id and flow.handler is the same thing to map entry with flow.
 | 
						|
        """
 | 
						|
        entry = self.hass.config_entries.async_get_entry(handler_key)
 | 
						|
        if entry is None:
 | 
						|
            raise UnknownEntry(handler_key)
 | 
						|
 | 
						|
        if entry.domain not in HANDLERS:
 | 
						|
            raise data_entry_flow.UnknownHandler
 | 
						|
 | 
						|
        return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry))
 | 
						|
 | 
						|
    async def async_finish_flow(
 | 
						|
        self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult
 | 
						|
    ) -> data_entry_flow.FlowResult:
 | 
						|
        """Finish an options flow and update options for configuration entry.
 | 
						|
 | 
						|
        Flow.handler and entry_id is the same thing to map flow with entry.
 | 
						|
        """
 | 
						|
        flow = cast(OptionsFlow, flow)
 | 
						|
 | 
						|
        if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
 | 
						|
            return result
 | 
						|
 | 
						|
        entry = self.hass.config_entries.async_get_entry(flow.handler)
 | 
						|
        if entry is None:
 | 
						|
            raise UnknownEntry(flow.handler)
 | 
						|
        if result["data"] is not None:
 | 
						|
            self.hass.config_entries.async_update_entry(entry, options=result["data"])
 | 
						|
 | 
						|
        result["result"] = True
 | 
						|
        return result
 | 
						|
 | 
						|
 | 
						|
class OptionsFlow(data_entry_flow.FlowHandler):
 | 
						|
    """Base class for config option flows."""
 | 
						|
 | 
						|
    handler: str
 | 
						|
 | 
						|
 | 
						|
class EntityRegistryDisabledHandler:
 | 
						|
    """Handler to handle when entities related to config entries updating disabled_by."""
 | 
						|
 | 
						|
    def __init__(self, hass: HomeAssistant) -> None:
 | 
						|
        """Initialize the handler."""
 | 
						|
        self.hass = hass
 | 
						|
        self.registry: entity_registry.EntityRegistry | None = None
 | 
						|
        self.changed: set[str] = set()
 | 
						|
        self._remove_call_later: Callable[[], None] | None = None
 | 
						|
 | 
						|
    @callback
 | 
						|
    def async_setup(self) -> None:
 | 
						|
        """Set up the disable handler."""
 | 
						|
        self.hass.bus.async_listen(
 | 
						|
            entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
 | 
						|
            self._handle_entry_updated,
 | 
						|
            event_filter=_handle_entry_updated_filter,
 | 
						|
        )
 | 
						|
 | 
						|
    async def _handle_entry_updated(self, event: Event) -> None:
 | 
						|
        """Handle entity registry entry update."""
 | 
						|
        if self.registry is None:
 | 
						|
            self.registry = await entity_registry.async_get_registry(self.hass)
 | 
						|
 | 
						|
        entity_entry = self.registry.async_get(event.data["entity_id"])
 | 
						|
 | 
						|
        if (
 | 
						|
            # Stop if no entry found
 | 
						|
            entity_entry is None
 | 
						|
            # Stop if entry not connected to config entry
 | 
						|
            or entity_entry.config_entry_id is None
 | 
						|
            # Stop if the entry got disabled. In that case the entity handles it
 | 
						|
            # themselves.
 | 
						|
            or entity_entry.disabled_by
 | 
						|
        ):
 | 
						|
            return
 | 
						|
 | 
						|
        config_entry = self.hass.config_entries.async_get_entry(
 | 
						|
            entity_entry.config_entry_id
 | 
						|
        )
 | 
						|
        assert config_entry is not None
 | 
						|
 | 
						|
        if config_entry.entry_id not in self.changed and config_entry.supports_unload:
 | 
						|
            self.changed.add(config_entry.entry_id)
 | 
						|
 | 
						|
        if not self.changed:
 | 
						|
            return
 | 
						|
 | 
						|
        # We are going to delay reloading on *every* entity registry change so that
 | 
						|
        # if a user is happily clicking along, it will only reload at the end.
 | 
						|
 | 
						|
        if self._remove_call_later:
 | 
						|
            self._remove_call_later()
 | 
						|
 | 
						|
        self._remove_call_later = async_call_later(
 | 
						|
            self.hass, RELOAD_AFTER_UPDATE_DELAY, self._handle_reload
 | 
						|
        )
 | 
						|
 | 
						|
    async def _handle_reload(self, _now: Any) -> None:
 | 
						|
        """Handle a reload."""
 | 
						|
        self._remove_call_later = None
 | 
						|
        to_reload = self.changed
 | 
						|
        self.changed = set()
 | 
						|
 | 
						|
        _LOGGER.info(
 | 
						|
            "Reloading configuration entries because disabled_by changed in entity registry: %s",
 | 
						|
            ", ".join(self.changed),
 | 
						|
        )
 | 
						|
 | 
						|
        await asyncio.gather(
 | 
						|
            *(self.hass.config_entries.async_reload(entry_id) for entry_id in to_reload)
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
@callback
 | 
						|
def _handle_entry_updated_filter(event: Event) -> bool:
 | 
						|
    """Handle entity registry entry update filter.
 | 
						|
 | 
						|
    Only handle changes to "disabled_by".
 | 
						|
    If "disabled_by" was CONFIG_ENTRY, reload is not needed.
 | 
						|
    """
 | 
						|
    if (
 | 
						|
        event.data["action"] != "update"
 | 
						|
        or "disabled_by" not in event.data["changes"]
 | 
						|
        or event.data["changes"]["disabled_by"]
 | 
						|
        is entity_registry.RegistryEntryDisabler.CONFIG_ENTRY
 | 
						|
    ):
 | 
						|
        return False
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool:
 | 
						|
    """Test if a domain supports entry unloading."""
 | 
						|
    integration = await loader.async_get_integration(hass, domain)
 | 
						|
    component = integration.get_component()
 | 
						|
    return hasattr(component, "async_unload_entry")
 |