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

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-actions → tjj/reinterview-on-checkin → tjj/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 19 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

bugfix This PR fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants