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#1849TheJulianJES wants to merge 5 commits into
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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
QueryNextImagereports acurrent_file_versionchange 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_notifyand 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.
- 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
DRAFT / EXPERIMENTAL.
Note
Part of a four-PR series making post-OTA re-interviews reliable for sleepy end devices. Merge order (first to last):
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-actions→tjj/reinterview-on-checkin→tjj/reinterview-pending-persistence).Problem
After a successful flash,
update_firmware()readcurrent_file_versionwith a fixed retry budget (#1818 bumped it from ~40 s to ~110 s), then sentimage_notifyand re-interviewed. Devices that reboot slower than the budget — and sleepy end devices that never answer reads at all — threw, skipping bothimage_notifyand the re-interview. OTAs done outside ofupdate_firmware()(another coordinator, factory pre-staging) never triggered a re-interview at all.Changes
QueryNextImageCommandwhosecurrent_file_versiondiffers 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_TIMEOUT= 5 min) that resolves on whichever arrives first: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),QueryNextImage(via theOtaQueryCacheUpdatedEventlistener, with a race-safe re-check around listener attach),device_joinedevent for this device (the Device_annce path after the post-flash reboot).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 onapplication.devices).update_firmware()logs a warning and still returnsSUCCESS; recovery is deferred to the version-change fallback.Notes
Device_anncedoes not firedevice_joined(handle_jointreats 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.POST_OTA_PROBE_RETRIES(10 retries after the initial read, 10 s apart, matchingread_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 returnsSUCCESSand skipsimage_notify). Existing OTA tests updated for the new flow. Full suite passes.