Allow config entry setup to raise not ready (#17135)

This commit is contained in:
Paulus Schoutsen 2018-10-04 15:53:50 +02:00 committed by Pascal Vizeli
parent c9976718d4
commit 0cfbb9ce91
3 changed files with 100 additions and 14 deletions

View File

@ -115,14 +115,13 @@ should follow the same return values as a normal step.
If the result of the step is to show a form, the user will be able to continue If the result of the step is to show a form, the user will be able to continue
the flow from the config panel. the flow from the config panel.
""" """
import logging import logging
import uuid import uuid
from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady
from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.setup import async_setup_component, async_process_deps_reqs
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
@ -161,9 +160,15 @@ PATH_CONFIG = '.config_entries.json'
SAVE_DELAY = 1 SAVE_DELAY = 1
# The config entry has been set up successfully
ENTRY_STATE_LOADED = 'loaded' ENTRY_STATE_LOADED = 'loaded'
# There was an error while trying to set up this config entry
ENTRY_STATE_SETUP_ERROR = 'setup_error' ENTRY_STATE_SETUP_ERROR = 'setup_error'
# The config entry was not ready to be set up yet, but might be later
ENTRY_STATE_SETUP_RETRY = 'setup_retry'
# The config entry has not been loaded
ENTRY_STATE_NOT_LOADED = 'not_loaded' ENTRY_STATE_NOT_LOADED = 'not_loaded'
# An error occurred when trying to unload the entry
ENTRY_STATE_FAILED_UNLOAD = 'failed_unload' ENTRY_STATE_FAILED_UNLOAD = 'failed_unload'
DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery'
@ -186,7 +191,8 @@ class ConfigEntry:
"""Hold a configuration entry.""" """Hold a configuration entry."""
__slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source', __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source',
'connection_class', 'state') 'connection_class', 'state', '_setup_lock',
'_async_cancel_retry_setup')
def __init__(self, version: str, domain: str, title: str, data: dict, def __init__(self, version: str, domain: str, title: str, data: dict,
source: str, connection_class: str, source: str, connection_class: str,
@ -217,8 +223,11 @@ class ConfigEntry:
# State of the entry (LOADED, NOT_LOADED) # State of the entry (LOADED, NOT_LOADED)
self.state = state self.state = state
# Function to cancel a scheduled retry
self._async_cancel_retry_setup = None
async def async_setup( async def async_setup(
self, hass: HomeAssistant, *, component=None) -> None: self, hass: HomeAssistant, *, component=None, tries=0) -> None:
"""Set up an entry.""" """Set up an entry."""
if component is None: if component is None:
component = getattr(hass.components, self.domain) component = getattr(hass.components, self.domain)
@ -230,6 +239,22 @@ class ConfigEntry:
_LOGGER.error('%s.async_config_entry did not return boolean', _LOGGER.error('%s.async_config_entry did not return boolean',
component.DOMAIN) component.DOMAIN)
result = False result = False
except ConfigEntryNotReady:
self.state = ENTRY_STATE_SETUP_RETRY
wait_time = 2**min(tries, 4) * 5
tries += 1
_LOGGER.warning(
'Config entry for %s not ready yet. Retrying in %d seconds.',
self.domain, wait_time)
async def setup_again(now):
"""Run setup again."""
self._async_cancel_retry_setup = None
await self.async_setup(hass, component=component, tries=tries)
self._async_cancel_retry_setup = \
hass.helpers.event.async_call_later(wait_time, setup_again)
return
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up entry %s for %s', _LOGGER.exception('Error setting up entry %s for %s',
self.title, component.DOMAIN) self.title, component.DOMAIN)
@ -252,6 +277,15 @@ class ConfigEntry:
if component is None: if component is None:
component = getattr(hass.components, self.domain) component = getattr(hass.components, self.domain)
if component.DOMAIN == self.domain:
if self._async_cancel_retry_setup is not None:
self._async_cancel_retry_setup()
self.state = ENTRY_STATE_NOT_LOADED
return True
if self.state != ENTRY_STATE_LOADED:
return True
supports_unload = hasattr(component, 'async_unload_entry') supports_unload = hasattr(component, 'async_unload_entry')
if not supports_unload: if not supports_unload:
@ -260,15 +294,17 @@ class ConfigEntry:
try: try:
result = await component.async_unload_entry(hass, self) result = await component.async_unload_entry(hass, self)
if not isinstance(result, bool): assert isinstance(result, bool)
_LOGGER.error('%s.async_unload_entry did not return boolean',
component.DOMAIN) # Only adjust state if we unloaded the component
result = False if result and component.DOMAIN == self.domain:
self.state = ENTRY_STATE_NOT_LOADED
return result return result
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error unloading entry %s for %s', _LOGGER.exception('Error unloading entry %s for %s',
self.title, component.DOMAIN) self.title, component.DOMAIN)
if component.DOMAIN == self.domain:
self.state = ENTRY_STATE_FAILED_UNLOAD self.state = ENTRY_STATE_FAILED_UNLOAD
return False return False
@ -342,10 +378,7 @@ class ConfigEntries:
entry = self._entries.pop(found) entry = self._entries.pop(found)
self._async_schedule_save() self._async_schedule_save()
if entry.state == ENTRY_STATE_LOADED:
unloaded = await entry.async_unload(self.hass) unloaded = await entry.async_unload(self.hass)
else:
unloaded = True
device_registry = await \ device_registry = await \
self.hass.helpers.device_registry.async_get_registry() self.hass.helpers.device_registry.async_get_registry()

View File

@ -35,6 +35,12 @@ class PlatformNotReady(HomeAssistantError):
pass pass
class ConfigEntryNotReady(HomeAssistantError):
"""Error to indicate that config entry is not ready."""
pass
class InvalidStateError(HomeAssistantError): class InvalidStateError(HomeAssistantError):
"""When an invalid state is encountered.""" """When an invalid state is encountered."""

View File

@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest import pytest
from homeassistant import config_entries, loader, data_entry_flow from homeassistant import config_entries, loader, data_entry_flow
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt from homeassistant.util import dt
@ -126,7 +127,7 @@ def test_remove_entry_if_not_loaded(hass, manager):
assert [item.entry_id for item in manager.async_entries()] == \ assert [item.entry_id for item in manager.async_entries()] == \
['test1', 'test3'] ['test1', 'test3']
assert len(mock_unload_entry.mock_calls) == 0 assert len(mock_unload_entry.mock_calls) == 1
@asyncio.coroutine @asyncio.coroutine
@ -367,3 +368,49 @@ async def test_updating_entry_data(manager):
assert entry.data == { assert entry.data == {
'second': True 'second': True
} }
async def test_setup_raise_not_ready(hass, caplog):
"""Test a setup raising not ready."""
entry = MockConfigEntry(domain='test')
mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady)
loader.set_component(
hass, 'test', MockModule('test', async_setup_entry=mock_setup_entry))
with patch('homeassistant.helpers.event.async_call_later') as mock_call:
await entry.async_setup(hass)
assert len(mock_call.mock_calls) == 1
assert 'Config entry for test not ready yet' in caplog.text
p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1]
assert p_hass is hass
assert p_wait_time == 5
assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY
mock_setup_entry.side_effect = None
mock_setup_entry.return_value = mock_coro(True)
await p_setup(None)
assert entry.state == config_entries.ENTRY_STATE_LOADED
async def test_setup_retrying_during_unload(hass):
"""Test if we unload an entry that is in retry mode."""
entry = MockConfigEntry(domain='test')
mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady)
loader.set_component(
hass, 'test', MockModule('test', async_setup_entry=mock_setup_entry))
with patch('homeassistant.helpers.event.async_call_later') as mock_call:
await entry.async_setup(hass)
assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY
assert len(mock_call.return_value.mock_calls) == 0
await entry.async_unload(hass)
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
assert len(mock_call.return_value.mock_calls) == 1