Repository navigation
700 and 800 series controllers: backup, restore and verify (1.1.0) - #1
Merged
Merged
Conversation
The plugin now images 700 and 800 series sticks (Zooz ZST10 700 and ZST39, Aeotec Z-Stick 7 and Z-Stick 10 Pro, Silicon Labs UZB-7 and similar) as well as the 500 series it already handled. The 500-series code paths are unchanged. controller_image.py - Nvm700: NVM3 open/read/write/close over NVMOperations 0x2E, or ExtendedNVMOperations 0x3D (32-bit offsets) when the stick advertises it in its GetSerialApiCapabilities function bitmask. - begin/end_nvm_session_700: radio off (0x10) and watchdog stopped (0xD3) before any NVM access; soft reset afterwards, waiting for the unsolicited SerialAPIStarted (0x0A). Mirrors zwave-js backupNVMRaw700, whose serial frames were captured from Z-Wave JS UI and matched byte for byte. - dump_nvm_700 / write_nvm_700: probe with a 255-byte read, take the chunk the stick answers with (64 on all three sticks tested), fall back to 48 on an empty probe. The final write, the one reaching the NVM size, is answered "end of file" and not performed by the firmware; it is reported and left alone, never split and retried: a one-byte write there hung a ZST39 until it was replugged, with the watchdog stopped so it could not recover itself. - compare_images_700: a 700/800 read-back is compared everywhere the image holds data; differences inside the declined tail or where the image is erased (0xFF) are the firmware's own housekeeping, anything else is a real mismatch. Byte-identical read-back is not achievable on NVM3. - read_identity: node ids parsed by payload length (16-bit node id mode, which zwave-js sets and which survives a soft reset), the advertised function list, and the full SDK version from GetProtocolVersion (0x09); an extra restore guard on that version. Known models: ZST39 LR, Z-Stick 10 Pro and the Silicon Labs 700 reference ids. - Two fixes found in live use: a backup folder pasted with quotes is cleaned up (a relative path put the images inside the plugin bundle), and the Home ID check considers every network Indigo's Z-Wave prefs remember rather than the first one found (a real prefs file held five). plugin.py - _with_stick opens a 700/800 session instead of refusing, and always sends the closing reset, on failure too, so a stick is never left with its radio off. Backup and verify pick the generation; verify tolerates additions in free space and compares the node table with the sidecar's. - _restore_body_700: write, reset, read back at once (no unplug on this generation), verify with compare_images_700 plus Home ID and node table. tests - FakeStick700 with profiles from the captured ZST39 and 700-stick payloads: declined tail, the hang on a tail write (asserted never triggered), 16-bit node ids, the probe fallback, housekeeping appended on reset, a stick that drops writes. 27 new tests; tests/run.sh green (96). Tested live 14-09-2026: Zooz ZST39 LR (Z-Wave 7.24) and Aeotec Z-Stick 10 Pro (Z-Wave 7.23), each backup/verify/restore/verify; a Silicon Labs 700 stick (Z-Wave 7.17, 108 node ids) running Indigo 2025.2's live network, backup from the plugin menu inside Indigo, 16 s, byte-identical to a terminal run and to a Z-Wave JS UI backup of the same stick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
700 and 800 series controllers
Adds backup, restore and verify for 700 and 800 series sticks (Zooz ZST10 700 / ZST39, Aeotec Z-Stick 7 / Z-Stick 10 Pro, UZB-7 and similar), as discussed on the forum thread. The 500-series code paths are untouched; the new generation gets its own functions in
controller_image.pyand its own restore body inplugin.py.How it works
Same shape as the 500 path, but through the NVM3 commands these sticks use — a straight port of zwave-js
backupNVMRaw700/restoreNVMRaw700, whose serial frames I captured from Z-Wave JS UI and matched byte for byte:SetRFReceiveModeoff andStopWatchdogbefore any NVM access;SoftResetafterwards and wait for the unsolicitedSerialAPIStarted. The reset is always sent on the way out (failure paths too), so the stick is never left with its radio off. No unplug on restore: the reset is the documented step and the read-back follows at once.NVMOperations 0x2E(16-bit offsets), orExtendedNVMOperations 0x3Dwhen the stick advertises it in its function bitmask (needed once the NVM passes 64 KB; the ZST39 and Z-Stick 10 Pro do, the 700 stick does not).GetProtocolVersion(7.17.2where the library string says7.17), used for the same-software restore guard and shown in Show Plugin Info / the device.Two things learnt on hardware that shape the restore path
0xFF(end of file) and dropped; zwave-js behaves the same and never checks. Those bytes hold nothing any file refers to. The plugin reports it and leaves it alone — and deliberately does not do what the 500 path does for the Gen5's last 16 bytes (split and retry): a 1-byte write near the end hung a ZST39 outright, no ACK, soft reset ignored, watchdog already stopped, replug needed.write_nvm_700writes in the probed chunk and accepts EOF on the last one; a test asserts the fake's "hang" is never triggered.compare_images_700allows differences only inside the declined tail and where the image holds0xFF; anything overwritten is "did NOT hold". Restore additionally checks the Home ID and the node table reported after the reset against the sidecar. Verify says how many bytes were added in free space and compares the node table too.Two small fixes found in live use
clean_folder_pathstrips quotes, expands~, and the dialog refuses anything not absolute.Home…_Node…key in Indigo's Z-Wave prefs; a real prefs file carried five Home IDs from past networks and the first was not the live one, so it warned about a mismatch that was not there. It now checks against the set.Tests
FakeStick700intests/fake_stick.py, built from the captured ZST39 and 700-stick payloads, covers the declined tail, the hang, 16-bit node ids, the probe fallback, housekeeping on reset, and a stick that says OK to writes and keeps none. 27 new tests;tests/run.shgreen (96 tests, syntax, XML, ruff). All existing tests pass unchanged apart from the one that asserted a 700 stick is refused.Live verification
0x2Eand0x3D; an older image restored brought back its older node tableVersion bumped to 1.1.0; README, SPEC and release notes updated. I don't have a 500-series stick, so the Gen5 path was covered by the existing tests only — worth one run on yours before release.
🤖 Generated with Claude Code