Skip to content

Emit INITIALIZED after rebuilding a reinterviewed device - #987

Draft
TheJulianJES wants to merge 2 commits into
zigpy:devfrom
TheJulianJES:tjj/reinterview-emit-initialized
Draft

TheJulianJES wants to merge 2 commits into
zigpy:devfrom
TheJulianJES:tjj/reinterview-emit-initialized

Conversation

@TheJulianJES

Copy link
Copy Markdown
Contributor

Proposed change

Re-pairing an already known Green Power device leaves its card on the Add Device page stuck at "Configuration complete / Initializing".

zigpy announces a re-commissioned GPD as device_reinterviewed, so it goes through _async_device_reinterviewed. That path only emits DeviceFullInitEvent(CONFIGURED). The frontend shows CONFIGURED as "Initializing" and only completes the card on INITIALIZED, which never comes. A first commissioning goes through async_device_initialized instead, which emits CONFIGURED and then INITIALIZED.

This PR makes the reinterview rebuild end with INITIALIZED as well, after CONFIGURED, for both the in-place rebuild and the device swap. A failed rebuild still emits neither.

The swap test in tests/test_gateway.py and the GP re-commission test in tests/test_green_power_device.py asserted the CONFIGURED-only sequence; both now expect CONFIGURED, INITIALIZED. The GP test also checks the in-place rebuild, which is the case a GPD without a quirk hits. Both tests fail on dev.

Additional information

Seen with a SENIC/GIRA Friends of Hue switch (EnOcean PTM 215Z) on EZSP: the first commissioning completed the card, every re-commissioning after that stuck at "Initializing".

The CONFIGURED-only emit dates back to #755. Before GP support (#868), only a Reconfigure-triggered reinterview reached it, with the Add Device page closed, so it wasn't visible. A Reconfigure-triggered reinterview now also emits INITIALIZED; HA only records the pairing status from it and forwards it to the frontend.

Related, but a different path: #981 (a known Zigbee device rejoining with the same NWK never reaches ZHA at all).

A re-commissioned Green Power device goes through the reinterview rebuild,
which only emitted CONFIGURED. The pairing UI shows that as "Initializing"
and never completes. Finish with INITIALIZED, like a join does.
@TheJulianJES TheJulianJES changed the title Emit INITIALIZED after rebuilding a reinterviewed device Emit INITIALIZED after rebuilding a reinterviewed device Oct 28, 2026
@codecov

codecov Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.58%. Comparing base (c552c7b) to head (36535d9).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #987   +/-   ##
=======================================
  Coverage   97.58%   97.58%           
=======================================
  Files          61       61           
  Lines       10870    10871    +1     
=======================================
+ Hits        10607    10608    +1     
  Misses        263      263           

☔ 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.

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good: _async_device_reinterviewed now ends with CONFIGURED then INITIALIZED, matching the join path (async_device_initialized / _async_device_joined), and a failed rebuild still returns before emitting either.

Verified (4 checks)
  • The emit loop sits after the all_succeeded early return, so the failure path is unchanged (zha/application/gateway.py:611-630).
  • It covers both the in-place rebuild and the quirk-swap branch, since both fall through to the same emit.
  • tests/test_gateway.py and tests/test_green_power_device.py pass at this head (87 passed).
  • The full[2:] assertion in the GP test does check the second (in-place) recommission, not the first.

Minor, no change needed: new_join stays False on both events, whereas the join path sends new_join=True on CONFIGURED only. That matches the swap-test docstring (HA repoints its device proxy off the first event), so it's consistent.

@TheJulianJES

Copy link
Copy Markdown
Contributor Author

@zigpy-review-bot review as non-trivial PR, check logic, check in HA, and so on. Why does this go through reinterview btw when it's just reset by the user and rejoining whilst the "add device" page is open? Is this just for ZGP? We don't clear attribute cache and do a full re-interview for other devices doing this, right?

@zigpy-review-bot

Copy link
Copy Markdown
Collaborator

Re-reviewed at a459434 as a non-trivial PR (logic traced through zigpy dev and the HA Core handler, tests run in a worktree). No blocking findings, the earlier approval stands.

Why a re-pair goes through the reinterview path

It doesn't, for Zigbee devices. It's Green Power only.

  • A reset Zigbee device rejoining goes through zigpy handle_join and then device_initialized, which lands in ZHA async_device_initialized and _async_device_rejoined. That path only calls async_configure(). It doesn't rebuild the device, doesn't clear the attribute cache and doesn't interview again. It then emits CONFIGURED and INITIALIZED.
  • device_reinterviewed has exactly two sources in zigpy: Device.reinterview() (Reconfigure, post-OTA) and gp_device_recommissioned(), called from _finalize_commissioning in zgp/manager.py for an already-known GPD. So the "full re-interview" only happens for a re-commissioned GPD, because zigpy hands over a fresh GreenPowerDevice and ZHA rebuilds from it. Nothing is read from the device and there is no attribute cache to clear for a GPD.
  • The Reconfigure path now also emits INITIALIZED. HA only stores the status on the device info and forwards it, so it's harmless there.

Logic

  • The loop emits CONFIGURED and then INITIALIZED only when all_succeeded, and the failed-rebuild early return is unchanged.
  • Both events use new_join=False, and both the in-place and swap branches reach the emit.

Check in HA

Read against HA Core dev handle_device_fully_initialized:

  • Swap case: the first event rebinds the proxy and re-adds entities, because swapped_device is true. By the second event the proxy already points at the new object, so it only sets pairing_status=INITIALIZED and forwards the message. No duplicate entity creation.
  • In-place case: neither event creates entities, as before. The second event only forwards the status, which is what completes the card.
    I didn't run the HA test suite or the frontend, since nothing in HA changes.

Tests

  • On the PR head, tests/test_gateway.py and tests/test_green_power_device.py pass (87 passed).
  • The two updated tests fail with the zha/ sources from dev, so they cover the change.
  • CI is green on 3.12, 3.13 and 3.14.

Nit (non-blocking)

The new comment says "a GPD is re-commissioned from the pairing UI" but the code runs for every reinterview. The PR body covers that, so it could say "also reached by Reconfigure".

@zigpy-review-bot zigpy-review-bot added the bugfix This PR fixes a bug label Oct 28, 2026
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.

2 participants