Skip to content

fix(fcm): recover connection status after settings reload (#183)#184

Merged
jleinenbach merged 1 commit into
BSkando:1.7from
jleinenbach:1.7.2
Jun 19, 2026
Merged

fix(fcm): recover connection status after settings reload (#183)#184
jleinenbach merged 1 commit into
BSkando:1.7from
jleinenbach:1.7.2

Conversation

@jleinenbach

Copy link
Copy Markdown
Collaborator

Summary

Fixes a regression where modifying the integration's settings (which triggers a config-entry reload) left the connection status stuck on Disconnected until Home Assistant was fully restarted. Reloading the integration alone did not recover it.

Fixes #183

Root cause

The FCM receiver is tracked in two places:

  • a per-entry store fcm_receivers (the source of truth that api.is_push_ready() resolves through the domain provider), and
  • a legacy singleton alias fcm_receiver (read by diagnostics and a few fallback paths).

On a reload, the shared FCM receiver is released when the refcount drops to zero, which empties the per-entry fcm_receivers dict. However, _sync_legacy_fcm_receiver_alias only cleared the legacy singleton when it held the wrong type, so a healthy singleton survived the release. The result was a desynchronized state:

  • diagnostics read the surviving singleton and reported the receiver as healthy (matching ref_count: 0 + start_count: 5 in the issue's diagnostic dump),
  • is_push_ready() resolved the now-empty per-entry dict and returned False,
  • the connectivity sensor (driven by is_push_ready()) showed Disconnected, and
  • the coordinator's poll-cycle recovery kept re-setting the degraded branch with the reason "Push transport not ready; continuing with cached data" — verbatim the fcm_status.reason from the report.

Because the stale singleton was reused on re-acquire, the per-entry store was never repopulated and the provider was never re-registered, so the condition persisted across reloads and only a full HA restart cleared it.

Fix

_sync_legacy_fcm_receiver_alias now treats the per-entry fcm_receivers dict as the single source of truth whenever it exists: if the dict is present but empty (post-release), the legacy singleton alias is cleared as well. This forces the next re-acquire down the fresh-receiver path, which rebuilds both the per-entry store and the provider registration, so is_push_ready() recovers without an HA restart and diagnostics stay consistent.

The legacy migration path (fcm_receivers entirely absent) is unchanged and remains guarded by a test.

Tests

  • New regression tests reproduce the post-reload store desync against the real is_push_ready() / domain-provider / diagnostics paths, and assert recovery after the fix.
  • A full acquire → release@refcount0 → re-acquire cycle test on the real receiver paths proves the per-entry store is repopulated.
  • A coordinator status test confirms fcm_status heals in the next poll cycle once readiness is correct.
  • Mutation checks confirm the tests fail when the fix is reverted.
  • ruff, ruff format, mypy --strict clean; full suite green.

) (#1120)

Issue #183: changing settings (which reloads the config entry) left the
FCM connection status stuck on "Disconnected" until a full Home Assistant
restart. The submitted diagnostics were self-contradictory: the receiver
snapshot was healthy/STARTED while coordinator.fcm_status was degraded
("Push transport not ready; continuing with cached data").

Root cause (empirically reproduced, hypothesis H2): two receiver stores
diverge across a refcount->0 release. _pop_fcm_receiver empties the
per-entry dict fcm_receivers (read by is_push_ready via the provider
getter), but _sync_legacy_fcm_receiver_alias only dropped the legacy
fcm_receiver singleton (read by diagnostics) when it held the wrong type.
A healthy, right-type receiver therefore survived as the singleton while
the per-entry store was empty: is_push_ready() resolved nothing and
returned False (rendered "Disconnected"), while diagnostics still reported
the receiver as healthy/connected.

Fix: treat fcm_receivers as the single source of truth whenever it exists.
When the per-entry dict holds no valid receiver (e.g. after the release
path empties it on reload), drop the legacy alias as well. The legacy
migration path (dict absent entirely, used by _get_fcm_receivers) is
preserved unchanged. This also makes the reload re-acquire reliably
repopulate the store: with no stale singleton to reuse, _async_acquire_
shared_fcm falls through to creating a fresh receiver that re-registers
the per-entry store and providers, so is_push_ready recovers without a
restart.

Tests:
- test_issue_183_reload_fcm_desync.py: alias cleared when dict empties,
  readiness and diagnostics stay consistent, runtime re-sync heals, and
  the absent-dict migration path is preserved.
- test_fcm_receiver.test_reload_reacquire_repopulates_receiver_store:
  full acquire -> release@0 -> re-acquire cycle on the real paths.
- test_coordinator_status: status stays non-CONNECTED while readiness is
  false and recovers on the next cycle once it is true.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@jleinenbach jleinenbach merged commit 8094a0c into BSkando:1.7 Jun 19, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant