From e8ebc9ff1922cf58b17819e5596543a63070c7b0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 19 Feb 2025 10:01:48 +0100 Subject: [PATCH] Document changed config entry state transitions (#2565) * Document changed config entry state transitions * Fix links * Address review comments * Address review comments * Address review comments * Fix line breaks in lists * Address review comments * Bump date * Close br tags * Bump date * Simplify example --- blog/2025-02-19-new-config-entry-states.md | 71 +++++++++++++ docs/config_entries_index.md | 113 +++------------------ 2 files changed, 84 insertions(+), 100 deletions(-) create mode 100644 blog/2025-02-19-new-config-entry-states.md diff --git a/blog/2025-02-19-new-config-entry-states.md b/blog/2025-02-19-new-config-entry-states.md new file mode 100644 index 00000000..13a88387 --- /dev/null +++ b/blog/2025-02-19-new-config-entry-states.md @@ -0,0 +1,71 @@ +--- +author: Erik Montnemery +authorURL: https://github.com/emontnemery +title: "Changed config entry state transitions" +--- + +Config entry state transitions when unloading and removing entries has been modified: + +- A new state `ConfigEntryState.UNLOAD_IN_PROGRESS` is added, which is set before calling the integration's `async_unload_entry`
+ Rationale: + - Make it easier to write cleanup code which should run after the last config entry has been unloaded + - Improve debugging of issues related to reload and unload of config entries + +- The config entry state is set to `ConfigEntryState.FAILED_UNLOAD` when the integration's `async_unload_entry` returns False
+ Rationale: + - If `async_unload_entry` returns `False`, we can't assume the integration is still loaded, most likely it has partially unloaded itself, especially considering this is the pattern we recommend: + ```py + async def async_unload_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool: + """Unload a config entry.""" + # async_unload_platforms returns False if at least one platform did not unload + if (unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS)): + entry.runtime_data.listener() + # Finish cleanup not related to platforms + return unload_ok + ``` + +- The config entry is removed from `hass.config_entries` before calling the integration's `async_remove_entry` is called
+ Rationale: + - Make it easier to write cleanup code which should run after the last config entry has been removed + +Custom integration authors need to review and update their integrations' `async_unload_entry` and `async_remove_entry` if needed. +The most common pattern which requires an update is this: + +```python +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state is ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # The last config entry is being unloaded, release shared resources, unregister services etc. + ... +``` + +This can now be simplified, if the custom integration's minimum Home Assistant version is set to 2025.3.0: +```python +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if not hass.config_entries.async_loaded_entries(DOMAIN): + # The last config entry is being unloaded, release shared resources, unregister services etc. + ... +``` + + +If the custom integration needs to be backwards compatible with previous releases of Home Assistant Core: +```python +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + other_loaded_entries = [ + _entry + for _entry in hass.config_entries.async_loaded_entries(DOMAIN) + if _entry.entry_id != entry.entry_id + ] + if not other_loaded_entries: + # The last config entry is being unloaded, release shared resources, unregister services etc. + ... +``` + +Check the [config entry documentation](/docs/config_entries_index), and the [home assistant core PR #138522](https://github.com/home-assistant/core/pull/138522) for additional background. diff --git a/docs/config_entries_index.md b/docs/config_entries_index.md index 5504caea..3ac6b444 100644 --- a/docs/config_entries_index.md +++ b/docs/config_entries_index.md @@ -17,121 +17,33 @@ Similar to config entries, subentries can optionally support a reconfigure step. | State | Description | | ----- | ----------- | | not loaded | The config entry has not been loaded. This is the initial state when a config entry is created or when Home Assistant is restarted. | +| setup in progress | An intermediate state while attempting to load the config entry. | | loaded | The config entry has been loaded. | | setup error | An error occurred when trying to set up the config entry. | -| setup retry | A dependency of the config entry was not ready yet. Home Assistant will automatically retry loading this config entry in the future. Time between attempts will automatically increase. -| migration error | The config entry had to be migrated to a newer version, but the migration failed. -| failed unload | The config entry was attempted to be unloaded, but this was either not supported or it raised an exception. +| setup retry | A dependency of the config entry was not ready yet. Home Assistant will automatically retry loading this config entry in the future. Time between attempts will automatically increase. | +| migration error | The config entry had to be migrated to a newer version, but the migration failed. | +| unload in progress | An intermediate state while attempting to unload the config entry. | +| failed unload | The config entry was attempted to be unloaded, but this was either not supported or it raised an exception. | More information about surfacing errors and requesting a retry are in [Handling Setup Failures](integration_setup_failures.md#integrations-using-async_setup_entry). - - -G - - -not loaded - -not loaded - - -loaded - -loaded - - -not loaded->loaded - - - - -setup error - -setup error - - -not loaded->setup error - - - - -setup retry - -setup retry - - -not loaded->setup retry - - - - -migration error - -migration error - - -not loaded->migration error - - - - -loaded->not loaded - - - - -failed unload - -failed unload - - -loaded->failed unload - - - - -setup error->not loaded - - - - -setup retry->not loaded - - - - - - - ## Setting up an entry -During startup, Home Assistant first calls the [normal component setup](/creating_component_index.md), -and then call the method `async_setup_entry(hass, entry)` for each entry. If a new Config Entry is +During startup, Home Assistant first calls the [normal integration setup](/creating_component_index.md), +and then calls the method `async_setup_entry(hass, entry)` for each entry. If a new Config Entry is created at runtime, Home Assistant will also call `async_setup_entry(hass, entry)` ([example](https://github.com/home-assistant/core/blob/f18ddb628c3574bc82e21563d9ba901bd75bc8b5/homeassistant/components/hassio/__init__.py#L522)). ### For platforms -If a component includes platforms, it will need to forward the Config Entry to the platform. This can +If an integration includes platforms, it will need to forward the Config Entry set up to the platform. This can be done by calling the forward function on the config entry manager ([example](https://github.com/home-assistant/core/blob/f18ddb628c3574bc82e21563d9ba901bd75bc8b5/homeassistant/components/hassio/__init__.py#L529)): ```python await hass.config_entries.async_forward_entry_setups(config_entry, ["light", "sensor", "switch"]) ``` -For a platform to support config entries, it will need to add a setup entry method ([example](https://github.com/home-assistant/core/blob/f18ddb628c3574bc82e21563d9ba901bd75bc8b5/homeassistant/components/hassio/__init__.py#L522)): +For a platform to support config entries, it will need to add a setup entry function ([example](https://github.com/home-assistant/core/blob/f18ddb628c3574bc82e21563d9ba901bd75bc8b5/homeassistant/components/hassio/__init__.py#L522)): ```python async def async_setup_entry(hass, config_entry, async_add_entities): @@ -140,19 +52,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ## Unloading entries -Components can optionally support unloading a config entry. When unloading an entry, the component needs to clean up all entities, unsubscribe any event listener and close all connections. To implement this, add `async_unload_entry(hass, entry)` to your component ([example](https://github.com/home-assistant/core/blob/f18ddb628c3574bc82e21563d9ba901bd75bc8b5/homeassistant/components/hassio/__init__.py#L534)). +Integrations can optionally support unloading a config entry. When unloading an entry, the integration needs to clean up all entities, unsubscribe any event listener and close all connections. To implement this, add `async_unload_entry(hass, entry)` to your integration ([example](https://github.com/home-assistant/core/blob/f18ddb628c3574bc82e21563d9ba901bd75bc8b5/homeassistant/components/hassio/__init__.py#L534)). The state of the config entry is set to `ConfigEntryState.UNLOAD_IN_PROGRESS` before `async_unload_entry` is called. For each platform that you forwarded the config entry to, you will need to forward the unloading too. ```python -await self.hass.config_entries.async_forward_entry_unload(self.config_entry, "light") +async def async_unload_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool: + """Unload a config entry.""" ``` If you need to clean up resources used by an entity in a platform, have the entity implement the [`async_will_remove_from_hass`](core/entity.md#async_will_remove_from_hass) method. ## Removal of entries -If a component needs to clean up code when an entry is removed, it can define a removal method: +If an integration needs to clean up code when an entry is removed, it can define a removal function `async_remove_entry`. The config entry is deleted from `hass.config_entries` before `async_remove_entry` is called. ```python async def async_remove_entry(hass, entry) -> None: