2014-11-05 07:34:19 +00:00
|
|
|
"""
|
2019-04-14 14:23:01 +00:00
|
|
|
The methods for loading Home Assistant integrations.
|
2014-11-15 07:17:18 +00:00
|
|
|
|
|
|
|
This module has quite some complex parts. I have tried to add as much
|
|
|
|
documentation as possible to keep it understandable.
|
2014-11-05 07:34:19 +00:00
|
|
|
"""
|
2019-04-16 03:38:24 +00:00
|
|
|
import asyncio
|
2017-07-16 16:23:06 +00:00
|
|
|
import functools as ft
|
2014-11-05 07:34:19 +00:00
|
|
|
import importlib
|
2019-04-09 16:30:32 +00:00
|
|
|
import json
|
2014-11-05 07:34:19 +00:00
|
|
|
import logging
|
2019-04-09 16:30:32 +00:00
|
|
|
import pathlib
|
2016-02-19 05:27:50 +00:00
|
|
|
import sys
|
2016-07-28 03:33:49 +00:00
|
|
|
from types import ModuleType
|
2019-04-09 16:30:32 +00:00
|
|
|
from typing import (
|
|
|
|
TYPE_CHECKING,
|
|
|
|
Any,
|
2019-12-09 15:42:10 +00:00
|
|
|
Callable,
|
2019-04-16 03:38:24 +00:00
|
|
|
Dict,
|
2019-12-09 15:42:10 +00:00
|
|
|
List,
|
|
|
|
Optional,
|
|
|
|
Set,
|
|
|
|
TypeVar,
|
2019-04-16 20:40:21 +00:00
|
|
|
Union,
|
|
|
|
cast,
|
2019-04-09 16:30:32 +00:00
|
|
|
)
|
2016-07-28 03:33:49 +00:00
|
|
|
|
2020-08-05 13:50:56 +00:00
|
|
|
from homeassistant.generated.ssdp import SSDP
|
|
|
|
from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF
|
|
|
|
|
2018-07-13 10:24:51 +00:00
|
|
|
# Typing imports that create a circular dependency
|
|
|
|
if TYPE_CHECKING:
|
2019-11-16 09:22:07 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2018-07-13 10:24:51 +00:00
|
|
|
|
2019-11-16 09:22:07 +00:00
|
|
|
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name
|
2018-07-23 08:24:39 +00:00
|
|
|
|
2014-11-05 07:34:19 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_COMPONENTS = "components"
|
|
|
|
DATA_INTEGRATIONS = "integrations"
|
|
|
|
DATA_CUSTOM_COMPONENTS = "custom_components"
|
|
|
|
PACKAGE_CUSTOM_COMPONENTS = "custom_components"
|
|
|
|
PACKAGE_BUILTIN = "homeassistant.components"
|
2019-04-15 05:31:01 +00:00
|
|
|
CUSTOM_WARNING = (
|
2019-07-31 19:25:30 +00:00
|
|
|
"You are using a custom integration for %s which has not "
|
|
|
|
"been tested by Home Assistant. This component might "
|
|
|
|
"cause stability problems, be sure to disable it if you "
|
2020-03-09 08:40:08 +00:00
|
|
|
"experience issues with Home Assistant."
|
2019-04-15 05:31:01 +00:00
|
|
|
)
|
2019-04-09 16:30:32 +00:00
|
|
|
_UNDEF = object()
|
|
|
|
|
|
|
|
|
2019-04-18 02:17:13 +00:00
|
|
|
def manifest_from_legacy_module(domain: str, module: ModuleType) -> Dict:
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Generate a manifest from a legacy module."""
|
|
|
|
return {
|
2019-07-31 19:25:30 +00:00
|
|
|
"domain": domain,
|
|
|
|
"name": domain,
|
|
|
|
"documentation": None,
|
|
|
|
"requirements": getattr(module, "REQUIREMENTS", []),
|
|
|
|
"dependencies": getattr(module, "DEPENDENCIES", []),
|
|
|
|
"codeowners": [],
|
2019-04-09 16:30:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-07-08 23:19:37 +00:00
|
|
|
async def _async_get_custom_components(
|
2019-10-29 06:32:34 +00:00
|
|
|
hass: "HomeAssistant",
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> Dict[str, "Integration"]:
|
2019-07-08 23:19:37 +00:00
|
|
|
"""Return list of custom integrations."""
|
2020-02-18 19:52:38 +00:00
|
|
|
if hass.config.safe_mode:
|
|
|
|
return {}
|
|
|
|
|
2019-07-08 23:19:37 +00:00
|
|
|
try:
|
2020-04-04 15:07:36 +00:00
|
|
|
import custom_components # pylint: disable=import-outside-toplevel
|
2019-07-08 23:19:37 +00:00
|
|
|
except ImportError:
|
|
|
|
return {}
|
|
|
|
|
2020-03-14 10:39:28 +00:00
|
|
|
def get_sub_directories(paths: List[str]) -> List[pathlib.Path]:
|
2019-07-08 23:19:37 +00:00
|
|
|
"""Return all sub directories in a set of paths."""
|
|
|
|
return [
|
|
|
|
entry
|
|
|
|
for path in paths
|
|
|
|
for entry in pathlib.Path(path).iterdir()
|
|
|
|
if entry.is_dir()
|
|
|
|
]
|
|
|
|
|
|
|
|
dirs = await hass.async_add_executor_job(
|
2019-07-31 19:25:30 +00:00
|
|
|
get_sub_directories, custom_components.__path__
|
|
|
|
)
|
2019-07-08 23:19:37 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
integrations = await asyncio.gather(
|
|
|
|
*(
|
|
|
|
hass.async_add_executor_job(
|
|
|
|
Integration.resolve_from_root, hass, custom_components, comp.name
|
|
|
|
)
|
|
|
|
for comp in dirs
|
|
|
|
)
|
|
|
|
)
|
2019-07-08 23:19:37 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
integration.domain: integration
|
|
|
|
for integration in integrations
|
|
|
|
if integration is not None
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async def async_get_custom_components(
|
2019-10-29 06:32:34 +00:00
|
|
|
hass: "HomeAssistant",
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> Dict[str, "Integration"]:
|
2019-07-08 23:19:37 +00:00
|
|
|
"""Return cached list of custom integrations."""
|
|
|
|
reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS)
|
|
|
|
|
|
|
|
if reg_or_evt is None:
|
|
|
|
evt = hass.data[DATA_CUSTOM_COMPONENTS] = asyncio.Event()
|
|
|
|
|
|
|
|
reg = await _async_get_custom_components(hass)
|
|
|
|
|
|
|
|
hass.data[DATA_CUSTOM_COMPONENTS] = reg
|
|
|
|
evt.set()
|
|
|
|
return reg
|
|
|
|
|
|
|
|
if isinstance(reg_or_evt, asyncio.Event):
|
|
|
|
await reg_or_evt.wait()
|
2019-07-31 19:25:30 +00:00
|
|
|
return cast(Dict[str, "Integration"], hass.data.get(DATA_CUSTOM_COMPONENTS))
|
2019-07-08 23:19:37 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
return cast(Dict[str, "Integration"], reg_or_evt)
|
2019-07-08 23:19:37 +00:00
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]:
|
2019-07-08 23:19:37 +00:00
|
|
|
"""Return cached list of config flows."""
|
2020-04-04 15:07:36 +00:00
|
|
|
# pylint: disable=import-outside-toplevel
|
2019-07-08 23:19:37 +00:00
|
|
|
from homeassistant.generated.config_flows import FLOWS
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2019-09-04 03:36:04 +00:00
|
|
|
flows: Set[str] = set()
|
2019-07-08 23:19:37 +00:00
|
|
|
flows.update(FLOWS)
|
|
|
|
|
|
|
|
integrations = await async_get_custom_components(hass)
|
2019-07-31 19:25:30 +00:00
|
|
|
flows.update(
|
|
|
|
[
|
|
|
|
integration.domain
|
|
|
|
for integration in integrations.values()
|
|
|
|
if integration.config_flow
|
|
|
|
]
|
|
|
|
)
|
2019-07-08 23:19:37 +00:00
|
|
|
|
|
|
|
return flows
|
|
|
|
|
|
|
|
|
2020-08-05 13:50:56 +00:00
|
|
|
async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List]:
|
|
|
|
"""Return cached list of zeroconf types."""
|
|
|
|
zeroconf: Dict[str, List] = ZEROCONF.copy()
|
|
|
|
|
|
|
|
integrations = await async_get_custom_components(hass)
|
|
|
|
for integration in integrations.values():
|
|
|
|
if not integration.zeroconf:
|
|
|
|
continue
|
|
|
|
for typ in integration.zeroconf:
|
|
|
|
zeroconf.setdefault(typ, [])
|
|
|
|
if integration.domain not in zeroconf[typ]:
|
|
|
|
zeroconf[typ].append(integration.domain)
|
|
|
|
|
|
|
|
return zeroconf
|
|
|
|
|
|
|
|
|
|
|
|
async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]:
|
|
|
|
"""Return cached list of homekit models."""
|
|
|
|
|
|
|
|
homekit: Dict[str, str] = HOMEKIT.copy()
|
|
|
|
|
|
|
|
integrations = await async_get_custom_components(hass)
|
|
|
|
for integration in integrations.values():
|
|
|
|
if (
|
|
|
|
not integration.homekit
|
|
|
|
or "models" not in integration.homekit
|
|
|
|
or not integration.homekit["models"]
|
|
|
|
):
|
|
|
|
continue
|
|
|
|
for model in integration.homekit["models"]:
|
|
|
|
homekit[model] = integration.domain
|
|
|
|
|
|
|
|
return homekit
|
|
|
|
|
|
|
|
|
|
|
|
async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List]:
|
|
|
|
"""Return cached list of ssdp mappings."""
|
|
|
|
|
|
|
|
ssdp: Dict[str, List] = SSDP.copy()
|
|
|
|
|
|
|
|
integrations = await async_get_custom_components(hass)
|
|
|
|
for integration in integrations.values():
|
|
|
|
if not integration.ssdp:
|
|
|
|
continue
|
|
|
|
|
|
|
|
ssdp[integration.domain] = integration.ssdp
|
|
|
|
|
|
|
|
return ssdp
|
|
|
|
|
|
|
|
|
2019-04-09 16:30:32 +00:00
|
|
|
class Integration:
|
|
|
|
"""An integration in Home Assistant."""
|
|
|
|
|
|
|
|
@classmethod
|
2019-07-31 19:25:30 +00:00
|
|
|
def resolve_from_root(
|
|
|
|
cls, hass: "HomeAssistant", root_module: ModuleType, domain: str
|
|
|
|
) -> "Optional[Integration]":
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Resolve an integration from a root module."""
|
2019-07-31 19:25:30 +00:00
|
|
|
for base in root_module.__path__: # type: ignore
|
|
|
|
manifest_path = pathlib.Path(base) / domain / "manifest.json"
|
2019-04-09 16:30:32 +00:00
|
|
|
|
|
|
|
if not manifest_path.is_file():
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
manifest = json.loads(manifest_path.read_text())
|
|
|
|
except ValueError as err:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"Error parsing manifest.json file at %s: %s", manifest_path, err
|
|
|
|
)
|
2019-04-09 16:30:32 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
return cls(
|
2019-08-23 16:53:33 +00:00
|
|
|
hass, f"{root_module.__name__}.{domain}", manifest_path.parent, manifest
|
2019-04-09 16:30:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
@classmethod
|
2019-07-31 19:25:30 +00:00
|
|
|
def resolve_legacy(
|
|
|
|
cls, hass: "HomeAssistant", domain: str
|
|
|
|
) -> "Optional[Integration]":
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Resolve legacy component.
|
|
|
|
|
|
|
|
Will create a stub manifest.
|
|
|
|
"""
|
2020-02-18 19:52:38 +00:00
|
|
|
comp = _load_file(hass, domain, _lookup_path(hass))
|
2019-04-09 16:30:32 +00:00
|
|
|
|
|
|
|
if comp is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return cls(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass,
|
|
|
|
comp.__name__,
|
|
|
|
pathlib.Path(comp.__file__).parent,
|
|
|
|
manifest_from_legacy_module(domain, comp),
|
2019-04-09 16:30:32 +00:00
|
|
|
)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
hass: "HomeAssistant",
|
|
|
|
pkg_path: str,
|
|
|
|
file_path: pathlib.Path,
|
2019-12-10 08:24:49 +00:00
|
|
|
manifest: Dict[str, Any],
|
2019-07-31 19:25:30 +00:00
|
|
|
):
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Initialize an integration."""
|
|
|
|
self.hass = hass
|
|
|
|
self.pkg_path = pkg_path
|
2019-04-12 17:09:17 +00:00
|
|
|
self.file_path = file_path
|
2019-12-10 08:24:49 +00:00
|
|
|
self.manifest = manifest
|
2020-04-15 20:36:16 +00:00
|
|
|
manifest["is_built_in"] = self.is_built_in
|
2020-06-20 00:24:33 +00:00
|
|
|
|
|
|
|
if self.dependencies:
|
|
|
|
self._all_dependencies_resolved: Optional[bool] = None
|
|
|
|
self._all_dependencies: Optional[Set[str]] = None
|
|
|
|
else:
|
|
|
|
self._all_dependencies_resolved = True
|
|
|
|
self._all_dependencies = set()
|
|
|
|
|
2019-04-16 03:38:24 +00:00
|
|
|
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
2019-04-09 16:30:32 +00:00
|
|
|
|
2019-12-10 08:24:49 +00:00
|
|
|
@property
|
|
|
|
def name(self) -> str:
|
|
|
|
"""Return name."""
|
|
|
|
return cast(str, self.manifest["name"])
|
|
|
|
|
2020-08-26 08:20:14 +00:00
|
|
|
@property
|
|
|
|
def disabled(self) -> Optional[str]:
|
|
|
|
"""Return reason integration is disabled."""
|
|
|
|
return cast(Optional[str], self.manifest.get("disabled"))
|
|
|
|
|
2019-12-10 08:24:49 +00:00
|
|
|
@property
|
|
|
|
def domain(self) -> str:
|
|
|
|
"""Return domain."""
|
|
|
|
return cast(str, self.manifest["domain"])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def dependencies(self) -> List[str]:
|
|
|
|
"""Return dependencies."""
|
|
|
|
return cast(List[str], self.manifest.get("dependencies", []))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def after_dependencies(self) -> List[str]:
|
|
|
|
"""Return after_dependencies."""
|
|
|
|
return cast(List[str], self.manifest.get("after_dependencies", []))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def requirements(self) -> List[str]:
|
|
|
|
"""Return requirements."""
|
|
|
|
return cast(List[str], self.manifest.get("requirements", []))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def config_flow(self) -> bool:
|
|
|
|
"""Return config_flow."""
|
|
|
|
return cast(bool, self.manifest.get("config_flow", False))
|
|
|
|
|
2019-12-16 19:16:23 +00:00
|
|
|
@property
|
|
|
|
def documentation(self) -> Optional[str]:
|
|
|
|
"""Return documentation."""
|
|
|
|
return cast(str, self.manifest.get("documentation"))
|
|
|
|
|
2020-05-05 18:00:00 +00:00
|
|
|
@property
|
|
|
|
def issue_tracker(self) -> Optional[str]:
|
|
|
|
"""Return issue tracker link."""
|
|
|
|
return cast(str, self.manifest.get("issue_tracker"))
|
|
|
|
|
2020-01-07 16:21:56 +00:00
|
|
|
@property
|
|
|
|
def quality_scale(self) -> Optional[str]:
|
|
|
|
"""Return Integration Quality Scale."""
|
|
|
|
return cast(str, self.manifest.get("quality_scale"))
|
|
|
|
|
2020-08-05 13:50:56 +00:00
|
|
|
@property
|
|
|
|
def ssdp(self) -> Optional[list]:
|
|
|
|
"""Return Integration SSDP entries."""
|
|
|
|
return cast(List[dict], self.manifest.get("ssdp"))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def zeroconf(self) -> Optional[list]:
|
|
|
|
"""Return Integration zeroconf entries."""
|
|
|
|
return cast(List[str], self.manifest.get("zeroconf"))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def homekit(self) -> Optional[dict]:
|
|
|
|
"""Return Integration homekit entries."""
|
|
|
|
return cast(Dict[str, List], self.manifest.get("homekit"))
|
|
|
|
|
2019-06-20 20:22:12 +00:00
|
|
|
@property
|
|
|
|
def is_built_in(self) -> bool:
|
|
|
|
"""Test if package is a built-in integration."""
|
|
|
|
return self.pkg_path.startswith(PACKAGE_BUILTIN)
|
|
|
|
|
2020-06-20 00:24:33 +00:00
|
|
|
@property
|
|
|
|
def all_dependencies(self) -> Set[str]:
|
|
|
|
"""Return all dependencies including sub-dependencies."""
|
|
|
|
if self._all_dependencies is None:
|
|
|
|
raise RuntimeError("Dependencies not resolved!")
|
|
|
|
|
|
|
|
return self._all_dependencies
|
|
|
|
|
|
|
|
@property
|
|
|
|
def all_dependencies_resolved(self) -> bool:
|
|
|
|
"""Return if all dependencies have been resolved."""
|
|
|
|
return self._all_dependencies_resolved is not None
|
|
|
|
|
|
|
|
async def resolve_dependencies(self) -> bool:
|
|
|
|
"""Resolve all dependencies."""
|
|
|
|
if self._all_dependencies_resolved is not None:
|
|
|
|
return self._all_dependencies_resolved
|
|
|
|
|
|
|
|
try:
|
|
|
|
dependencies = await _async_component_dependencies(
|
|
|
|
self.hass, self.domain, self, set(), set()
|
|
|
|
)
|
|
|
|
dependencies.discard(self.domain)
|
|
|
|
self._all_dependencies = dependencies
|
|
|
|
self._all_dependencies_resolved = True
|
|
|
|
except IntegrationNotFound as err:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Unable to resolve dependencies for %s: we are unable to resolve (sub)dependency %s",
|
|
|
|
self.domain,
|
|
|
|
err.domain,
|
|
|
|
)
|
|
|
|
self._all_dependencies_resolved = False
|
|
|
|
except CircularDependency as err:
|
|
|
|
_LOGGER.error(
|
|
|
|
"Unable to resolve dependencies for %s: it contains a circular dependency: %s -> %s",
|
|
|
|
self.domain,
|
|
|
|
err.from_domain,
|
|
|
|
err.to_domain,
|
|
|
|
)
|
|
|
|
self._all_dependencies_resolved = False
|
|
|
|
|
|
|
|
return self._all_dependencies_resolved
|
|
|
|
|
2019-04-11 08:26:36 +00:00
|
|
|
def get_component(self) -> ModuleType:
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Return the component."""
|
2019-04-15 02:07:05 +00:00
|
|
|
cache = self.hass.data.setdefault(DATA_COMPONENTS, {})
|
2019-04-11 08:26:36 +00:00
|
|
|
if self.domain not in cache:
|
|
|
|
cache[self.domain] = importlib.import_module(self.pkg_path)
|
|
|
|
return cache[self.domain] # type: ignore
|
2019-04-09 16:30:32 +00:00
|
|
|
|
2019-04-11 08:26:36 +00:00
|
|
|
def get_platform(self, platform_name: str) -> ModuleType:
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Return a platform for an integration."""
|
2019-04-15 02:07:05 +00:00
|
|
|
cache = self.hass.data.setdefault(DATA_COMPONENTS, {})
|
2019-08-23 16:53:33 +00:00
|
|
|
full_name = f"{self.domain}.{platform_name}"
|
2019-04-11 08:26:36 +00:00
|
|
|
if full_name not in cache:
|
|
|
|
cache[full_name] = importlib.import_module(
|
2019-08-23 16:53:33 +00:00
|
|
|
f"{self.pkg_path}.{platform_name}"
|
2019-04-11 08:26:36 +00:00
|
|
|
)
|
|
|
|
return cache[full_name] # type: ignore
|
2019-04-09 16:30:32 +00:00
|
|
|
|
2019-04-14 23:59:06 +00:00
|
|
|
def __repr__(self) -> str:
|
|
|
|
"""Text representation of class."""
|
2019-08-23 16:53:33 +00:00
|
|
|
return f"<Integration {self.domain}: {self.pkg_path}>"
|
2019-04-14 23:59:06 +00:00
|
|
|
|
2019-04-09 16:30:32 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integration:
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Get an integration."""
|
|
|
|
cache = hass.data.get(DATA_INTEGRATIONS)
|
|
|
|
if cache is None:
|
|
|
|
if not _async_mount_config_dir(hass):
|
|
|
|
raise IntegrationNotFound(domain)
|
|
|
|
cache = hass.data[DATA_INTEGRATIONS] = {}
|
|
|
|
|
2019-09-04 03:36:04 +00:00
|
|
|
int_or_evt: Union[Integration, asyncio.Event, None] = cache.get(domain, _UNDEF)
|
2019-04-09 16:30:32 +00:00
|
|
|
|
2019-04-16 03:38:24 +00:00
|
|
|
if isinstance(int_or_evt, asyncio.Event):
|
|
|
|
await int_or_evt.wait()
|
|
|
|
int_or_evt = cache.get(domain, _UNDEF)
|
|
|
|
|
2019-04-23 05:06:58 +00:00
|
|
|
# When we have waited and it's _UNDEF, it doesn't exist
|
|
|
|
# We don't cache that it doesn't exist, or else people can't fix it
|
|
|
|
# and then restart, because their config will never be valid.
|
|
|
|
if int_or_evt is _UNDEF:
|
|
|
|
raise IntegrationNotFound(domain)
|
|
|
|
|
|
|
|
if int_or_evt is not _UNDEF:
|
2019-04-16 20:40:21 +00:00
|
|
|
return cast(Integration, int_or_evt)
|
2019-04-16 03:38:24 +00:00
|
|
|
|
|
|
|
event = cache[domain] = asyncio.Event()
|
2019-04-09 16:30:32 +00:00
|
|
|
|
2019-07-08 23:19:37 +00:00
|
|
|
# Instead of using resolve_from_root we use the cache of custom
|
|
|
|
# components to find the integration.
|
|
|
|
integration = (await async_get_custom_components(hass)).get(domain)
|
|
|
|
if integration is not None:
|
|
|
|
_LOGGER.warning(CUSTOM_WARNING, domain)
|
|
|
|
cache[domain] = integration
|
|
|
|
event.set()
|
|
|
|
return integration
|
2019-04-09 16:30:32 +00:00
|
|
|
|
2020-04-04 15:07:36 +00:00
|
|
|
from homeassistant import components # pylint: disable=import-outside-toplevel
|
2019-04-09 16:30:32 +00:00
|
|
|
|
|
|
|
integration = await hass.async_add_executor_job(
|
|
|
|
Integration.resolve_from_root, hass, components, domain
|
|
|
|
)
|
|
|
|
|
|
|
|
if integration is not None:
|
|
|
|
cache[domain] = integration
|
2019-04-16 03:38:24 +00:00
|
|
|
event.set()
|
2019-04-09 16:30:32 +00:00
|
|
|
return integration
|
|
|
|
|
2019-04-16 03:38:24 +00:00
|
|
|
integration = Integration.resolve_legacy(hass, domain)
|
2019-04-23 05:06:58 +00:00
|
|
|
if integration is not None:
|
|
|
|
cache[domain] = integration
|
|
|
|
else:
|
|
|
|
# Remove event from cache.
|
|
|
|
cache.pop(domain)
|
|
|
|
|
2019-04-16 03:38:24 +00:00
|
|
|
event.set()
|
2019-04-09 16:30:32 +00:00
|
|
|
|
|
|
|
if not integration:
|
|
|
|
raise IntegrationNotFound(domain)
|
|
|
|
|
|
|
|
return integration
|
2016-10-27 07:16:23 +00:00
|
|
|
|
2014-11-05 15:56:36 +00:00
|
|
|
|
2019-02-07 21:56:40 +00:00
|
|
|
class LoaderError(Exception):
|
|
|
|
"""Loader base error."""
|
|
|
|
|
|
|
|
|
2019-04-09 16:30:32 +00:00
|
|
|
class IntegrationNotFound(LoaderError):
|
2019-02-07 21:56:40 +00:00
|
|
|
"""Raised when a component is not found."""
|
|
|
|
|
|
|
|
def __init__(self, domain: str) -> None:
|
|
|
|
"""Initialize a component not found error."""
|
2019-09-27 15:48:48 +00:00
|
|
|
super().__init__(f"Integration '{domain}' not found.")
|
2019-02-07 21:56:40 +00:00
|
|
|
self.domain = domain
|
|
|
|
|
|
|
|
|
|
|
|
class CircularDependency(LoaderError):
|
|
|
|
"""Raised when a circular dependency is found when resolving components."""
|
|
|
|
|
|
|
|
def __init__(self, from_domain: str, to_domain: str) -> None:
|
|
|
|
"""Initialize circular dependency error."""
|
2019-08-23 16:53:33 +00:00
|
|
|
super().__init__(f"Circular dependency detected: {from_domain} -> {to_domain}.")
|
2019-02-07 21:56:40 +00:00
|
|
|
self.from_domain = from_domain
|
|
|
|
self.to_domain = to_domain
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def _load_file(
|
2019-09-07 06:48:58 +00:00
|
|
|
hass: "HomeAssistant", comp_or_platform: str, base_paths: List[str]
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> Optional[ModuleType]:
|
2019-01-11 19:30:22 +00:00
|
|
|
"""Try to load specified file.
|
|
|
|
|
2018-05-07 09:25:48 +00:00
|
|
|
Looks in config dir first, then built-in components.
|
|
|
|
Only returns it if also found to be valid.
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2018-05-01 18:57:30 +00:00
|
|
|
try:
|
2019-04-15 02:07:05 +00:00
|
|
|
return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore
|
2018-05-01 18:57:30 +00:00
|
|
|
except KeyError:
|
|
|
|
pass
|
2016-10-27 07:16:23 +00:00
|
|
|
|
2019-04-15 02:07:05 +00:00
|
|
|
cache = hass.data.get(DATA_COMPONENTS)
|
2018-05-01 18:57:30 +00:00
|
|
|
if cache is None:
|
2019-04-09 16:30:32 +00:00
|
|
|
if not _async_mount_config_dir(hass):
|
2018-07-23 08:24:39 +00:00
|
|
|
return None
|
2019-04-15 02:07:05 +00:00
|
|
|
cache = hass.data[DATA_COMPONENTS] = {}
|
2014-11-12 05:39:17 +00:00
|
|
|
|
2019-08-23 16:53:33 +00:00
|
|
|
for path in (f"{base}.{comp_or_platform}" for base in base_paths):
|
2018-05-01 18:57:30 +00:00
|
|
|
try:
|
2018-05-07 09:25:48 +00:00
|
|
|
module = importlib.import_module(path)
|
2014-11-15 07:17:18 +00:00
|
|
|
|
2018-05-07 09:25:48 +00:00
|
|
|
# In Python 3 you can import files from directories that do not
|
|
|
|
# contain the file __init__.py. A directory is a valid module if
|
|
|
|
# it contains a file with the .py extension. In this case Python
|
|
|
|
# will succeed in importing the directory as a module and call it
|
|
|
|
# a namespace. We do not care about namespaces.
|
|
|
|
# This prevents that when only
|
|
|
|
# custom_components/switch/some_platform.py exists,
|
|
|
|
# the import custom_components.switch would succeed.
|
2018-07-07 14:48:02 +00:00
|
|
|
# __file__ was unset for namespaces before Python 3.7
|
2019-07-31 19:25:30 +00:00
|
|
|
if getattr(module, "__file__", None) is None:
|
2018-05-07 09:25:48 +00:00
|
|
|
continue
|
2014-11-05 07:34:19 +00:00
|
|
|
|
2018-05-07 09:25:48 +00:00
|
|
|
cache[comp_or_platform] = module
|
2018-05-01 18:57:30 +00:00
|
|
|
|
2019-02-21 22:57:38 +00:00
|
|
|
if module.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS):
|
2019-04-15 05:31:01 +00:00
|
|
|
_LOGGER.warning(CUSTOM_WARNING, comp_or_platform)
|
2018-06-27 19:21:32 +00:00
|
|
|
|
2018-05-07 09:25:48 +00:00
|
|
|
return module
|
2018-05-01 18:57:30 +00:00
|
|
|
|
2018-05-07 09:25:48 +00:00
|
|
|
except ImportError as err:
|
|
|
|
# This error happens if for example custom_components/switch
|
|
|
|
# exists and we try to load switch.demo.
|
2018-05-07 17:12:12 +00:00
|
|
|
# Ignore errors for custom_components, custom_components.switch
|
|
|
|
# and custom_components.switch.demo.
|
|
|
|
white_listed_errors = []
|
|
|
|
parts = []
|
2019-07-31 19:25:30 +00:00
|
|
|
for part in path.split("."):
|
2018-05-07 17:12:12 +00:00
|
|
|
parts.append(part)
|
2020-04-12 15:18:09 +00:00
|
|
|
white_listed_errors.append(f"No module named '{'.'.join(parts)}'")
|
2018-05-07 17:12:12 +00:00
|
|
|
|
|
|
|
if str(err) not in white_listed_errors:
|
2018-05-07 09:25:48 +00:00
|
|
|
_LOGGER.exception(
|
2020-04-05 15:48:55 +00:00
|
|
|
("Error loading %s. Make sure all dependencies are installed"), path
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2018-05-01 18:57:30 +00:00
|
|
|
|
2018-05-07 09:25:48 +00:00
|
|
|
return None
|
2014-11-28 23:34:42 +00:00
|
|
|
|
|
|
|
|
2018-07-23 08:24:39 +00:00
|
|
|
class ModuleWrapper:
|
|
|
|
"""Class to wrap a Python module and auto fill in hass argument."""
|
|
|
|
|
2019-09-07 06:48:58 +00:00
|
|
|
def __init__(self, hass: "HomeAssistant", module: ModuleType) -> None:
|
2018-07-23 08:24:39 +00:00
|
|
|
"""Initialize the module wrapper."""
|
|
|
|
self._hass = hass
|
|
|
|
self._module = module
|
|
|
|
|
|
|
|
def __getattr__(self, attr: str) -> Any:
|
|
|
|
"""Fetch an attribute."""
|
|
|
|
value = getattr(self._module, attr)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if hasattr(value, "__bind_hass"):
|
2018-07-23 08:24:39 +00:00
|
|
|
value = ft.partial(value, self._hass)
|
|
|
|
|
|
|
|
setattr(self, attr, value)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2017-07-16 16:23:06 +00:00
|
|
|
class Components:
|
|
|
|
"""Helper to load components."""
|
|
|
|
|
2019-09-07 06:48:58 +00:00
|
|
|
def __init__(self, hass: "HomeAssistant") -> None:
|
2017-07-16 16:23:06 +00:00
|
|
|
"""Initialize the Components class."""
|
|
|
|
self._hass = hass
|
|
|
|
|
2018-07-23 08:24:39 +00:00
|
|
|
def __getattr__(self, comp_name: str) -> ModuleWrapper:
|
2017-07-16 16:23:06 +00:00
|
|
|
"""Fetch a component."""
|
2019-04-15 02:07:05 +00:00
|
|
|
# Test integration cache
|
|
|
|
integration = self._hass.data.get(DATA_INTEGRATIONS, {}).get(comp_name)
|
|
|
|
|
2019-04-16 03:38:24 +00:00
|
|
|
if isinstance(integration, Integration):
|
2019-09-04 03:36:04 +00:00
|
|
|
component: Optional[ModuleType] = integration.get_component()
|
2019-04-15 02:07:05 +00:00
|
|
|
else:
|
|
|
|
# Fallback to importing old-school
|
2020-02-18 19:52:38 +00:00
|
|
|
component = _load_file(self._hass, comp_name, _lookup_path(self._hass))
|
2019-04-15 02:07:05 +00:00
|
|
|
|
2017-07-16 16:23:06 +00:00
|
|
|
if component is None:
|
2019-08-23 16:53:33 +00:00
|
|
|
raise ImportError(f"Unable to load {comp_name}")
|
2019-04-15 02:07:05 +00:00
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
wrapped = ModuleWrapper(self._hass, component)
|
2017-07-16 16:23:06 +00:00
|
|
|
setattr(self, comp_name, wrapped)
|
|
|
|
return wrapped
|
|
|
|
|
|
|
|
|
2017-10-08 15:17:54 +00:00
|
|
|
class Helpers:
|
|
|
|
"""Helper to load helpers."""
|
2017-07-16 16:23:06 +00:00
|
|
|
|
2019-09-07 06:48:58 +00:00
|
|
|
def __init__(self, hass: "HomeAssistant") -> None:
|
2017-10-08 15:17:54 +00:00
|
|
|
"""Initialize the Helpers class."""
|
|
|
|
self._hass = hass
|
|
|
|
|
2018-07-23 08:24:39 +00:00
|
|
|
def __getattr__(self, helper_name: str) -> ModuleWrapper:
|
2017-10-08 15:17:54 +00:00
|
|
|
"""Fetch a helper."""
|
2019-08-23 16:53:33 +00:00
|
|
|
helper = importlib.import_module(f"homeassistant.helpers.{helper_name}")
|
2017-10-08 15:17:54 +00:00
|
|
|
wrapped = ModuleWrapper(self._hass, helper)
|
|
|
|
setattr(self, helper_name, wrapped)
|
|
|
|
return wrapped
|
|
|
|
|
|
|
|
|
2018-07-23 08:24:39 +00:00
|
|
|
def bind_hass(func: CALLABLE_T) -> CALLABLE_T:
|
2018-01-21 06:35:38 +00:00
|
|
|
"""Decorate function to indicate that first argument is hass."""
|
2019-07-31 19:25:30 +00:00
|
|
|
setattr(func, "__bind_hass", True)
|
2017-07-16 16:23:06 +00:00
|
|
|
return func
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
async def _async_component_dependencies(
|
2020-06-20 00:24:33 +00:00
|
|
|
hass: "HomeAssistant",
|
|
|
|
start_domain: str,
|
|
|
|
integration: Integration,
|
|
|
|
loaded: Set[str],
|
|
|
|
loading: Set[str],
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> Set[str]:
|
2019-02-07 21:56:40 +00:00
|
|
|
"""Recursive function to get component dependencies.
|
2016-10-27 07:16:23 +00:00
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2020-06-20 00:24:33 +00:00
|
|
|
domain = integration.domain
|
2019-04-09 16:30:32 +00:00
|
|
|
loading.add(domain)
|
2014-11-28 23:34:42 +00:00
|
|
|
|
2019-04-09 16:30:32 +00:00
|
|
|
for dependency_domain in integration.dependencies:
|
2014-11-28 23:34:42 +00:00
|
|
|
# Check not already loaded
|
2019-04-09 16:30:32 +00:00
|
|
|
if dependency_domain in loaded:
|
2015-08-03 15:05:33 +00:00
|
|
|
continue
|
2014-11-28 23:34:42 +00:00
|
|
|
|
2016-03-07 23:06:04 +00:00
|
|
|
# If we are already loading it, we have a circular dependency.
|
2019-04-09 16:30:32 +00:00
|
|
|
if dependency_domain in loading:
|
|
|
|
raise CircularDependency(domain, dependency_domain)
|
2014-11-28 23:34:42 +00:00
|
|
|
|
2020-06-20 00:24:33 +00:00
|
|
|
loaded.add(dependency_domain)
|
|
|
|
|
|
|
|
dep_integration = await async_get_integration(hass, dependency_domain)
|
|
|
|
|
|
|
|
if start_domain in dep_integration.after_dependencies:
|
|
|
|
raise CircularDependency(start_domain, dependency_domain)
|
|
|
|
|
|
|
|
if dep_integration.dependencies:
|
|
|
|
dep_loaded = await _async_component_dependencies(
|
|
|
|
hass, start_domain, dep_integration, loaded, loading
|
|
|
|
)
|
2014-11-28 23:34:42 +00:00
|
|
|
|
2020-06-20 00:24:33 +00:00
|
|
|
loaded.update(dep_loaded)
|
2014-11-28 23:34:42 +00:00
|
|
|
|
2019-04-09 16:30:32 +00:00
|
|
|
loaded.add(domain)
|
|
|
|
loading.remove(domain)
|
2014-11-28 23:34:42 +00:00
|
|
|
|
2019-02-07 21:56:40 +00:00
|
|
|
return loaded
|
2019-04-09 16:30:32 +00:00
|
|
|
|
|
|
|
|
2019-09-07 06:48:58 +00:00
|
|
|
def _async_mount_config_dir(hass: "HomeAssistant") -> bool:
|
2019-04-09 16:30:32 +00:00
|
|
|
"""Mount config dir in order to load custom_component.
|
|
|
|
|
|
|
|
Async friendly but not a coroutine.
|
|
|
|
"""
|
|
|
|
if hass.config.config_dir is None:
|
2020-02-13 16:27:00 +00:00
|
|
|
_LOGGER.error("Can't load integrations - configuration directory is not set")
|
2019-04-09 16:30:32 +00:00
|
|
|
return False
|
|
|
|
if hass.config.config_dir not in sys.path:
|
|
|
|
sys.path.insert(0, hass.config.config_dir)
|
|
|
|
return True
|
2020-02-18 19:52:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _lookup_path(hass: "HomeAssistant") -> List[str]:
|
|
|
|
"""Return the lookup paths for legacy lookups."""
|
|
|
|
if hass.config.safe_mode:
|
|
|
|
return [PACKAGE_BUILTIN]
|
|
|
|
return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
|