diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 45e1df5907c..9687a407ccb 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -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): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bfd8c0f2df7..a018713dee7 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -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) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index f008551c0fa..2ec343ad0c7 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -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) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 5a597d33d43..653fd60f368 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -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 diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index cdce7433398..594ac5d9762 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -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']) ############################ diff --git a/tests/test_loader.py b/tests/test_loader.py index cd0cb692702..2b8b5ab79b0 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -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