From c71503501662fc9de86937748fecf6ad780432cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 25 Nov 2022 11:33:03 +0100 Subject: [PATCH] Add support for raising ConfigEntryError (#82689) --- .../components/fjaraskupan/__init__.py | 2 + homeassistant/components/hassio/__init__.py | 5 +- homeassistant/config_entries.py | 17 +++- homeassistant/exceptions.py | 4 + homeassistant/helpers/update_coordinator.py | 24 ++++- tests/test_config_entries.py | 91 +++++++++++++++++++ 6 files changed, 139 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 4a6c40fbeae..66de2ddb8a6 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -70,12 +70,14 @@ class Coordinator(DataUpdateCoordinator[State]): log_failures: bool = True, raise_on_auth_failed: bool = False, scheduled: bool = False, + raise_on_entry_error: bool = False, ) -> None: self._refresh_was_scheduled = scheduled await super()._async_refresh( log_failures=log_failures, raise_on_auth_failed=raise_on_auth_failed, scheduled=scheduled, + raise_on_entry_error=raise_on_entry_error, ) async def _async_update_data(self) -> State: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 598871f57d5..467d1e878b9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -866,6 +866,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): log_failures: bool = True, raise_on_auth_failed: bool = False, scheduled: bool = False, + raise_on_entry_error: bool = False, ) -> None: """Refresh data.""" if not scheduled: @@ -874,4 +875,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): await self.hassio.refresh_updates() except HassioAPIError as err: _LOGGER.warning("Error on Supervisor API: %s", err) - await super()._async_refresh(log_failures, raise_on_auth_failed, scheduled) + await super()._async_refresh( + log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error + ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4956955cd21..e619c3e73f2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -19,7 +19,12 @@ from .components import persistent_notification from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform from .core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback from .data_entry_flow import FlowResult -from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError +from .exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, + HomeAssistantError, +) from .helpers import device_registry, entity_registry, storage from .helpers.dispatcher import async_dispatcher_send from .helpers.event import async_call_later @@ -371,6 +376,16 @@ class ConfigEntry: "%s.async_setup_entry did not return boolean", integration.domain ) result = False + except ConfigEntryError as ex: + error_reason = str(ex) or "Unknown fatal config entry error" + _LOGGER.exception( + "Error setting up entry %s for %s: %s", + self.title, + self.domain, + error_reason, + ) + await self._async_process_on_unload() + result = False except ConfigEntryAuthFailed as ex: message = str(ex) auth_base_message = "could not authenticate" diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 4f2599aa2da..77ac2938cf2 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -114,6 +114,10 @@ class PlatformNotReady(IntegrationError): """Error to indicate that platform is not ready.""" +class ConfigEntryError(IntegrationError): + """Error to indicate that config entry setup has failed.""" + + class ConfigEntryNotReady(IntegrationError): """Error to indicate that config entry is not ready.""" diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f25691ae504..205a7848613 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -15,7 +15,11 @@ import requests from homeassistant import config_entries from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) from homeassistant.util.dt import utcnow from . import entity, event @@ -183,7 +187,9 @@ class DataUpdateCoordinator(Generic[_T]): fails. Additionally logging is handled by config entry setup to ensure that multiple retries do not cause log spam. """ - await self._async_refresh(log_failures=False, raise_on_auth_failed=True) + await self._async_refresh( + log_failures=False, raise_on_auth_failed=True, raise_on_entry_error=True + ) if self.last_update_success: return ex = ConfigEntryNotReady() @@ -199,6 +205,7 @@ class DataUpdateCoordinator(Generic[_T]): log_failures: bool = True, raise_on_auth_failed: bool = False, scheduled: bool = False, + raise_on_entry_error: bool = False, ) -> None: """Refresh data.""" if self._unsub_refresh: @@ -250,6 +257,19 @@ class DataUpdateCoordinator(Generic[_T]): self.logger.error("Error fetching %s data: %s", self.name, err) self.last_update_success = False + except ConfigEntryError as err: + self.last_exception = err + if self.last_update_success: + if log_failures: + self.logger.error( + "Config entry setup failed while fetching %s data: %s", + self.name, + err, + ) + self.last_update_success = False + if raise_on_entry_error: + raise + except ConfigEntryAuthFailed as err: auth_failed = True self.last_exception = err diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 68ece465226..b1e3fd760d5 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -20,6 +20,7 @@ from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType from homeassistant.exceptions import ( ConfigEntryAuthFailed, + ConfigEntryError, ConfigEntryNotReady, HomeAssistantError, ) @@ -2866,6 +2867,96 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): assert entry.state is config_entries.ConfigEntryState.LOADED +async def test_setup_raise_entry_error(hass, caplog): + """Test a setup raising ConfigEntryError.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock( + side_effect=ConfigEntryError("Incompatible firmware version") + ) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert ( + "Error setting up entry test_title for test: Incompatible firmware version" + in caplog.text + ) + + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert entry.reason == "Incompatible firmware version" + + +async def test_setup_raise_entry_error_from_first_coordinator_update(hass, caplog): + """Test async_config_entry_first_refresh raises ConfigEntryError.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" + + async def _async_update_data(): + raise ConfigEntryError("Incompatible firmware version") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_config_entry_first_refresh() + return True + + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert ( + "Error setting up entry test_title for test: Incompatible firmware version" + in caplog.text + ) + + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert entry.reason == "Incompatible firmware version" + + +async def test_setup_not_raise_entry_error_from_future_coordinator_update(hass, caplog): + """Test a coordinator not raises ConfigEntryError in the future.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" + + async def _async_update_data(): + raise ConfigEntryError("Incompatible firmware version") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_refresh() + return True + + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert ( + "Config entry setup failed while fetching any data: Incompatible firmware version" + in caplog.text + ) + + assert entry.state is config_entries.ConfigEntryState.LOADED + + async def test_setup_raise_auth_failed(hass, caplog): """Test a setup raising ConfigEntryAuthFailed.""" entry = MockConfigEntry(title="test_title", domain="test")