Skip to content

Commit

Permalink
EntityComponent to retry platforms that are not ready yet (#8209)
Browse files Browse the repository at this point in the history
* Add PlatformNotReady Exception

* lint

* Remove cap, adjust algorithm
  • Loading branch information
balloob authored Jun 26, 2017
1 parent f02d169 commit d73b695
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 5 deletions.
2 changes: 1 addition & 1 deletion homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 16 additions & 3 deletions homeassistant/helpers/entity_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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."
Expand Down
47 changes: 46 additions & 1 deletion tests/helpers/test_entity_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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"
Expand Down Expand Up @@ -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

0 comments on commit d73b695

Please sign in to comment.