Skip to content

Fix EZSP v14+ leaveNetwork and setManufacturerCode schemas - #749

Merged
puddly merged 3 commits into
devfrom
zigpy-bot/v14-leave-network-options
Sep 17, 2026
Merged

puddly merged 3 commits into
devfrom
zigpy-bot/v14-leave-network-options

Conversation

@zigpy-review-bot

@zigpy-review-bot zigpy-review-bot commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Since EZSP v14, leaveNetwork takes a one-byte options argument and setManufacturerCode returns an sl_status_t. bellows' v14 command table inherited both v4-era layouts: _REPLACEMENTS only swaps EmberStatus/EzspStatus for sl_Status, and v16–v19 spread the v14 table. Found in the audit on #747.

  • leaveNetwork (0x0020): on v14+, bellows sent the command with no argument, but the NCP reads options = fetchInt8u(); regardless, so it read one byte past the end of the frame. EZSP.leaveNetwork() now sends options=SlZigbeeLeaveNetworkOption.WITH_NO_OPTION (0x00) on v14+, the way the SDK host and zigbee-herdsman do. Pre-v14 behaviour is unchanged.
  • setManufacturerCode (0x0015): on v14+, the 4-byte status was left over as trailing data. The v14 table now parses it. The call site in application.py ignores the result, as before.

Both are overridden in bellows/ezsp/v14/commands.py, so v16–v19 pick them up through inheritance. New tests check the leaveNetwork frame bytes and both response layouts through every v14+ table, and the version gate in EZSP.leaveNetwork(). The full suite passes.

Related: #747. The other v14+ drift from that audit is covered by #748 (getExtendedTimeout/lookupNodeIdByEui64) and #750 (the definitions bellows doesn't call).

Verification
  • v13 (GSDK 4.4.6 command-functions.h): ezspLeaveNetwork(void) sends no argument and reads a 1-byte status. ezspSetManufacturerCode reads nothing back.
  • v14 (SiSDK 2024.6.0) and v19 (SiSDK 2026.6.1): sl_zigbee_ezsp_leave_network(options) does appendInt8u(options) then fetchInt32u(). sl_zigbee_ezsp_set_manufacturer_code does fetchInt32u(). The NCP handlers in app/em260/command-handlers-networking-generated.c match. Neither command changed between v14 and v19.
  • The option values (0x00 no option, 0x20 rejoin, 0x40 requested) are from sl_zigbee_types.h.
  • Review rounds: a Fable 5.1 agent and Copilot (GPT-5.6 Sol) both came back with no findings.

🤖 Generated with Claude Code

Since EZSP v14, `leaveNetwork` takes a one-byte `options` argument and
`setManufacturerCode` returns an `sl_status_t`. The v14 table inherited
both v4-era layouts, so bellows sent `leaveNetwork` without the byte the
NCP reads and left the `setManufacturerCode` status as trailing data.
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.55%. Comparing base (8f9d87b) to head (8c8fbba).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #749   +/-   ##
=======================================
  Coverage   99.55%   99.55%           
=======================================
  Files          64       64           
  Lines        4269     4281   +12     
=======================================
+ Hits         4250     4262   +12     
  Misses         19       19           

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

Comment thread bellows/ezsp/__init__.py Outdated

with self.wait_for_stack_status(t.sl_Status.NETWORK_DOWN) as stack_status:
(status,) = await self._command("leaveNetwork")
if self._ezsp_version >= 14:

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.

@zigpy-review-bot Turn leave_network into a version-specific protocol command accepting options. We generally do not want version branching like this. I believe this same options enum is used by ZDO so use the zigpy one directly in the shared interface to avoid EZSP internals from leaking out. For the V4 implementation, throw an error if anything other than "no option" is passed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pushed dd592dd. ProtocolHandler now has an abstract leave_network(options) that takes zigpy's ZDO.LeaveOptions, and EZSP.leaveNetwork() calls it (passing options through) instead of checking the version. EZSPv4 raises ValueError for anything other than LeaveOptions.NONE.

One correction: the ZDO and EZSP enums don't share values. zigpy's LeaveOptions uses the Mgmt_Leave_req layout (RemoveChildren = 0x40, Rejoin = 0x80). EZSP's sl_zigbee_leave_network_option_t uses the NWK leave command layout: WITH_OPTION_REJOIN = 0x20, IS_REQUESTED = 0x40, and 0x80 is SL_ZIGBEE_LEAVE_NWK_SILENTLY, a testing-only "leave without broadcasting a Network Leave" bit that used to be remove-children. Passing zigpy's value straight through would turn Rejoin into a silent leave. So EZSPv14 translates NONEWITH_NO_OPTION and RejoinWITH_OPTION_REJOIN, and raises ValueError for anything that includes RemoveChildren, since EZSP has no equivalent. SlZigbeeLeaveNetworkOption is now only used in the v14 command schema.

The PR description's leaveNetwork bullet still describes the old version check. Re-mention me if you want it updated, or if you'd rather handle RemoveChildren differently.

Verification
  • Enum values: stack/include/sl_zigbee_types.h (SiSDK) for sl_zigbee_leave_network_option_t; zigpy/zdo/__init__.py ZDO.LeaveOptions for the ZDO side.
  • New tests: v4 leave_network() with no options, and ValueError for Rejoin/RemoveChildren; the v14 option mapping and the RemoveChildren rejection; the EZSP.leaveNetwork() pass-through, error, and timeout paths now mock the protocol's leave_network.
  • Full suite: 466 passed. The changed lines in v4/__init__.py and v14/__init__.py are fully covered. pre-commit passes under Python 3.12 (the pinned pyupgrade/mypy hooks crash under 3.14, which is an environment issue).

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.

@zigpy-review-bot Hmm, t.SlZigbeeLeaveNetworkOption indeed looks cleaner. Let's avoid the ZDO remapping and just use the SiLabs enum directly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pushed 8c8fbba. The ZDO remapping is gone: ProtocolHandler.leave_network() and EZSP.leaveNetwork() now take t.SlZigbeeLeaveNetworkOption (default WITH_NO_OPTION). EZSPv14 passes it straight to the NCP. EZSPv4 still raises ValueError for anything other than WITH_NO_OPTION. Nothing in bellows imports zigpy.zdo for this anymore.

The PR description's leaveNetwork bullet still describes the original version check. I haven't edited it. Re-mention me if you want it updated.

Verification
  • Tests updated: v14 now checks the default and a WITH_OPTION_REJOIN pass-through. v4 checks that WITH_OPTION_REJOIN/IS_REQUESTED raise. The EZSP.leaveNetwork() pass-through test uses the SiLabs enum. The ZDO-mapping tests are removed.
  • Full suite: 464 passed. The changed lines in v4/__init__.py and v14/__init__.py are fully covered. pre-commit passes under Python 3.12.

The shared protocol interface takes zigpy's ZDO `LeaveOptions` instead of
branching on the EZSP version. EZSPv4 rejects any option, since its
`leaveNetwork` has no options argument. EZSPv14 translates `Rejoin` into
`SlZigbeeLeaveNetworkOption.WITH_OPTION_REJOIN` and rejects `RemoveChildren`:
EZSP uses the NWK leave command's bit layout, where 0x40 means the leave was
requested and 0x80 is a testing-only "leave silently" bit, so the ZDO values
can't be passed through.
Drop the ZDO `LeaveOptions` translation: `ProtocolHandler.leave_network`
and `EZSP.leaveNetwork` now take the SiLabs enum. v14+ passes it through
to the NCP unchanged; v4 raises `ValueError` for anything other than
`WITH_NO_OPTION`.
@puddly
puddly merged commit 15ccb49 into dev Sep 17, 2026
39 of 42 checks passed
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