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
- 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.
- 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.
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-295pauses only the pending upload:but
_has_pending_work()(delivery.py:249-252) is:_pending_config_resyncis never cleared on the auth path, so_has_pending_work()staysTrueand the next advertisement immediately starts another identical, doomed session. Delivery is advertisement-driven (coordinator.py:144fires 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, soMAX_DELIVERY_ATTEMPTS = 5never 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 reportingAUTH_STATUS_RATE_LIMITinstead.Both land in the same
exceptclause and produce the same user-facing string (authentication_error, "check your encryption key"), so:Suggested fix
_pending_config_resyncon auth failure, or add an auth-paused flag that_has_pending_work()consults — so the loop reaches a terminal state.AuthenticationRequiredError(status0x03, permanent until a config write) reports "device has no encryption key configured" rather than prompting reauth.Distinguishing
0x01(wrong key) from0x04(rate limited) needs a separate exception class in py-opendisplay first — they are currently bothAuthenticationFailedError.Notes
Line numbers read from
feat/wifi-pr; the auth/delivery logic is byte-identical onfeat/clean-port. Device-side evidence is from ESP32 firmware logs (2026-08-05) showing0x0050 -> 00 50 03on every attempt.