Skip to content

Auth failure on a pending config resync retries forever (one connection per advertisement) #91

Description

@davelee98

Summary

When authentication fails while a config resync is pending, the delivery manager retries indefinitely — one BLE connect/auth/disconnect cycle per received advertisement (~3 s apart in practice), with no backoff, no attempt cap, and no terminal state. Observed as ~20 identical failed sessions in 60 s on a device firmware log, and it would have continued indefinitely.

Cause

custom_components/opendisplay/delivery.py:288-295 pauses only the pending upload:

except (AuthenticationFailedError, AuthenticationRequiredError):
    if self._pending_upload is not None:
        self._pending_upload.paused = True
    self._entry.async_start_reauth(self._hass)

but _has_pending_work() (delivery.py:249-252) is:

return upload_ready or self._pending_config_resync

_pending_config_resync is never cleared on the auth path, so _has_pending_work() stays True and the next advertisement immediately starts another identical, doomed session. Delivery is advertisement-driven (coordinator.py:144 fires every parsed advertisement with no throttle), so there is no timer to slow it down.

The same branch also does not call _register_attempt_failure, so MAX_DELIVERY_ATTEMPTS = 5 never increments here — the only cap in the file does not apply to auth failures.

A pending upload correctly stops (paused = True); a pending config resync loops. That asymmetry is the bug.

Impact

The device rate-limits authentication at >= 10 attempts / 60 s. At ~20/60 s this loop sits permanently over the limit, so the device stops reporting the real error (AUTH_STATUS_NOT_CONFIG — no encryption key configured) and starts reporting AUTH_STATUS_RATE_LIMIT instead.

Both land in the same except clause and produce the same user-facing string (authentication_error, "check your encryption key"), so:

  • the user is told their key is wrong when the device actually has no key configured, and
  • after ~30 s the genuine cause is no longer observable from the host at all.

Suggested fix

  1. Clear _pending_config_resync on auth failure, or add an auth-paused flag that _has_pending_work() consults — so the loop reaches a terminal state.
  2. Split the clause so AuthenticationRequiredError (status 0x03, permanent until a config write) reports "device has no encryption key configured" rather than prompting reauth.

Distinguishing 0x01 (wrong key) from 0x04 (rate limited) needs a separate exception class in py-opendisplay first — they are currently both AuthenticationFailedError.

Notes

Line numbers read from feat/wifi-pr; the auth/delivery logic is byte-identical on feat/clean-port. Device-side evidence is from ESP32 firmware logs (2026-08-05) showing 0x0050 -> 00 50 03 on every attempt.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions