mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 10:47:10 +00:00
Allow config entry setup to raise not ready (#17135)
This commit is contained in:
parent
c9976718d4
commit
0cfbb9ce91
@ -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()
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user