From 5657dde8d48e8685fed901c613c55b9d41e9c190 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 Apr 2018 00:08:54 -0400 Subject: [PATCH 1/5] Use config entry to setup platforms --- homeassistant/components/hue/bridge.py | 16 +++--- homeassistant/components/light/__init__.py | 7 ++- homeassistant/components/light/hue.py | 14 +++-- homeassistant/config_entries.py | 21 +++++++- homeassistant/helpers/entity_component.py | 20 +++++++ homeassistant/helpers/entity_platform.py | 63 ++++++++++++++++------ tests/components/hue/test_bridge.py | 6 +-- tests/components/light/test_hue.py | 7 ++- 8 files changed, 117 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 8093c84971ed57..ac89201f746c22 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -39,18 +39,17 @@ def host(self): async def async_setup(self, tries=0): """Set up a phue bridge based on host parameter.""" host = self.host + hass = self.hass try: self.api = await get_bridge( - self.hass, host, - self.config_entry.data['username'] - ) + hass, host, self.config_entry.data['username']) except AuthenticationRequired: # usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. - self.hass.async_add_job(self.hass.config_entries.flow.async_init( + hass.async_add_job(hass.config_entries.flow.async_init( DOMAIN, source='import', data={ 'host': host, } @@ -69,7 +68,7 @@ async def retry_setup(_now): self.config_entry.state = config_entries.ENTRY_STATE_LOADED # Unhandled edge case: cancel this if we discover bridge on new IP - self.hass.helpers.event.async_call_later(retry_delay, retry_setup) + hass.helpers.event.async_call_later(retry_delay, retry_setup) return False @@ -78,11 +77,10 @@ async def retry_setup(_now): host) return False - self.hass.async_add_job( - self.hass.helpers.discovery.async_load_platform( - 'light', DOMAIN, {'host': host})) + hass.async_add_job(hass.config_entries.delegate_entry( + self.config_entry, component=hass.components.light)) - self.hass.services.async_register( + hass.services.async_register( DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 39d3203795e2e5..d497c8f9880d9b 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -334,7 +334,7 @@ async def async_handle(self, intent_obj): async def async_setup(hass, config): """Expose light control via state machine and services.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS) await component.async_setup(config) @@ -388,6 +388,11 @@ async def async_handle_light_service(service): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + class Profiles: """Representation of available color profiles.""" diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 1701b886b68735..6eb8de99c9947c 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -49,11 +49,17 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the Hue lights.""" - if discovery_info is None: - return + """Old way of setting up Hue lights. + + Can only be called when a user accidentally mentions hue platform in their + config. But even in that case it would have been ignored. + """ + pass + - bridge = hass.data[hue.DOMAIN][discovery_info['host']] +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the Hue lights from a config entry.""" + bridge = hass.data[hue.DOMAIN][config_entry.data['host']] cur_lights = {} cur_groups = {} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 69491af1aad09a..fd44ac4b59799c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -187,13 +187,17 @@ async def async_setup(self, hass, *, component=None): if not isinstance(result, bool): _LOGGER.error('%s.async_config_entry did not return boolean', - self.domain) + component.DOMAIN) result = False except Exception: # pylint: disable=broad-except _LOGGER.exception('Error setting up entry %s for %s', - self.title, self.domain) + self.title, component.DOMAIN) result = False + # Only store setup result as state if it was for component. + if self.domain != component.DOMAIN: + return + if result: self.state = ENTRY_STATE_LOADED else: @@ -336,6 +340,19 @@ async def _async_add_entry(self, entry): await async_setup_component( self.hass, entry.domain, self._hass_config) + async def delegate_entry(self, entry, component): + """Delegate an entry to a different component. + + TODO this needs a better name. + """ + # Setup Component if not set up yet + if component not in self.hass.config.components: + await async_setup_component( + self.hass, component, self._hass_config) + + await entry.async_setup( + self.hass, component=getattr(self.hass.components, component)) + @callback def _async_schedule_save(self): """Schedule saving the entity registry.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 6ff9b6f6571391..abd9c72eb7174c 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -93,6 +93,26 @@ async def component_platform_discovered(platform, info): discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered) + async def async_setup_entry(self, config_entry): + """Setup a config entry.""" + platform_type = config_entry.domain + platform = await async_prepare_setup_platform( + self.hass, self.config, self.domain, platform_type) + + if platform is None: + return + + key = config_entry.entry_id + + if key in self._platforms: + raise ValueError('Config entry has already been setup!') + + self._platforms[key] = self._async_init_entity_platform( + platform_type, platform + ) + + return await self._platforms[key].async_setup_entry(config_entry) + @callback def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 3c6deaba94af94..562ad03d05f4ca 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -42,6 +42,7 @@ def __init__(self, *, hass, logger, domain, platform_name, platform, self.scan_interval = scan_interval self.entity_namespace = entity_namespace self.async_entities_added_callback = async_entities_added_callback + self.config_entry = None self.entities = {} self._tasks = [] self._async_unsub_polling = None @@ -68,9 +69,47 @@ def __init__(self, *, hass, logger, domain, platform_name, platform, else: self.parallel_updates = None - async def async_setup(self, platform_config, discovery_info=None, tries=0): - """Setup the platform.""" + async def async_setup(self, platform_config, discovery_info=None): + """Setup the platform from a config file.""" platform = self.platform + hass = self.hass + + @callback + def async_create_setup_task(): + """Get task to setup platform.""" + if getattr(platform, 'async_setup_platform', None): + return platform.async_setup_platform( + hass, platform_config, + self._async_schedule_add_entities, discovery_info + ) + + # This should not be replaced with hass.async_add_job because + # we don't want to track this task in case it blocks startup. + return hass.loop.run_in_executor( + None, platform.setup_platform, hass, platform_config, + self._schedule_add_entities, discovery_info + ) + await self._async_setup_platform(async_create_setup_task) + + async def async_setup_entry(self, config_entry): + """Setup the platform from a config entry.""" + # Store it so that we can save config entry ID in entity registry + self.config_entry = config_entry + platform = self.platform + + @callback + def async_create_setup_task(): + """Get task to setup platform.""" + return platform.async_setup_entry( + self.hass, config_entry, self._async_schedule_add_entities) + + return await self._async_setup_platform(async_create_setup_task) + + async def _async_setup_platform(self, async_create_setup_task, tries=0): + """Helper to setup a platform via config file or config entry. + + async_create_setup_task creates a coroutine that sets up platform. + """ logger = self.logger hass = self.hass full_name = '{}.{}'.format(self.domain, self.platform_name) @@ -82,18 +121,8 @@ async def async_setup(self, platform_config, discovery_info=None, tries=0): self.platform_name, SLOW_SETUP_WARNING) try: - if getattr(platform, 'async_setup_platform', None): - task = platform.async_setup_platform( - hass, platform_config, - self._async_schedule_add_entities, discovery_info - ) - else: - # This should not be replaced with hass.async_add_job because - # we don't want to track this task in case it blocks startup. - task = hass.loop.run_in_executor( - None, platform.setup_platform, hass, platform_config, - self._schedule_add_entities, discovery_info - ) + task = async_create_setup_task() + await asyncio.wait_for( asyncio.shield(task, loop=hass.loop), SLOW_SETUP_MAX_WAIT, loop=hass.loop) @@ -108,6 +137,7 @@ async def async_setup(self, platform_config, discovery_info=None, tries=0): pending, loop=self.hass.loop) hass.config.components.add(full_name) + return True except PlatformNotReady: tries += 1 wait_time = min(tries, 6) * 30 @@ -115,16 +145,19 @@ async def async_setup(self, platform_config, discovery_info=None, tries=0): 'Platform %s not ready yet. Retrying in %d seconds.', self.platform_name, wait_time) async_track_point_in_time( - hass, self.async_setup(platform_config, discovery_info, tries), + hass, self._async_setup_platform(async_create_setup_task, tries), dt_util.utcnow() + timedelta(seconds=wait_time)) + return False except asyncio.TimeoutError: logger.error( "Setup of platform %s is taking longer than %s seconds." " Startup will proceed without waiting any longer.", self.platform_name, SLOW_SETUP_MAX_WAIT) + return False except Exception: # pylint: disable=broad-except logger.exception( "Error while setting up platform %s", self.platform_name) + return False finally: warn_task.cancel() diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 0845aa2f0773f3..0277788c5f8294 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -18,10 +18,8 @@ async def test_bridge_setup(): assert await hue_bridge.async_setup() is True assert hue_bridge.api is api - assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1 - assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == { - 'host': '1.2.3.4' - } + assert len(hass.config_entries.delegate_entry.mock_calls) == 1 + assert hass.config_entries.delegate_entry.mock_calls[0][1][0] is entry async def test_bridge_setup_invalid_username(): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 7b6c3a21a79ad9..c3b78fc739ad5a 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -9,6 +9,7 @@ from aiohue.groups import Groups import pytest +from homeassistant import config_entries from homeassistant.components import hue import homeassistant.components.light.hue as hue_light from homeassistant.util import color @@ -196,9 +197,11 @@ async def setup_bridge(hass, mock_bridge): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) hass.data[hue.DOMAIN] = {'mock-host': mock_bridge} - await hass.helpers.discovery.async_load_platform('light', 'hue', { + config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', { 'host': 'mock-host' - }) + }, 'test') + await hass.config_entries.delegate_entry(config_entry, 'light') + # To flush out the service call to update the group await hass.async_block_till_done() From 0fda466a797c041bfd2181736a49f26fe2474c3c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 Apr 2018 10:38:38 -0400 Subject: [PATCH 2/5] Rename to async_forward_entry --- homeassistant/components/hue/bridge.py | 4 +-- homeassistant/config_entries.py | 31 ++++++++++++++---------- homeassistant/helpers/entity_platform.py | 14 ++++++----- tests/components/hue/test_bridge.py | 5 ++-- tests/components/light/test_hue.py | 2 +- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index ac89201f746c22..4693a2f4dbe57f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -77,8 +77,8 @@ async def retry_setup(_now): host) return False - hass.async_add_job(hass.config_entries.delegate_entry( - self.config_entry, component=hass.components.light)) + hass.async_add_job(hass.config_entries.async_forward_entry( + self.config_entry, 'light')) hass.services.async_register( DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index fd44ac4b59799c..790877fb2a2a40 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -326,6 +326,24 @@ async def async_load(self): entries = await self.hass.async_add_job(load_json, path) self._entries = [ConfigEntry(**entry) for entry in entries] + async def async_forward_entry(self, entry, component): + """Forward the setup of an entry to a different component. + + By default an entry is setup with the component it belongs to. If that + component also has related platforms, the component will have to + forward the entry to be setup by that component. + + You don't want to await this coroutine if it is called as part of the + setup of a component, because it can cause a deadlock. + """ + # Setup Component if not set up yet + if component not in self.hass.config.components: + await async_setup_component( + self.hass, component, self._hass_config) + + await entry.async_setup( + self.hass, component=getattr(self.hass.components, component)) + async def _async_add_entry(self, entry): """Add an entry.""" self._entries.append(entry) @@ -340,19 +358,6 @@ async def _async_add_entry(self, entry): await async_setup_component( self.hass, entry.domain, self._hass_config) - async def delegate_entry(self, entry, component): - """Delegate an entry to a different component. - - TODO this needs a better name. - """ - # Setup Component if not set up yet - if component not in self.hass.config.components: - await async_setup_component( - self.hass, component, self._hass_config) - - await entry.async_setup( - self.hass, component=getattr(self.hass.components, component)) - @callback def _async_schedule_save(self): """Schedule saving the entity registry.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 562ad03d05f4ca..ba8df7e01d812b 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,15 +1,13 @@ """Class to manage the entities for a single platform.""" import asyncio -from datetime import timedelta from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) -import homeassistant.util.dt as dt_util -from .event import async_track_time_interval, async_track_point_in_time +from .event import async_track_time_interval, async_call_later from .entity_registry import async_get_registry SLOW_SETUP_WARNING = 10 @@ -144,9 +142,13 @@ async def _async_setup_platform(self, async_create_setup_task, tries=0): logger.warning( 'Platform %s not ready yet. Retrying in %d seconds.', self.platform_name, wait_time) - async_track_point_in_time( - hass, self._async_setup_platform(async_create_setup_task, tries), - dt_util.utcnow() + timedelta(seconds=wait_time)) + + async def setup_again(now): + """Run setup again.""" + await self._async_setup_platform( + async_create_setup_task, tries) + + async_call_later(hass, wait_time, setup_again) return False except asyncio.TimeoutError: logger.error( diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 0277788c5f8294..1f53d5aac14312 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -18,8 +18,9 @@ async def test_bridge_setup(): assert await hue_bridge.async_setup() is True assert hue_bridge.api is api - assert len(hass.config_entries.delegate_entry.mock_calls) == 1 - assert hass.config_entries.delegate_entry.mock_calls[0][1][0] is entry + assert len(hass.config_entries.async_forward_entry.mock_calls) == 1 + assert hass.config_entries.async_forward_entry.mock_calls[0][1] == \ + (entry, 'light') async def test_bridge_setup_invalid_username(): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index c3b78fc739ad5a..dee27adfe34735 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -200,7 +200,7 @@ async def setup_bridge(hass, mock_bridge): config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', { 'host': 'mock-host' }, 'test') - await hass.config_entries.delegate_entry(config_entry, 'light') + await hass.config_entries.async_forward_entry(config_entry, 'light') # To flush out the service call to update the group await hass.async_block_till_done() From f113d4a3365c41dd391d0fe30132a5eec2012859 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 Apr 2018 22:24:26 -0400 Subject: [PATCH 3/5] Add tests --- homeassistant/config_entries.py | 5 ++- tests/common.py | 14 +++++++- tests/helpers/test_entity_component.py | 37 ++++++++++++++++++++- tests/helpers/test_entity_platform.py | 46 +++++++++++++++++++++++++- tests/test_config_entries.py | 36 ++++++++++++++++++++ 5 files changed, 134 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 790877fb2a2a40..323966e4b02085 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -338,9 +338,12 @@ async def async_forward_entry(self, entry, component): """ # Setup Component if not set up yet if component not in self.hass.config.components: - await async_setup_component( + result = await async_setup_component( self.hass, component, self._hass_config) + if not result: + return False + await entry.async_setup( self.hass, component=getattr(self.hass.components, component)) diff --git a/tests/common.py b/tests/common.py index 388898e7024369..54c214da4e95f1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -344,7 +344,8 @@ class MockPlatform(object): # pylint: disable=invalid-name def __init__(self, setup_platform=None, dependencies=None, - platform_schema=None, async_setup_platform=None): + platform_schema=None, async_setup_platform=None, + async_setup_entry=None): """Initialize the platform.""" self.DEPENDENCIES = dependencies or [] @@ -358,6 +359,9 @@ def __init__(self, setup_platform=None, dependencies=None, if async_setup_platform is not None: self.async_setup_platform = async_setup_platform + if async_setup_entry is not None: + self.async_setup_entry = async_setup_entry + if setup_platform is None and async_setup_platform is None: self.async_setup_platform = mock_coro_func() @@ -376,6 +380,14 @@ def __init__( async_entities_added_callback=lambda: None ): """Initialize a mock entity platform.""" + if logger is None: + logger = logging.getLogger('homeassistant.helpers.entity_platform') + + # Otherwise the constructor will blow up. + if (isinstance(platform, Mock) and + isinstance(platform.PARALLEL_UPDATES, Mock)): + platform.PARALLEL_UPDATES = 0 + super().__init__( hass=hass, logger=logger, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index d8dac11f6a041f..e2332e8fefa225 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -7,6 +7,8 @@ from unittest.mock import patch, Mock from datetime import timedelta +import pytest + import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.exceptions import PlatformNotReady @@ -19,7 +21,7 @@ from tests.common import ( get_test_home_assistant, MockPlatform, MockModule, mock_coro, - async_fire_time_changed, MockEntity) + async_fire_time_changed, MockEntity, MockConfigEntry) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -333,3 +335,36 @@ def test_setup_dependencies_platform(hass): assert 'test_component' in hass.config.components assert 'test_component2' in hass.config.components assert 'test_domain.test_component' in hass.config.components + + +async def test_setup_entry(hass): + """Test setup entry calls async_setup_entry on platform.""" + mock_setup_entry = Mock(return_value=mock_coro(True)) + loader.set_component( + 'test_domain.entry_domain', + MockPlatform(async_setup_entry=mock_setup_entry)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='entry_domain') + + await component.async_setup_entry(entry) + assert len(mock_setup_entry.mock_calls) == 1 + p_hass, p_entry, p_add_entities = mock_setup_entry.mock_calls[0][1] + assert p_hass is hass + assert p_entry is entry + + +async def test_setup_entry_fails_duplicate(hass): + """Test we don't allow setting up a config entry twice.""" + mock_setup_entry = Mock(return_value=mock_coro(True)) + loader.set_component( + 'test_domain.entry_domain', + MockPlatform(async_setup_entry=mock_setup_entry)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='entry_domain') + + await component.async_setup_entry(entry) + + with pytest.raises(ValueError): + await component.async_setup_entry(entry) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 8c085e4abb13ad..a8394ff6a49784 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -5,6 +5,7 @@ from unittest.mock import patch, Mock, MagicMock from datetime import timedelta +from homeassistant.exceptions import PlatformNotReady import homeassistant.loader as loader from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import ( @@ -15,7 +16,7 @@ from tests.common import ( get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry, - MockEntity, MockEntityPlatform) + MockEntity, MockEntityPlatform, MockConfigEntry, mock_coro) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -511,3 +512,46 @@ async def test_entity_registry_updates(hass): state = hass.states.get('test_domain.world') assert state.name == 'after update' + + +async def test_setup_entry(hass): + """Test we can setup an entry.""" + async_setup_entry = Mock(return_value=mock_coro(True)) + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry() + entity_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + + full_name = '{}.{}'.format(entity_platform.domain, config_entry.domain) + assert full_name in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + + +async def test_setup_entry_platform_not_ready(hass, caplog): + """Test when an entry is not ready yet.""" + async_setup_entry = Mock(side_effect=PlatformNotReady) + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + with patch.object(entity_platform, 'async_call_later') as mock_call_later: + assert not await ent_platform.async_setup_entry(config_entry) + + full_name = '{}.{}'.format(ent_platform.domain, config_entry.domain) + assert full_name not in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + assert 'Platform test not ready yet' in caplog.text + assert len(mock_call_later.mock_calls) == 1 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 5b1ec3b8ec0d5e..8bbd79a7ac72f8 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -389,3 +389,39 @@ def async_step_discovery(self, info): assert entry.title == 'hello' assert entry.data == data assert entry.source == config_entries.SOURCE_DISCOVERY + + +async def test_forward_entry_sets_up_component(hass): + """Test we setup the component entry is forwarded to.""" + entry = MockConfigEntry(domain='original') + + mock_original_setup_entry = MagicMock(return_value=mock_coro(True)) + loader.set_component( + 'original', + MockModule('original', async_setup_entry=mock_original_setup_entry)) + + mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True)) + loader.set_component( + 'forwarded', + MockModule('forwarded', async_setup_entry=mock_forwarded_setup_entry)) + + await hass.config_entries.async_forward_entry(entry, 'forwarded') + assert len(mock_original_setup_entry.mock_calls) == 0 + assert len(mock_forwarded_setup_entry.mock_calls) == 1 + + +async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): + """Test we do not setup entry if component setup fails.""" + entry = MockConfigEntry(domain='original') + + mock_setup = MagicMock(return_value=mock_coro(False)) + mock_setup_entry = MagicMock() + loader.set_component('forwarded', MockModule( + 'forwarded', + async_setup=mock_setup, + async_setup_entry=mock_setup_entry, + )) + + await hass.config_entries.async_forward_entry(entry, 'forwarded') + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 From 8f65119339f4879e0bf39fdad0e7e309bd035435 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 Apr 2018 22:27:49 -0400 Subject: [PATCH 4/5] Catch if platform not exists for entry --- homeassistant/helpers/entity_component.py | 2 +- tests/helpers/test_entity_component.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index abd9c72eb7174c..265464d548ddf0 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -100,7 +100,7 @@ async def async_setup_entry(self, config_entry): self.hass, self.config, self.domain, platform_type) if platform is None: - return + return False key = config_entry.entry_id diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index e2332e8fefa225..f53b69274ef37e 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -347,13 +347,21 @@ async def test_setup_entry(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) entry = MockConfigEntry(domain='entry_domain') - await component.async_setup_entry(entry) + assert await component.async_setup_entry(entry) assert len(mock_setup_entry.mock_calls) == 1 p_hass, p_entry, p_add_entities = mock_setup_entry.mock_calls[0][1] assert p_hass is hass assert p_entry is entry +async def test_setup_entry_platform_not_exist(hass): + """Test setup entry fails if platform doesnt exist.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='non_existing') + + assert (await component.async_setup_entry(entry)) is False + + async def test_setup_entry_fails_duplicate(hass): """Test we don't allow setting up a config entry twice.""" mock_setup_entry = Mock(return_value=mock_coro(True)) @@ -364,7 +372,7 @@ async def test_setup_entry_fails_duplicate(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) entry = MockConfigEntry(domain='entry_domain') - await component.async_setup_entry(entry) + assert await component.async_setup_entry(entry) with pytest.raises(ValueError): await component.async_setup_entry(entry) From 0ae686ba77bc48ac039747389fb09dc62128e972 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Apr 2018 10:08:43 -0400 Subject: [PATCH 5/5] Clarify comment --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 323966e4b02085..fc781bd62c8368 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -194,7 +194,7 @@ async def async_setup(self, hass, *, component=None): self.title, component.DOMAIN) result = False - # Only store setup result as state if it was for component. + # Only store setup result as state if it was not forwarded. if self.domain != component.DOMAIN: return