Skip to content

fix: FCM reconnect storm hardening + phantom doorbell ring on restart (v0.16.4-beta.2) - #14

Merged
bvis merged 2 commits into
mainfrom
fix/fcm-reconnect-storm-hardening
Jun 11, 2026
Merged

bvis merged 2 commits into
mainfrom
fix/fcm-reconnect-storm-hardening

Conversation

@bvis

@bvis bvis commented Jun 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Two independent reliability fixes bundled into the 0.16.4-beta.2 release.

1. Phantom doorbell ring on HA restart

After an HA restart, the doorbell automation fired "someone is at the door" with no FCM message involved (confirmed in logs: no Doorbell notification line, the coordinator grace-period and dedup never engaged).

Root cause: the event entities (doorbell ring, door opened, camera on) inherited available from the device connection_state. A transient intercom disconnect — or the brief window during an HA restart — flapped them to unavailable and back. On recovery the EventEntity restores its last event (ring) and HA fires state triggers, so automations watching the doorbell ran. Verified against the live instance: event.videoportero_badalona went ring → unavailable (22:18:02) → ring (22:23:03), and the automation ran at 22:23:03 on that recovery edge.

Fix: event availability is decoupled from connectivity (events are momentary historical markers, so available is constant True). Real events still fire via _trigger_event.

2. FCM reconnect storm hardening (closes #12)

  • Rate-limited exception logging: a logging.Filter on the firebase_messaging.fcmpushclient logger strips exc_info after 3 tracebacks per 5-minute window. Filters run before formatting, so the quadratic Python 3.14 traceback formatting never runs on the ever-growing exception chain even while the upstream loop spins hot.
  • Bounded failure + supervised restart: abort_on_sequential_error_count goes from None back to 3, and ensure_running() defers restarts with a doubling backoff (5 → 15 min cap) that resets once the listener is healthy. Converts the crash loop into "push down for a few minutes".

Tests

TDD throughout. 12 new tests (rate-limit filter, backoff escalation/reset, bounded-abort config, event availability decoupling). make pre-push green: lint, format, mypy, vulture, 150 tests on Python 3.12 + 3.13.

Closes #12

bvis added 2 commits June 11, 2026 23:20
…4-beta.2)

A poisoned StreamReader in firebase_messaging's _listen loop re-raises
the same exception object every iteration, growing its traceback while
logging.exception formats it inside the HA event loop - quadratic on
Python 3.14 and fatal (Supervisor watchdog kill -> crash loop).

- Rate-limit filter on the firebase_messaging.fcmpushclient logger:
  strips exc_info after 3 tracebacks per 5-minute window, keeping
  one-line messages. Filters run before formatting, so this defuses
  the CPU bomb even while the upstream loop spins.
- Bounded abort (abort_on_sequential_error_count=3) instead of
  unbounded retries; the watchdog restarts the client with a delayed
  doubling backoff (5 -> 15 min cap) that resets when healthy.

Closes #12
Event entities (doorbell ring, door opened, camera on) inherited
availability from the device connection_state. A transient intercom
disconnect - or the brief window during an HA restart - flapped them
to unavailable and back; on recovery the EventEntity restores its last
event (e.g. ring) and HA fires state triggers, producing a phantom
doorbell ring with no FCM message involved (confirmed in logs: no
'Doorbell notification' line, grace-period and dedup never engaged).

Events are momentary historical markers, so their availability is now
constant True instead of tracking connectivity. Real events still fire
via _trigger_event.
@bvis bvis changed the title fix(notification): harden FCM client against reconnect storms (v0.16.4-beta.2) fix: FCM reconnect storm hardening + phantom doorbell ring on restart (v0.16.4-beta.2) Jun 11, 2026
@bvis
bvis merged commit 15f7d58 into main Jun 11, 2026
10 checks passed
@bvis
bvis deleted the fix/fcm-reconnect-storm-hardening branch June 11, 2026 21:38
anthonws added a commit to anthonws/fermax-blue-hass that referenced this pull request Jul 4, 2026
… (v0.13.0)

Addresses 46 findings from a multi-agent performance/reliability/security
audit. Highlights below; each change is annotated with its audit id in-code.

## Performance — stream startup latency (the headline)
- Video now goes live independently of the ~10s Fermax "pickup": the video
  relay + frame grabber + is_active are started right after the video consumer,
  and the audio relay is pre-created from a silent switchable source so its
  m-line is negotiated up front. When pickup completes, the real intercom audio
  is swapped into that relay — no WebRTC renegotiation. Cuts perceived
  first-frame latency from ~13s to ~3-4s (bvis#2, bvis#6).
- Frame grabber: decode+JPEG-encode moved off the event loop via to_thread,
  encoded once (overlay burned in) instead of twice, and throttled to ~5fps
  when not recording since the WebRTC path consumes raw relay frames (bvis#3,bvis#7,bvis#8).
- WebRTC offer handler polls the relay at 0.1s (was 0.5s) and caps ICE
  gathering at 4s (was 10s) (bvis#18, bvis#19).

## Reliability
- CRITICAL: re-register a rotated FCM token with Fermax. Previously a token
  rotation silently killed the doorbell until a full reload (bvis#1).
- Notification de-duplication now persists persistent_ids across restarts, so
  FCM re-deliveries are recognised by id; the blunt 10s time-grace is used only
  on first run — a genuine ring right after restart is no longer dropped (bvis#9,bvis#24).
- Serialise stream start/stop with a lock + session-identity guards so
  overlapping notifications can't orphan a half-started session; the grabber's
  end-callback no longer clobbers a newer session; stop() is idempotent and the
  session is fully torn down on natural end (bvis#11, bvis#26, bvis#27, bvis#45).
- FCM decrypt: escalate to ERROR after 5 consecutive failures (systemic
  outage) instead of masking it as healthy (bvis#10).
- Socket.IO connect() is bounded by a 15s timeout; startup HTTP calls carry an
  explicit per-request timeout (bvis#13, bvis#30).
- Partial-setup teardown: a later pairing failing no longer leaks earlier
  coordinators' FCM listeners / API client (bvis#14).
- Auto-stop timer defers while a card viewer is connected (G3); call_mode and
  stream_duration persist across restarts via RestoreEntity (G2).
- Camera closes in-flight PCs on unload; lock cancels its auto-lock timer on
  removal; send_audio resolves coordinators dynamically and is unregistered on
  the last unload; shared API client closed once (bvis#12, G5, G4, G7).
- Misc: recv() raises MediaStreamError not StopIteration; get_running_loop;
  access-token / credentials-save / callDate parse hardened (bvis#29,bvis#28,bvis#31,bvis#33,bvis#39).

## Security / hardening
- TLS enforcement uses a urlparse allowlist and rejects whitespace, closing the
  " http://…" leading-space bypass; signaling URL must be https/wss on
  *.fermax.io (bvis#16, bvis#37).
- Diagnostics redacts all Firebase identifiers + OAuth tokens; log redaction
  recurses into lists; join_call errors no longer logged verbatim (bvis#35,bvis#34,bvis#36).
- send_audio and auto-response file paths validated before av.open (bvis#38);
  recording temp files already use mkstemp; ffmpeg shell=False.
- Rate-limit log filter is now thread-safe (bvis#44).

## Custom Lovelace card (v1.1.1)
- Mic is never acquired during idle polling (no OS mic-indicator flicker);
  talkback is enabled on demand via a mic-inclusive sendrecv reconnect, which
  is how a single-offer server negotiates outbound audio (bvis#5).
- Epoch guard prevents PC/subscription leaks when the card is removed mid-
  connect; synchronous re-entrancy lock stops duplicate connects; explicit
  hangup no longer auto-reconnects; aspect_ratio is sanitised (bvis#4,bvis#17,bvis#40,bvis#41).

## Dependencies
- Declare av>=11 and numpy>=1.20 (used on core streaming paths; permissive
  bounds satisfied by HA, no re-conflict with the av 17 situation) (G1).

Deferred (negligible/intentional): per-frame relay list copy (bvis#22), in-memory
recording buffer bounded by auto-stop (bvis#23), FCM restart backoff by design
(bvis#25), minor stop() sleeps (bvis#42), reload token ordering (bvis#43).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

Harden FCM push client against firebase-messaging reconnect storm (HA event-loop CPU exhaustion)

1 participant