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).
-
-
-
## 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: