EntityComponent to retry platforms that are not ready yet (#8209)

* Add PlatformNotReady Exception

* lint

* Remove cap, adjust algorithm
This commit is contained in:
Paulus Schoutsen 2017-06-26 09:41:48 -07:00 committed by GitHub
parent f02d169864
commit d73b695e73
4 changed files with 69 additions and 5 deletions

View File

@ -240,7 +240,7 @@ class HomeAssistant(object):
target: target to call. target: target to call.
args: parameters for method to call. args: parameters for method to call.
""" """
if is_callback(target): if not asyncio.iscoroutine(target) and is_callback(target):
target(*args) target(*args)
else: else:
self.async_add_job(target, *args) self.async_add_job(target, *args)

View File

@ -26,3 +26,9 @@ class TemplateError(HomeAssistantError):
"""Init the error.""" """Init the error."""
super().__init__('{}: {}'.format(exception.__class__.__name__, super().__init__('{}: {}'.format(exception.__class__.__name__,
exception)) exception))
class PlatformNotReady(HomeAssistantError):
"""Error to indicate that platform is not ready."""
pass

View File

@ -8,19 +8,22 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE,
DEVICE_DEFAULT_NAME) DEVICE_DEFAULT_NAME)
from homeassistant.core import callback, valid_entity_id 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.loader import get_component
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import async_generate_entity_id 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.helpers.service import extract_entity_ids
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.util.async import ( from homeassistant.util.async import (
run_callback_threadsafe, run_coroutine_threadsafe) run_callback_threadsafe, run_coroutine_threadsafe)
import homeassistant.util.dt as dt_util
DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
SLOW_SETUP_WARNING = 10 SLOW_SETUP_WARNING = 10
SLOW_SETUP_MAX_WAIT = 60 SLOW_SETUP_MAX_WAIT = 60
PLATFORM_NOT_READY_RETRIES = 10
class EntityComponent(object): class EntityComponent(object):
@ -113,7 +116,7 @@ class EntityComponent(object):
@asyncio.coroutine @asyncio.coroutine
def _async_setup_platform(self, platform_type, platform_config, def _async_setup_platform(self, platform_type, platform_config,
discovery_info=None): discovery_info=None, tries=0):
"""Set up a platform for this component. """Set up a platform for this component.
This method must be run in the event loop. This method must be run in the event loop.
@ -162,6 +165,16 @@ class EntityComponent(object):
yield from entity_platform.async_block_entities_done() yield from entity_platform.async_block_entities_done()
self.hass.config.components.add( self.hass.config.components.add(
'{}.{}'.format(self.domain, platform_type)) '{}.{}'.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: except asyncio.TimeoutError:
self.logger.error( self.logger.error(
"Setup of platform %s is taking longer than %s seconds." "Setup of platform %s is taking longer than %s seconds."

View File

@ -9,6 +9,7 @@ from datetime import timedelta
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.exceptions import PlatformNotReady
from homeassistant.components import group from homeassistant.components import group
from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.entity import Entity, generate_entity_id
from homeassistant.helpers.entity_component import ( from homeassistant.helpers.entity_component import (
@ -21,7 +22,7 @@ import homeassistant.util.dt as dt_util
from tests.common import ( from tests.common import (
get_test_home_assistant, MockPlatform, MockModule, fire_time_changed, get_test_home_assistant, MockPlatform, MockModule, fire_time_changed,
mock_coro) mock_coro, async_fire_time_changed)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "test_domain" DOMAIN = "test_domain"
@ -533,3 +534,47 @@ def test_extract_from_service_available_device(hass):
assert ['test_domain.test_3'] == \ assert ['test_domain.test_3'] == \
sorted(ent.entity_id for ent in sorted(ent.entity_id for ent in
component.async_extract_from_service(call_2)) 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