Support config flow on custom components ()

* 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
Joakim Plate 2019-07-09 01:19:37 +02:00 committed by GitHub
parent a2237ce5d4
commit 2fbbcafaed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 31 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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'])
############################

View File

@ -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