Skip to content

Tolerate unloading a never-loaded config entry in EntityComponent - #176594

Open
honzup wants to merge 2 commits into
home-assistant:devfrom
honzup:entity-component-tolerant-unload
Open

Tolerate unloading a never-loaded config entry in EntityComponent#176594
honzup wants to merge 2 commits into
home-assistant:devfrom
honzup:entity-component-tolerant-unload

Conversation

@honzup

@honzup honzup commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Breaking change

Proposed change

EntityComponent.async_unload_entry raises ValueError("Config entry was never loaded!") when the entry is not in self._platforms. In practice this situation arises when a forwarded platform's setup silently failed earlier: _async_forward_entry_setups_locked gathers per-platform setup tasks and discards their results, so an integration's entry can be LOADED while one of its platforms (e.g. notify) was never registered with the corresponding EntityComponent.

The next unload of that entry then hits the ValueError, which config_entries converts into a failed unload — wedging the whole entry into the non-recoverable FAILED_UNLOAD state. The user's only way out is a full Home Assistant restart, for a situation where there was nothing to unload in the first place.

Real-world reports of exactly this chain:

This change makes async_unload_entry log a warning (naming the entry and domain, and pointing at a possible earlier platform setup failure) and return True when the entry was never registered — there is no EntityPlatform state to reset, so treating it as already-unloaded is accurate and lets the entry unload/reload normally instead of requiring a restart. The async_setup_entry side (including its "has already been setup" guard) is unchanged.

Test evidence: pytest tests/helpers/test_entity_component.py → 30 passed (the previous test_unload_entry_fails_if_never_loaded is replaced by test_unload_entry_tolerates_never_loaded, asserting True plus the warning in caplog; the new test fails with the old ValueError when the helper is reverted). Sanity run of tests/components/tibber/ on this branch: 92 passed.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

When a forwarded platform setup fails, config_entries discards the
forwarded setup result and the owning entry stays LOADED while the
platform is untracked in EntityComponent. The next unload then raised
ValueError("Config entry was never loaded!"), which config_entries
converts into a failed unload, wedging the entry into the
non-recoverable FAILED_UNLOAD state until restart.

There is nothing to unload in this situation, so log a warning naming
the entry and platform domain and report success instead of raising.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DERobfZZh3npFRbwpdxzz2
Copilot AI review requested due to automatic review settings July 16, 2026 08:29
@honzup
honzup requested a review from a team as a code owner July 16, 2026 08:29

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @honzup

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant home-assistant Bot added bugfix cla-needed core small-pr PRs with less than 30 lines. labels Jul 16, 2026
@home-assistant

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant
home-assistant Bot marked this pull request as draft July 16, 2026 08:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents failed unload states when an entity platform was never loaded.

Changes:

  • Treat missing platforms as already unloaded and log a warning.
  • Update unit coverage for the new behavior.

Outstanding: Restore the complete PR template and address the CLA request.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
homeassistant/helpers/entity_component.py Tolerates unloading absent platforms.
tests/helpers/test_entity_component.py Verifies success and warning logging.


if (platform := self._platforms.pop(key, None)) is None:
raise ValueError("Config entry was never loaded!")
self.logger.warning(
@honzup
honzup marked this pull request as ready for review July 16, 2026 08:54
Copilot AI review requested due to automatic review settings July 16, 2026 08:54
@home-assistant
home-assistant Bot dismissed their stale review July 16, 2026 08:54

Stale

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@nick-tgcs nick-tgcs left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traced this through EntityComponent. async_setup_entry always registers the platform in self._platforms before setup, so _platforms.pop(key) returning None in async_unload_entry means setup for this component and entry never ran, which is exactly the discarded-failure case described. In that state there is no EntityPlatform and no entities registered through this component, so async_reset() would have nothing to do and returning True is accurate rather than lenient. Downgrading to a warning avoids the ValueError being converted into the non-recoverable FAILED_UNLOAD that forces a restart, and the message names the entry, domain, and likely cause. The replacement test asserts both the True return and the warning text, a clear improvement over the previous stub. One note for maintainers: this handles the symptom, while the root cause is that _async_forward_entry_setups_locked discards per-platform setup results, so the entry reports LOADED while a platform silently failed. Is that tracked separately? Either way this change looks correct and low risk. LGTM.

@honzup

honzup commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Is that tracked separately?

Thanks for tracing it through so thoroughly.

The root cause is described in #176593 (the discarded forwarded-setup results are what create the phantom platform in the first place), but it isn't currently tracked as its own issue - this PR deliberately fixes only the unload side, since the missing registration on its own is harmless: it's the unload raising on it that turns a stale internal record into a wedged entry that only a restart can recover.

I'll file a separate issue for the forward-setup side so it's tracked properly. Fixing that one is more architectural - a silently failed forwarded platform arguably shouldn't leave the entry reporting LOADED at all, but whether the right behaviour is marking the entry setup_error, scheduling a platform retry, or raising a repair issue deserves its own discussion rather than being bundled in here.

@honzup

honzup commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Filed the forward-setup root cause as #176652.

@honzup

honzup commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@joostlek thanks for merging #176595.

While tracing that one I found the same wedge a level down, which is what this PR fixes. EntityComponent.async_unload_entry raises ValueError("Config entry was never loaded!") when the entry isn't in self._platforms. config_entries turns that into a failed unload, and the entry lands in FAILED_UNLOAD - recoverable only by restarting Home Assistant, for a platform that had nothing to unload.

An entry reaches that state because _async_forward_entry_setups_locked gathers the per-platform setup tasks and discards their results, so it can stay LOADED with a platform that never registered.

It's the general case behind the Tibber symptom. The same failure is reported in #166228 (tibber notify), #159949 (esphome assist_satellite) and #90180 (tibber sensor). All three are closed, though none by a change to this code path: #166228 was resolved by the pyTibber 0.37.0 bump and the set_access_token fix, which removed a trigger rather than the wedge itself, and the other two were auto-closed as stale by the triage bot.

The change is 21 lines - the raise becomes a warning naming the entry and domain, and the method returns True, since there's no EntityPlatform state to reset.

It's been open since 16 July without a core review. It's in helpers/, so there's no code owner to ping. If this isn't yours to look at, a pointer to who would be is just as useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix cla-signed core small-pr PRs with less than 30 lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Silently failed forwarded platform setup wedges config entries into non-recoverable FAILED_UNLOAD on next unload

3 participants