From d73b695e73c6dec198405e4eb90d63b327f6371e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Jun 2017 09:41:48 -0700 Subject: [PATCH] EntityComponent to retry platforms that are not ready yet (#8209) * Add PlatformNotReady Exception * lint * Remove cap, adjust algorithm --- homeassistant/core.py | 2 +- homeassistant/exceptions.py | 6 +++ homeassistant/helpers/entity_component.py | 19 +++++++-- tests/helpers/test_entity_component.py | 47 ++++++++++++++++++++++- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 37b39ed17db5cd..cb6c3522496046 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -240,7 +240,7 @@ def async_run_job(self, target: Callable[..., None], *args: Any) -> None: target: target to call. args: parameters for method to call. """ - if is_callback(target): + if not asyncio.iscoroutine(target) and is_callback(target): target(*args) else: self.async_add_job(target, *args) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 4981e13beebef8..2889d83af5ce48 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -26,3 +26,9 @@ def __init__(self, exception): """Init the error.""" super().__init__('{}: {}'.format(exception.__class__.__name__, exception)) + + +class PlatformNotReady(HomeAssistantError): + """Error to indicate that platform is not ready.""" + + pass diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 8cfc9984e2ed43..2833010789ef5b 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -8,19 +8,22 @@ ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, DEVICE_DEFAULT_NAME) from homeassistant.core import callback, valid_entity_id -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.loader import get_component from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import ( + async_track_time_interval, async_track_point_in_time) from homeassistant.helpers.service import extract_entity_ids from homeassistant.util import slugify from homeassistant.util.async import ( run_callback_threadsafe, run_coroutine_threadsafe) +import homeassistant.util.dt as dt_util DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 +PLATFORM_NOT_READY_RETRIES = 10 class EntityComponent(object): @@ -113,7 +116,7 @@ def async_extract_from_service(self, service, expand_group=True): @asyncio.coroutine def _async_setup_platform(self, platform_type, platform_config, - discovery_info=None): + discovery_info=None, tries=0): """Set up a platform for this component. This method must be run in the event loop. @@ -162,6 +165,16 @@ def _async_setup_platform(self, platform_type, platform_config, yield from entity_platform.async_block_entities_done() self.hass.config.components.add( '{}.{}'.format(self.domain, platform_type)) + except PlatformNotReady: + tries += 1 + wait_time = min(tries, 6) * 30 + self.logger.warning( + 'Platform %s not ready yet. Retrying in %d seconds.', + platform_type, wait_time) + async_track_point_in_time( + self.hass, self._async_setup_platform( + platform_type, platform_config, discovery_info, tries), + dt_util.utcnow() + timedelta(seconds=wait_time)) except asyncio.TimeoutError: self.logger.error( "Setup of platform %s is taking longer than %s seconds." diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index f68090358c7876..11717c75e20297 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -9,6 +9,7 @@ import homeassistant.core as ha import homeassistant.loader as loader +from homeassistant.exceptions import PlatformNotReady from homeassistant.components import group from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.entity_component import ( @@ -21,7 +22,7 @@ from tests.common import ( get_test_home_assistant, MockPlatform, MockModule, fire_time_changed, - mock_coro) + mock_coro, async_fire_time_changed) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -533,3 +534,47 @@ def test_extract_from_service_available_device(hass): assert ['test_domain.test_3'] == \ sorted(ent.entity_id for ent in component.async_extract_from_service(call_2)) + + +@asyncio.coroutine +def test_platform_not_ready(hass): + """Test that we retry when platform not ready.""" + platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, + None]) + loader.set_component('test_domain.mod1', MockPlatform(platform1_setup)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_setup({ + DOMAIN: { + 'platform': 'mod1' + } + }) + + assert len(platform1_setup.mock_calls) == 1 + assert 'test_domain.mod1' not in hass.config.components + + utcnow = dt_util.utcnow() + + with patch('homeassistant.util.dt.utcnow', return_value=utcnow): + # Should not trigger attempt 2 + async_fire_time_changed(hass, utcnow + timedelta(seconds=29)) + yield from hass.async_block_till_done() + assert len(platform1_setup.mock_calls) == 1 + + # Should trigger attempt 2 + async_fire_time_changed(hass, utcnow + timedelta(seconds=30)) + yield from hass.async_block_till_done() + assert len(platform1_setup.mock_calls) == 2 + assert 'test_domain.mod1' not in hass.config.components + + # This should not trigger attempt 3 + async_fire_time_changed(hass, utcnow + timedelta(seconds=59)) + yield from hass.async_block_till_done() + assert len(platform1_setup.mock_calls) == 2 + + # Trigger attempt 3, which succeeds + async_fire_time_changed(hass, utcnow + timedelta(seconds=60)) + yield from hass.async_block_till_done() + assert len(platform1_setup.mock_calls) == 3 + assert 'test_domain.mod1' in hass.config.components