Skip to content

Confirm post-OTA reboot via announce/version-change instead of a fixed retry budget - #1849

Draft
TheJulianJES wants to merge 5 commits into
zigpy:devfrom
TheJulianJES:tjj/reinterview_ota_announcement
Draft

Confirm post-OTA reboot via announce/version-change instead of a fixed retry budget#1849
TheJulianJES wants to merge 5 commits into
zigpy:devfrom
TheJulianJES:tjj/reinterview_ota_announcement

Conversation

@TheJulianJES

@TheJulianJES TheJulianJES commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

DRAFT / EXPERIMENTAL.

Note

Part of a four-PR series making post-OTA re-interviews reliable for sleepy end devices. Merge order (first to last):

  1. Confirm post-OTA reboot via announce/version-change instead of a fixed retry budget #1849 — Confirm post-OTA reboot via announce/version-change instead of a fixed retry budget (this PR)
  2. Add generic per-device check-in action mechanism #1850 — Add generic per-device check-in action mechanism
  3. Queue a pending re-interview for a device's next check-in #1851 — Queue a pending re-interview for a device's next check-in
  4. Persist pending re-interview requests in the application database #1852 — Persist pending re-interview requests in the application database

Reworks the post-OTA recovery flow in update_firmware() so it no longer depends on the device answering a fixed read-attributes retry window. Branch: tjj/reinterview_ota_announcement. First of a series ending in queued re-interviews for sleepy devices (tjj/checkin-actionstjj/reinterview-on-checkintjj/reinterview-pending-persistence).

Problem

After a successful flash, update_firmware() read current_file_version with a fixed retry budget (#1818 bumped it from ~40 s to ~110 s), then sent image_notify and re-interviewed. Devices that reboot slower than the budget — and sleepy end devices that never answer reads at all — threw, skipping both image_notify and the re-interview. OTAs done outside of update_firmware() (another coordinator, factory pre-staging) never triggered a re-interview at all.

Changes

  • Stateless version-change fallback. When a device sends a QueryNextImageCommand whose current_file_version differs from the previously cached value, Ota._maybe_schedule_post_ota_reinterview() schedules a re-interview. This works regardless of how or when the device was flashed and survives restarts of zigpy itself. Guarded: skipped on first observation (no baseline), unchanged version, ota_in_progress, an already-running re-interview, and the coordinator.
  • Post-OTA confirmation wait. The fixed read/notify/reinterview sequence is replaced by a wait (POST_OTA_CONFIRMATION_TIMEOUT = 5 min) that resolves on whichever arrives first:
    • a successful active probe read of current_file_version (kept from the old flow, but now best-effort — exhausted retries no longer abort anything; this covers rebooted mains devices that stay quiet),
    • a version-changed QueryNextImage (via the OtaQueryCacheUpdatedEvent listener, with a race-safe re-check around listener attach),
    • a device_joined event for this device (the Device_annce path after the post-flash reboot).
  • After confirmation: best-effort image_notify (so the device re-queries and repopulates the OTA query cache), then a direct re-interview — skipped if the version-change listener already replaced this device with a re-interviewed one (staleness check on application.devices).
  • On timeout, update_firmware() logs a warning and still returns SUCCESS; recovery is deferred to the version-change fallback.
  • The pre-flash OTA query cache is deliberately no longer cleared on success: the cached version is the baseline the version-change comparison needs.

Notes

  • A same-NWK Device_annce does not fire device_joined (handle_join treats it as a non-join); those devices are covered by the probe and the QNI signal — and by the check-in trigger in the follow-up branches.
  • New constants: POST_OTA_PROBE_RETRIES (10 retries after the initial read, 10 s apart, matching read_attributes() semantics), POST_OTA_CONFIRMATION_TIMEOUT (300 s).

Testing

New tests for the version-change fallback (baseline/unchanged/coordinator/during-OTA/already-reinterviewing guards, scheduled-not-awaited) and the confirmation wait (resolves on device_joined, on version change, on probe read; ignores other devices and unchanged versions; race-safe pre-attach change; listener cleanup on cancel; timeout still returns SUCCESS and skips image_notify). Existing OTA tests updated for the new flow. Full suite passes.

When a Zigbee device boots after an OTA, it sends a `QueryNextImageCommand`
with its running `current_file_version`. If that version differs from the
previously cached value for this cluster, schedule `device.reinterview()`
to rebuild endpoints, clusters, and quirks against the new firmware.

This makes post-OTA recovery stateless: it works for slow-rebooting devices
that miss the post-flash polling window in `update_firmware()`, devices
flashed outside HA, and HA restarts during the reboot window.
Replace the fixed post-OTA read_attributes / image_notify / reinterview
sequence with a confirmation step that resolves on whichever signal
arrives first:

- A successful active read of `current_file_version` (kept from the old
  flow as a probe for quietly-rebooting mains devices, but now
  best-effort: exhausted retries no longer abort the post-OTA steps).
- A new `QueryNextImageCommand` whose `current_file_version` differs from
  the value the OTA cluster cached pre-flash. The OTA cluster's listener
  also schedules a re-interview when this happens.
- A `Device_annce` from this device after the post-flash reboot.

If none arrive within `POST_OTA_CONFIRMATION_TIMEOUT` (5 min),
update_firmware() still returns SUCCESS — recovery is deferred to the
version-change listener if the device wakes up later.

After confirmation, a best-effort `image_notify` is sent so the device
refreshes its OTA query cache, and a re-interview is driven directly
unless the version-change listener already replaced this device with a
re-interviewed one.

This fixes the slow-reboot race where the read_attributes retry budget
was exhausted before the device finished rebooting (e.g. Hue LLC020
~102s), causing update_firmware() to throw and skip image_notify and
reinterview.

Claude-Session: https://claude.ai/code/session_01VnDASUDhbLfANUM9UbnAeo
Switch the post-OTA wait's announce signal from a `device.zdo` listener for
`device_announce` to an `application.add_listener` for `device_joined`.

`device_joined` is the existing application-level event for new joins / NWK
changes (including the Device_annce path), so we don't add a new ZDO-level
listener. The version-change signal still covers same-NWK rejoins.

Also adds a test verifying that a `device_joined` event for a different
device does NOT resolve this device's wait.
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.47%. Comparing base (925ea3d) to head (f06690b).
⚠️ Report is 13 commits behind head on dev.

Additional details and impacted files
@@           Coverage Diff           @@
##              dev    #1849   +/-   ##
=======================================
  Coverage   99.47%   99.47%           
=======================================
  Files          57       57           
  Lines       12026    12079   +53     
=======================================
+ Hits        11963    12016   +53     
  Misses         63       63           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

This PR reworks the post-OTA recovery flow to confirm device reboot via announce/version-change signals (and a best-effort probe) rather than relying on a fixed read-attributes retry budget, and adds a stateless version-change fallback to trigger re-interviews when firmware changes are observed.

Changes:

  • Schedule a re-interview when a device’s QueryNextImage reports a current_file_version change vs the previously cached value.
  • Replace the post-OTA fixed retry read/notify/reinterview sequence with a confirmation wait (join event, version-changed QNI, or active probe), followed by best-effort image_notify and conditional re-interview.
  • Add/update tests covering the new version-change fallback and the post-OTA confirmation wait behavior.

Reviewed changes

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

File Description
zigpy/zcl/clusters/general.py Tracks prior QNI version and schedules a re-interview on firmware version change.
zigpy/device.py Replaces fixed post-OTA retry window with confirmation wait + best-effort notify + conditional re-interview; adds related constants and helper.
tests/test_zcl_clusters.py Adds unit tests for QNI version-change-triggered re-interview scheduling and guards.
tests/test_device.py Updates OTA tests for new confirmation flow and adds coverage for timeout/confirmation paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread zigpy/device.py Outdated
Comment thread zigpy/device.py
- Rename `POST_OTA_PROBE_ATTEMPTS` to `POST_OTA_PROBE_RETRIES`:
  `read_attributes(retries=N)` performs N+1 total attempts, so the old
  name misrepresented the behavior (which matches dev's existing
  `retries=10`).
- Document that the QueryNextImage confirmation deliberately accepts any
  query when no pre-flash baseline was cached: the wait only establishes
  that the device is alive after the flash.
- Use `asyncio.create_task()` instead of `asyncio.ensure_future()` for
  the probe task.

Claude-Session: https://claude.ai/code/session_01VnDASUDhbLfANUM9UbnAeo
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.

2 participants