Install discovery requirements if used (#29795)
* Install discovery requirements if used * Update loader.py * Fix typespull/29824/head
parent
0ed6a434f8
commit
27244e29c4
|
@ -7,6 +7,7 @@ from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
@ -169,10 +170,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
async def async_step_ssdp(self, discovery_info):
|
async def async_step_ssdp(self, discovery_info):
|
||||||
"""Handle a discovered deCONZ bridge."""
|
"""Handle a discovered deCONZ bridge."""
|
||||||
# Import it here, because only now do we know ssdp integration loaded.
|
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
|
|
||||||
|
|
||||||
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
|
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
|
||||||
return self.async_abort(reason="not_deconz_bridge")
|
return self.async_abort(reason="not_deconz_bridge")
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
@ -134,9 +135,6 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
This flow is triggered by the SSDP component. It will check if the
|
This flow is triggered by the SSDP component. It will check if the
|
||||||
host is already configured and delegate to the import step if not.
|
host is already configured and delegate to the import step if not.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=import-outside-toplevel
|
|
||||||
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME
|
|
||||||
|
|
||||||
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
|
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
|
||||||
return self.async_abort(reason="not_hue_bridge")
|
return self.async_abort(reason="not_hue_bridge")
|
||||||
|
|
||||||
|
|
|
@ -195,22 +195,45 @@ class Integration:
|
||||||
hass: "HomeAssistant",
|
hass: "HomeAssistant",
|
||||||
pkg_path: str,
|
pkg_path: str,
|
||||||
file_path: pathlib.Path,
|
file_path: pathlib.Path,
|
||||||
manifest: Dict,
|
manifest: Dict[str, Any],
|
||||||
):
|
):
|
||||||
"""Initialize an integration."""
|
"""Initialize an integration."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.pkg_path = pkg_path
|
self.pkg_path = pkg_path
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.name: str = manifest["name"]
|
self.manifest = manifest
|
||||||
self.domain: str = manifest["domain"]
|
|
||||||
self.dependencies: List[str] = manifest["dependencies"]
|
|
||||||
self.after_dependencies: Optional[List[str]] = manifest.get(
|
|
||||||
"after_dependencies"
|
|
||||||
)
|
|
||||||
self.requirements: List[str] = manifest["requirements"]
|
|
||||||
self.config_flow: bool = manifest.get("config_flow", False)
|
|
||||||
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return name."""
|
||||||
|
return cast(str, self.manifest["name"])
|
||||||
|
|
||||||
|
@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))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_built_in(self) -> bool:
|
def is_built_in(self) -> bool:
|
||||||
"""Test if package is a built-in integration."""
|
"""Test if package is a built-in integration."""
|
||||||
|
|
|
@ -3,7 +3,7 @@ import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Set
|
from typing import Any, Dict, List, Optional, Set, Iterable
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
@ -15,6 +15,10 @@ DATA_PKG_CACHE = "pkg_cache"
|
||||||
CONSTRAINT_FILE = "package_constraints.txt"
|
CONSTRAINT_FILE = "package_constraints.txt"
|
||||||
PROGRESS_FILE = ".pip_progress"
|
PROGRESS_FILE = ".pip_progress"
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
|
||||||
|
"ssdp": ("ssdp",),
|
||||||
|
"zeroconf": ("zeroconf", "homekit"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RequirementsNotFound(HomeAssistantError):
|
class RequirementsNotFound(HomeAssistantError):
|
||||||
|
@ -30,7 +34,7 @@ class RequirementsNotFound(HomeAssistantError):
|
||||||
async def async_get_integration_with_requirements(
|
async def async_get_integration_with_requirements(
|
||||||
hass: HomeAssistant, domain: str, done: Set[str] = None
|
hass: HomeAssistant, domain: str, done: Set[str] = None
|
||||||
) -> Integration:
|
) -> Integration:
|
||||||
"""Get an integration with installed requirements.
|
"""Get an integration with all requirements installed, including the dependencies.
|
||||||
|
|
||||||
This can raise IntegrationNotFound if manifest or integration
|
This can raise IntegrationNotFound if manifest or integration
|
||||||
is invalid, RequirementNotFound if there was some type of
|
is invalid, RequirementNotFound if there was some type of
|
||||||
|
@ -53,10 +57,18 @@ async def async_get_integration_with_requirements(
|
||||||
|
|
||||||
deps_to_check = [
|
deps_to_check = [
|
||||||
dep
|
dep
|
||||||
for dep in integration.dependencies + (integration.after_dependencies or [])
|
for dep in integration.dependencies + integration.after_dependencies
|
||||||
if dep not in done
|
if dep not in done
|
||||||
]
|
]
|
||||||
|
|
||||||
|
for check_domain, to_check in DISCOVERY_INTEGRATIONS.items():
|
||||||
|
if (
|
||||||
|
check_domain not in done
|
||||||
|
and check_domain not in deps_to_check
|
||||||
|
and any(check in integration.manifest for check in to_check)
|
||||||
|
):
|
||||||
|
deps_to_check.append(check_domain)
|
||||||
|
|
||||||
if deps_to_check:
|
if deps_to_check:
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[
|
*[
|
||||||
|
|
|
@ -3,6 +3,8 @@ import pathlib
|
||||||
import re
|
import re
|
||||||
from typing import Dict, Set
|
from typing import Dict, Set
|
||||||
|
|
||||||
|
from homeassistant.requirements import DISCOVERY_INTEGRATIONS
|
||||||
|
|
||||||
from .model import Integration
|
from .model import Integration
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,7 +51,6 @@ ALLOWED_USED_COMPONENTS = {
|
||||||
"system_log",
|
"system_log",
|
||||||
"person",
|
"person",
|
||||||
# Discovery
|
# Discovery
|
||||||
"ssdp",
|
|
||||||
"discovery",
|
"discovery",
|
||||||
# Other
|
# Other
|
||||||
"mjpeg", # base class, has no reqs or component to load.
|
"mjpeg", # base class, has no reqs or component to load.
|
||||||
|
@ -93,6 +94,13 @@ def validate_dependencies(integration: Integration):
|
||||||
referenced -= set(integration.manifest["dependencies"])
|
referenced -= set(integration.manifest["dependencies"])
|
||||||
referenced -= set(integration.manifest.get("after_dependencies", []))
|
referenced -= set(integration.manifest.get("after_dependencies", []))
|
||||||
|
|
||||||
|
# Discovery requirements are ok if referenced in manifest
|
||||||
|
for check_domain, to_check in DISCOVERY_INTEGRATIONS.items():
|
||||||
|
if check_domain in referenced and any(
|
||||||
|
check in integration.manifest for check in to_check
|
||||||
|
):
|
||||||
|
referenced.remove(check_domain)
|
||||||
|
|
||||||
if referenced:
|
if referenced:
|
||||||
for domain in sorted(referenced):
|
for domain in sorted(referenced):
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import call, patch
|
from unittest.mock import call, patch
|
||||||
|
import pytest
|
||||||
|
|
||||||
from pytest import raises
|
from homeassistant import setup, loader
|
||||||
|
|
||||||
from homeassistant import setup
|
|
||||||
from homeassistant.requirements import (
|
from homeassistant.requirements import (
|
||||||
CONSTRAINT_FILE,
|
CONSTRAINT_FILE,
|
||||||
PROGRESS_FILE,
|
PROGRESS_FILE,
|
||||||
|
@ -15,7 +14,12 @@ from homeassistant.requirements import (
|
||||||
async_process_requirements,
|
async_process_requirements,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tests.common import MockModule, get_test_home_assistant, mock_integration
|
from tests.common import (
|
||||||
|
MockModule,
|
||||||
|
get_test_home_assistant,
|
||||||
|
mock_coro,
|
||||||
|
mock_integration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def env_without_wheel_links():
|
def env_without_wheel_links():
|
||||||
|
@ -104,7 +108,7 @@ async def test_install_missing_package(hass):
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.util.package.install_package", return_value=False
|
"homeassistant.util.package.install_package", return_value=False
|
||||||
) as mock_inst:
|
) as mock_inst:
|
||||||
with raises(RequirementsNotFound):
|
with pytest.raises(RequirementsNotFound):
|
||||||
await async_process_requirements(hass, "test_component", ["hello==1.0.0"])
|
await async_process_requirements(hass, "test_component", ["hello==1.0.0"])
|
||||||
|
|
||||||
assert len(mock_inst.mock_calls) == 1
|
assert len(mock_inst.mock_calls) == 1
|
||||||
|
@ -222,3 +226,44 @@ async def test_progress_lock(hass):
|
||||||
_install(hass, "hello", kwargs)
|
_install(hass, "hello", kwargs)
|
||||||
|
|
||||||
assert not progress_path.exists()
|
assert not progress_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_discovery_requirements_ssdp(hass):
|
||||||
|
"""Test that we load discovery requirements."""
|
||||||
|
hass.config.skip_pip = False
|
||||||
|
ssdp = await loader.async_get_integration(hass, "ssdp")
|
||||||
|
|
||||||
|
mock_integration(
|
||||||
|
hass, MockModule("ssdp_comp", partial_manifest={"ssdp": [{"st": "roku:ecp"}]})
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.requirements.async_process_requirements",
|
||||||
|
side_effect=lambda _, _2, _3: mock_coro(),
|
||||||
|
) as mock_process:
|
||||||
|
await async_get_integration_with_requirements(hass, "ssdp_comp")
|
||||||
|
|
||||||
|
assert len(mock_process.mock_calls) == 1
|
||||||
|
assert mock_process.mock_calls[0][1][2] == ssdp.requirements
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"partial_manifest",
|
||||||
|
[{"zeroconf": ["_googlecast._tcp.local."]}, {"homekit": {"models": ["LIFX"]}}],
|
||||||
|
)
|
||||||
|
async def test_discovery_requirements_zeroconf(hass, partial_manifest):
|
||||||
|
"""Test that we load discovery requirements."""
|
||||||
|
hass.config.skip_pip = False
|
||||||
|
zeroconf = await loader.async_get_integration(hass, "zeroconf")
|
||||||
|
|
||||||
|
mock_integration(
|
||||||
|
hass, MockModule("comp", partial_manifest=partial_manifest),
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.requirements.async_process_requirements",
|
||||||
|
side_effect=lambda _, _2, _3: mock_coro(),
|
||||||
|
) as mock_process:
|
||||||
|
await async_get_integration_with_requirements(hass, "comp")
|
||||||
|
|
||||||
|
assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http
|
||||||
|
assert mock_process.mock_calls[0][1][2] == zeroconf.requirements
|
||||||
|
|
Loading…
Reference in New Issue