Support config flow on custom components (#24946)
* Support populating list of flows from custom components * Re-allow custom component config flows * Add tests for custom component retrieval * Don't crash view if no handler exist * Use get_custom_components instead fo resolve_from_root * Switch to using an event instead of lock * Leave list of integrations as set * The returned list is not guaranteed to be ordered Backend uses a set to represent them.pull/25033/head
parent
a2237ce5d4
commit
2fbbcafaed
homeassistant
tests
components/config
|
@ -1,12 +1,11 @@
|
|||
"""Http views to control the config manager."""
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.generated import config_flows
|
||||
from homeassistant.loader import async_get_config_flows
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
|
@ -61,7 +60,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
|
|||
'state': entry.state,
|
||||
'connection_class': entry.connection_class,
|
||||
'supports_options': hasattr(
|
||||
config_entries.HANDLERS[entry.domain],
|
||||
config_entries.HANDLERS.get(entry.domain),
|
||||
'async_get_options_flow'),
|
||||
} for entry in hass.config_entries.async_entries()])
|
||||
|
||||
|
@ -173,7 +172,8 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
|
|||
|
||||
async def get(self, request):
|
||||
"""List available flow handlers."""
|
||||
return self.json(config_flows.FLOWS)
|
||||
hass = request.app['hass']
|
||||
return self.json(await async_get_config_flows(hass))
|
||||
|
||||
|
||||
class OptionManagerFlowIndexView(FlowManagerIndexView):
|
||||
|
|
|
@ -553,14 +553,6 @@ class ConfigEntries:
|
|||
_LOGGER.error('Cannot find integration %s', handler_key)
|
||||
raise data_entry_flow.UnknownHandler
|
||||
|
||||
# Our config flow list is based on built-in integrations. If overriden,
|
||||
# we should not load it's config flow.
|
||||
if not integration.is_built_in:
|
||||
_LOGGER.error(
|
||||
'Config flow is not supported for custom integration %s',
|
||||
handler_key)
|
||||
raise data_entry_flow.UnknownHandler
|
||||
|
||||
# Make sure requirements and dependencies of component are resolved
|
||||
await async_process_deps_reqs(
|
||||
self.hass, self._hass_config, integration)
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
import logging
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from homeassistant.loader import async_get_integration, bind_hass
|
||||
from homeassistant.loader import (
|
||||
async_get_integration, bind_hass, async_get_config_flows)
|
||||
from homeassistant.util.json import load_json
|
||||
from homeassistant.generated import config_flows
|
||||
from .typing import HomeAssistantType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -106,7 +106,8 @@ async def async_get_component_resources(hass: HomeAssistantType,
|
|||
translation_cache = hass.data[TRANSLATION_STRING_CACHE][language]
|
||||
|
||||
# Get the set of components
|
||||
components = hass.config.components | set(config_flows.FLOWS)
|
||||
components = (hass.config.components |
|
||||
await async_get_config_flows(hass))
|
||||
|
||||
# Calculate the missing components
|
||||
missing_components = components - set(translation_cache)
|
||||
|
|
|
@ -36,9 +36,9 @@ DEPENDENCY_BLACKLIST = {'config'}
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DATA_COMPONENTS = 'components'
|
||||
DATA_INTEGRATIONS = 'integrations'
|
||||
DATA_CUSTOM_COMPONENTS = 'custom_components'
|
||||
PACKAGE_CUSTOM_COMPONENTS = 'custom_components'
|
||||
PACKAGE_BUILTIN = 'homeassistant.components'
|
||||
LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
|
||||
|
@ -63,6 +63,81 @@ def manifest_from_legacy_module(domain: str, module: ModuleType) -> Dict:
|
|||
}
|
||||
|
||||
|
||||
async def _async_get_custom_components(
|
||||
hass: 'HomeAssistant') -> Dict[str, 'Integration']:
|
||||
"""Return list of custom integrations."""
|
||||
try:
|
||||
import custom_components
|
||||
except ImportError:
|
||||
return {}
|
||||
|
||||
def get_sub_directories(paths: List) -> List:
|
||||
"""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(
|
||||
get_sub_directories, custom_components.__path__)
|
||||
|
||||
integrations = await asyncio.gather(*[
|
||||
hass.async_add_executor_job(
|
||||
Integration.resolve_from_root,
|
||||
hass,
|
||||
custom_components,
|
||||
comp.name)
|
||||
for comp in dirs
|
||||
])
|
||||
|
||||
return {
|
||||
integration.domain: integration
|
||||
for integration in integrations
|
||||
if integration is not None
|
||||
}
|
||||
|
||||
|
||||
async def async_get_custom_components(
|
||||
hass: 'HomeAssistant') -> Dict[str, 'Integration']:
|
||||
"""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()
|
||||
return cast(Dict[str, 'Integration'],
|
||||
hass.data.get(DATA_CUSTOM_COMPONENTS))
|
||||
|
||||
return cast(Dict[str, 'Integration'],
|
||||
reg_or_evt)
|
||||
|
||||
|
||||
async def async_get_config_flows(hass: 'HomeAssistant') -> Set[str]:
|
||||
"""Return cached list of config flows."""
|
||||
from homeassistant.generated.config_flows import FLOWS
|
||||
flows = set() # type: Set[str]
|
||||
flows.update(FLOWS)
|
||||
|
||||
integrations = await async_get_custom_components(hass)
|
||||
flows.update([
|
||||
integration.domain
|
||||
for integration in integrations.values()
|
||||
if integration.config_flow
|
||||
])
|
||||
|
||||
return flows
|
||||
|
||||
|
||||
class Integration:
|
||||
"""An integration in Home Assistant."""
|
||||
|
||||
|
@ -121,6 +196,7 @@ class Integration:
|
|||
self.after_dependencies = manifest.get(
|
||||
'after_dependencies') # type: Optional[List[str]]
|
||||
self.requirements = manifest['requirements'] # type: List[str]
|
||||
self.config_flow = manifest.get('config_flow', False) # type: bool
|
||||
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
||||
|
||||
@property
|
||||
|
@ -177,20 +253,14 @@ async def async_get_integration(hass: 'HomeAssistant', domain: str)\
|
|||
|
||||
event = cache[domain] = asyncio.Event()
|
||||
|
||||
try:
|
||||
import custom_components
|
||||
integration = await hass.async_add_executor_job(
|
||||
Integration.resolve_from_root, hass, custom_components, domain
|
||||
)
|
||||
if integration is not None:
|
||||
_LOGGER.warning(CUSTOM_WARNING, domain)
|
||||
cache[domain] = integration
|
||||
event.set()
|
||||
return integration
|
||||
|
||||
except ImportError:
|
||||
# Import error if "custom_components" doesn't exist
|
||||
pass
|
||||
# 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
|
||||
|
||||
from homeassistant import components
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ def test_available_flows(hass, client):
|
|||
'/api/config/config_entries/flow_handlers')
|
||||
assert resp.status == 200
|
||||
data = yield from resp.json()
|
||||
assert data == ['hello', 'world']
|
||||
assert set(data) == set(['hello', 'world'])
|
||||
|
||||
|
||||
############################
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Test to verify that we can load components."""
|
||||
from asynctest.mock import ANY, patch
|
||||
import pytest
|
||||
|
||||
import homeassistant.loader as loader
|
||||
|
@ -172,3 +173,57 @@ async def test_integrations_only_once(hass):
|
|||
loader.async_get_integration(hass, 'hue'))
|
||||
|
||||
assert await int_1 is await int_2
|
||||
|
||||
|
||||
async def test_get_custom_components_internal(hass):
|
||||
"""Test that we can a list of custom components."""
|
||||
# pylint: disable=protected-access
|
||||
integrations = await loader._async_get_custom_components(hass)
|
||||
assert integrations == {
|
||||
'test': ANY,
|
||||
"test_package": ANY
|
||||
}
|
||||
|
||||
|
||||
def _get_test_integration(hass, name, config_flow):
|
||||
"""Return a generated test integration."""
|
||||
return loader.Integration(
|
||||
hass, "homeassistant.components.{}".format(name), None, {
|
||||
'name': name,
|
||||
'domain': name,
|
||||
'config_flow': config_flow,
|
||||
'dependencies': [],
|
||||
'requirements': []})
|
||||
|
||||
|
||||
async def test_get_custom_components(hass):
|
||||
"""Verify that custom components are cached."""
|
||||
test_1_integration = _get_test_integration(hass, 'test_1', False)
|
||||
test_2_integration = _get_test_integration(hass, 'test_2', True)
|
||||
|
||||
name = 'homeassistant.loader._async_get_custom_components'
|
||||
with patch(name) as mock_get:
|
||||
mock_get.return_value = {
|
||||
'test_1': test_1_integration,
|
||||
'test_2': test_2_integration,
|
||||
}
|
||||
integrations = await loader.async_get_custom_components(hass)
|
||||
assert integrations == mock_get.return_value
|
||||
integrations = await loader.async_get_custom_components(hass)
|
||||
assert integrations == mock_get.return_value
|
||||
mock_get.assert_called_once_with(hass)
|
||||
|
||||
|
||||
async def test_get_config_flows(hass):
|
||||
"""Verify that custom components with config_flow are available."""
|
||||
test_1_integration = _get_test_integration(hass, 'test_1', False)
|
||||
test_2_integration = _get_test_integration(hass, 'test_2', True)
|
||||
|
||||
with patch('homeassistant.loader.async_get_custom_components') as mock_get:
|
||||
mock_get.return_value = {
|
||||
'test_1': test_1_integration,
|
||||
'test_2': test_2_integration,
|
||||
}
|
||||
flows = await loader.async_get_config_flows(hass)
|
||||
assert 'test_2' in flows
|
||||
assert 'test_1' not in flows
|
||||
|
|
Loading…
Reference in New Issue