Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Discuss how Indigo and Z-Wave work together. If you have a Z-Wave device that's not listed in the Home Automation Hardware forum or in the supported devices web app, report it here.
mat
Posts: 776
Joined: Thu Nov 25, 2010 10:48 am
Location: Cambridgeshire - UK

Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by mat »

Hello,

Skip the first three paras if you just want the technical.

My home automation has been running indigo since around 2011 (ish) and with the exception of one house move, has worked flawlessly. Like many I have gone quiet on the boards as it's just working. Until about 18 months ago. My gen5 stick failed. With limited time and other things taking priority it got pushed to the bottom of the pile. Everything else was working except 106 z-wave modules. I just couldn't face added them all back. We had arguments about lights being left on. My daughter had a melt down as she didn't know where any light switches were <- weeks of crashing into things in the dark which I personally found hilarious, and at the same time being called a "dick" with every trip. Those with teenage daughters will understand it was my fault, but that's a generation that have never needed to use a switch to turn a light on! We all know what adding 106 modules back really takes, especially those hidden in the back of cupboards and in the loft.

So I'm currently working on a new suit of software for DJ's. After 100 days coding I decided to take a break. With winter drawing in and the lights on more and more I decided it was time to tackle the stick! I had a fresh one ready.

Before starting, I thought I'd get claude to have a look at what was wrong with the stick. As expected the chip had lost the mapping table. Now im not a tech tech like lots of you, but I asked claude if it could fix the stick. 4 hours work, and we found the method, and a Mac save and restore script to boot. I'll leave claude to finish the story in its own words. I hope this helps someone.

Recovering a Z-Wave 500-series controller's lost node list WITHOUT re-pairing (Aeotec Z-Stick Gen5)

Mats Z-Stick Gen5 lost its entire node table: Indigo still listed all of the devices, but the controller reported no nodes, so nothing could be commanded ("can't find node"), and he had had no controller backup from before the loss (dick). The standard answer to that is "re-pair every device" — around a hundred here, many behind faceplates. Instead, working through it with Claude (Anthropic's AI), it rebuilt the controller's node table from Indigo's own device database and brought the whole network back with zero re-pairing. It's all confirmed working — on/off, dimming, blinds, colour, meters. Full method and generic scripts below in case it saves someone else the pain.

The situation
A Z-Wave 500-series controller that is the primary can end up with an empty node table (Mats did — cause unknown, possibly a glitch/failing NVM). The Home ID and every physical device are still fine and still bound to the network; it's only the controller that forgot its list. Indigo can't command devices it can't find on the controller, so the house goes dark. Without a pre-loss backup, everyone (zwave-js, HomeSeer, Silicon Labs, the "rebuild your mesh" guides) says the same thing: re-pair everything. A node table can normally only be built by the inclusion protocol.

Why the obvious fixes don't work
- The Aeotec backup tool can clone a controller's identity (Home ID) onto a fresh same-model stick — but it carries no node list.
- zwave-js (@zwave-js/nvmedit) can build and inject a full node table — the nodes read back — but on this firmware its jsonToNVM500 output isn't byte-native (it's ~57 bytes short and drops the routing tables), so the firmware sees a not-quite-valid NVM and zeroes the protected Home ID on boot. You get the nodes back but lose the Home ID.

The key insight
Don't rebuild the NVM (lossy). Seed the Home ID with the Aeotec clone, then edit that byte-native image IN PLACE — write only the node-table and routing entries via zwave-js's low-level NVM500 class, leaving every validity marker, the Home ID and the S0 key byte-identical — and write it back raw (restoreNVMRaw, never restoreNVM, which re-runs the lossy conversion). The node data comes from Indigo, which still stores each device's node ID, its [basic, generic, specific] device class, and its neighbour list.

Confirmed working
Network fully recovered, no devices re-included. The final rebuild writes every device's correct class into the node table — dimmers, blinds, colour, meters, not a generic placeholder — so the controller reports each device's true type and full function returns across the board once you reconnect the stick. Important gotcha we hit: a device's available commands follow the class the stick reports, and Indigo's per-device "Sync" (Edit device → Define and Sync… → Sync) just re-reads that class. So (a) rebuild the table with the correct classes first; and (b) if you'd already Synced a device earlier, while the stick still had a wrong/placeholder class, that Sync "corrected" its Indigo record down to on/off — re-Sync it against the now-correct stick to repair it. That was the only manual fixup we needed.

Requirements — two same-model 500-series controllers, target on a zwave-js-supported SDK (≥ 6.61; Gen5 v1.02 / "6.07" works, v1.00 doesn't). Node.js 20 + npm i zwave-js. Aeotec Backup Tool (Windows) for the Home-ID seed.

Scripts (generic — fill the CONFIG blocks for your own network):

1) backup.js

Code: Select all

const { Driver } = require("zwave-js"); const fs = require("fs");
const PORT = process.argv[2], OUT = process.argv[3], CACHE = process.argv[4] || "./cache";
const d = new Driver(PORT, { logConfig:{enabled:false}, storage:{cacheDir:CACHE} });
const wd = setTimeout(()=>{console.error("timeout");process.exit(3)},180000);
d.on("error",e=>console.error("ERR:",e.message));
d.once("driver ready", async()=>{ try{
  const nvm = await d.controller.backupNVMRaw((c,t)=>process.stdout.write("\r  "+c+"/"+t+"   "));
  fs.writeFileSync(OUT, nvm);
  console.log("\nSAVED",nvm.length,"bytes ->",OUT,"| homeId",d.controller.homeId?.toString(16));
} catch(e){ console.error("\nERR:",e.stack||e.message);} finally{ clearTimeout(wd); await d.destroy().catch(()=>{}); process.exit(0);} });
d.start().catch(e=>{console.error("START ERR (stick on this machine?):",e.message);process.exit(1)});
Run: node backup.js /dev/cu.usbmodemXXXX backup.bin

2) extract_nodes.py — pull per-node data from Indigo's database (the .indiDb is XML):

Code: Select all

import xml.etree.ElementTree as ET, shutil, json, sys
SRC = sys.argv[1]                      # path to your Indigo .indiDb
ZW  = "com.perceptiveautomation.indigoplugin.zwave"
shutil.copy(SRC, "/tmp/_hadb"); root = ET.parse("/tmp/_hadb").getroot()
parent = {c:r for r in root.iter() for c in r}
def txt(e,t): x=e.find(t); return (x.text or "").strip() if (x is not None and x.text) else ""
def ints(e,t):
    x=e.find(t); return [int(i.text) for i in x.findall("Item") if i.text and i.text.strip().lstrip('-').isdigit()] if x is not None else []
nodes={}
for el in root.iter():
    if el.tag!=ZW: continue
    mp=parent.get(el); dev=parent.get(mp) if mp is not None else None
    if dev is None or dev.tag!="Device": continue
    a=txt(el,"address")
    if not a or not a.isdigit(): continue
    n=nodes.setdefault(a,{"battery":False,"neighbors":[],"classIds":[]})
    if txt(el,"SupportsBatteryLevel")=="true": n["battery"]=True
    nb=ints(el,"zwNodeNeighbors"); n["neighbors"]=nb if len(nb)>len(n["neighbors"]) else n["neighbors"]
    ci=ints(el,"zwClassIds");      n["classIds"]=ci if len(ci)>len(n["classIds"]) else n["classIds"]
print(json.dumps({"nodes":nodes}))
Run: python3 extract_nodes.py "<your .indiDb>" > nodes.json (classIds = [basic, generic, specific] device class)

3) inject.js — rebuild the node table into a native image (base = a backup that already has the Home ID):

Code: Select all

const nv = require("@zwave-js/nvmedit"), fs = require("fs");
const { NVMMemoryIO } = require(require.resolve("@zwave-js/nvmedit").replace(/index.js$/,"lib/io/NVMMemoryIO.js"));
const BASE=process.argv[2], NODESJSON=process.argv[3], OUT=process.argv[4];
// ---------- CONFIG for your network ----------
const SECURE_NODES    = new Set([]);  // node IDs included with S0 security (from Indigo), else []
const CLASS_OVERRIDES = {};           // {nodeId:[generic,specific]} for any node whose stored class is wrong
// ---------------------------------------------
(async()=>{
  const io=new NVMMemoryIO(Uint8Array.from(fs.readFileSync(BASE)));
  const nvm=new nv.NVM500(io); await nvm.init();
  const nt=await nvm.get("EX_NVM_NODE_TABLE_START_far");     // 232-slot NodeInfo array (empty = undefined)
  const rt=await nvm.get("EX_NVM_ROUTING_TABLE_START_far");  // 232 neighbour-lists
  const base=nt[0];
  const data=JSON.parse(fs.readFileSync(NODESJSON,"utf8"));
  const ids=Object.keys(data.nodes).map(Number).filter(n=>n>=2).sort((a,b)=>a-b);
  for(const id of ids){
    const d=data.nodes[String(id)]; const cls=Array.isArray(d.classIds)?d.classIds:[];
    let gen = cls.length>=2 ? cls[1] : (d.battery?0x20:0x10);
    let spec= cls.length>=3 ? cls[2] : 1;
    if(CLASS_OVERRIDES[id]){ gen=CLASS_OVERRIDES[id][0]; spec=CLASS_OVERRIDES[id][1]; }
    nt[id-1]={ isListening:!d.battery, isFrequentListening:false, isRouting:!d.battery,
      supportedDataRates:base.supportedDataRates, protocolVersion:base.protocolVersion,
      optionalFunctionality:false, nodeType:1, supportsSecurity:SECURE_NODES.has(id),
      supportsBeaming:true, genericDeviceClass:gen, specificDeviceClass:spec };
    rt[id-1]=(d.neighbors||[]).filter(x=>x>=1&&x<=232);
  }
  await nvm.set("EX_NVM_NODE_TABLE_START_far", nt);
  await nvm.set("EX_NVM_ROUTING_TABLE_START_far", rt);
  await nvm.set("EX_NVM_MAX_NODE_ID_far",[Math.max(...ids)]);
  await nvm.set("EX_NVM_LAST_USED_NODE_ID_START_far",[Math.max(...ids)]);
  fs.writeFileSync(OUT, Buffer.from(io._buffer));
  const chk=await nv.nvm500ToJSON(Buffer.from(io._buffer));
  console.log("built:",OUT,"| homeId:",chk.controller.ownHomeId,"| nodes:",Object.keys(chk.nodes).length);
})().catch(e=>console.log("ERR",e.stack||e.message));
Run: node inject.js seeded_backup.bin nodes.json injected.bin (do NOT use nvm500ToJSON→jsonToNVM500 — lossy)

4) restore_raw.js — write RAW (never restoreNVM); prints a clear next step, not a scary error:

Code: Select all

const { Driver } = require("zwave-js"); const fs = require("fs");
const PORT=process.argv[2], NVMFILE=process.argv[3], CACHE=process.argv[4]||"./cache";
const nvm=fs.readFileSync(NVMFILE);
const BENIGN=/requires a driver restart|did not respond after soft-reset|being destroyed/i; // expected, not failures
let wrote=false;
const d=new Driver(PORT,{logConfig:{enabled:false},storage:{cacheDir:CACHE}});
const wd=setTimeout(()=>{console.error("\nWATCHDOG: no completion in 180s.");process.exit(3)},180000);
d.on("error",e=>{ if(!BENIGN.test(e.message||"")) console.error("REAL ERROR:",e.message); });
function finish(){ clearTimeout(wd);
  console.log("\n=================================================================");
  if(wrote){ console.log(" WRITE COMPLETE -> image written to the stick.");
    console.log(" NEXT: unplug the stick, wait ~3s, plug it back in (power-cycle to activate).");
    console.log(" 'driver restart' / 'soft-reset' messages above are EXPECTED, not errors.");
    console.log(" THEN VERIFY: python3 verify.py"); }
  else console.log(" WRITE DID NOT CONFIRM — see REAL ERROR above; do NOT replug/verify yet.");
  console.log("=================================================================");
  d.destroy().catch(()=>{}).finally(()=>process.exit(0));
}
d.once("driver ready",async()=>{ try{
  let last=0; await d.controller.restoreNVMRaw(nvm,(c,t)=>{ if(c-last>=20000||c===t){last=c;process.stdout.write("\r  writing "+c+"/"+t+"   ");}});
  wrote=true;
} catch(e){ if(BENIGN.test(e.message||"")) wrote=true; else console.error("\nREAL ERROR:",e.stack||e.message);} finally{ finish(); } });
d.start().catch(e=>{console.error("START ERR — stick on THIS machine? (ls /dev/cu.usbmodem*)\n ",e.message);process.exit(1)});
Run: node restore_raw.js /dev/cu.usbmodemXXXX injected.bin → then power-cycle the stick

5) verify.py — read Home ID + node list over the raw Serial API:

Code: Select all

import os, termios, time, glob, sys
p=sorted(glob.glob("/dev/cu.usbmodem*"))
if not p: sys.exit("no usbmodem port")
PORT=p[0]
def frame(f,d=b""):
    b=bytes([0,f])+d; L=len(b)+1; c=0xFF
    for x in bytes([L])+b: c^=x
    return bytes([1,L])+b+bytes([c])
fd=os.open(PORT,os.O_RDWR|os.O_NOCTTY|os.O_NONBLOCK); a=termios.tcgetattr(fd)
a[2]|=(termios.CLOCAL|termios.CREAD); a[2]&=~termios.CSIZE; a[2]|=termios.CS8
a[2]&=~(termios.PARENB|termios.CSTOPB|termios.CRTSCTS)
a[0]&=~(termios.IXON|termios.IXOFF|termios.IXANY|termios.INPCK|termios.ISTRIP)
a[1]&=~termios.OPOST; a[3]&=~(termios.ICANON|termios.ECHO|termios.ECHOE|termios.ISIG); a[4]=a[5]=termios.B115200
termios.tcsetattr(fd,termios.TCSANOW,a); os.write(fd,b"\x15"); time.sleep(0.3)
def rd(t=1.5):
    buf=b""; end=time.time()+t
    while time.time()<end:
        try:
            c=os.read(fd,256)
            if c: buf+=c; end=time.time()+0.3; os.write(fd,b"\x06")
        except BlockingIOError: time.sleep(0.02)
    return buf
os.write(fd,frame(0x20)); r=rd(); j=0; home=None
while j<len(r)-3:
    if r[j]==1 and r[j+2]==1 and r[j+3]==0x20:
        pl=r[j:j+r[j+1]+2]
        if len(pl)>=9: home=pl[4:8].hex().upper()
        break
    j+=1
os.write(fd,frame(0x02)); r=rd(2.0); j=0; nodes=[]
while j<len(r)-3:
    if r[j]==1 and r[j+2]==1 and r[j+3]==0x02:
        fr=r[j:j+r[j+1]+2]; bm=fr[7:7+fr[6]]
        nodes=[bi*8+bit+1 for bi,by in enumerate(bm) for bit in range(8) if by&(1<<bit)]; break
    j+=1
os.close(fd)
print("HOME ID:", ("0x"+home) if home else "?", "| NODES(%d):"%len(nodes), nodes)
Note: the first read right after a replug can show "?" or empty (the stick is settling / live node traffic in the buffer) — just run it again once or twice.

Step-by-step
1. node backup.js <oldstickport> old.bin (or Aeotec Read EEPROM) — keep it as a safety copy.
2. Aeotec tool: Read EEPROM the old (Home-ID-bearing) stick → Write EEPROM to a same-model, same-firmware target stick. This seeds the correct Home ID onto the target.
3. node backup.js <targetport> seeded.bin — native image: Home ID present, empty node list.
4. python3 extract_nodes.py "<your .indiDb>" > nodes.json — each device's node ID, [basic,generic,specific] class, and neighbours.
5. node inject.js seeded.bin nodes.json injected.bin — rebuilds the whole node table with every device's real class (from nodes.json). Only touch the CONFIG for SECURE_NODES, and CLASS_OVERRIDES for any single device whose stored class you know is wrong.
6. node restore_raw.js <targetport> injected.bin → power-cycle the stick → python3 verify.py (expect the correct Home ID + full node count).
7. Reconnect the stick to your controller machine and point the software at it. Because the table now carries each device's correct class, devices regain full function on their own — there's no per-device step to run.
Only repair case: if you Synced a device earlier, while the stick still had a wrong/placeholder class, that Sync corrupted its record down to on/off — re-Sync only that device against the now-correct stick. Never Sync against a stick that has wrong classes.

Expected messages (look like errors, aren't) — "Activating the NVM backup requires a driver restart", "did not respond after soft-reset", "controller instance is being destroyed": the write succeeded; just power-cycle the stick. First post-replug read showing "?"/empty: re-read.
Real problems — "Failed to open the serial port … No such file or directory" = stick not on this machine/wrong port. "Did not find a matching NVM 500 parser … SDK 6.61 or higher" = firmware too old for zwave-js. Home ID reads 0x00000000/random on a settled re-read = the write was rejected (non-native image) → rebuild in place + restoreNVMRaw, or restore your backup.

Do this now that it works (so it never recurs)
Back up after any change: node backup.js <port> backup_YYYYMMDD.bin, stored off-device. Restore to the same stick: node restore_raw.js <port> backup.bin → power-cycle. Restore to a new same-model, same-firmware stick: Aeotec Write EEPROM the backup (or restore_raw.js it) — carries Home ID + node table, no re-pairing. The one rule: always restore with restoreNVMRaw (raw/native) or the Aeotec tool — never restoreNVM, whose lossy conversion zeroes the Home ID.

Credit — the reverse-engineering and these scripts were worked out with Claude (Anthropic). Sharing generically in case it helps anyone else who's staring down a re-pair of their whole house.
Last edited by mat on Sat Sep 12, 2026 5:47 am, edited 3 times in total.
Late 2018 mini 10.14
mat
Posts: 776
Joined: Thu Nov 25, 2010 10:48 am
Location: Cambridgeshire - UK

Re: Revived dead Z-stick Gen 5 and Mac backup and restore

Post by mat »

if like me you are not technical, I had claude write a brief that others can use, and may assist.

Paste the block below into Claude (or similar) and fill in the [ ] parts. It primes the assistant with the method and, importantly, the safety rules from this thread.

Code: Select all

I need help recovering a Z-Wave 500-series controller that has lost its node table, WITHOUT re-pairing my devices. This is a known-good method from an Indigo forum write-up — follow it carefully and INTERACTIVELY, one step at a time, waiting for each result.

MY SETUP (I'll confirm):
- Controller: [e.g. Aeotec Z-Stick Gen5 / ZW090]; firmware/SDK: [we'll check]
- Spare same-model controller available? [yes/no]
- Home-automation software + database location: [e.g. Indigo, DB at <path>]
- Symptom: controller reports an empty/near-empty node list; software can't command devices ("can't find node"); devices are alive on the old Home ID; NO pre-loss backup.

THE METHOD (don't deviate without telling me why):
1. FIRST confirm my controller's firmware/SDK. @zwave-js/nvmedit needs SDK >= 6.61 on the TARGET stick; if mine is older, we use a same-model stick on supported firmware.
2. Seed the Home ID: the vendor backup tool "Read EEPROM" from the Home-ID stick -> "Write EEPROM" to the target. This is the ONLY reliable way to move a 500-series Home ID (no software can "set" one; a raw write of a changed Home ID gets zeroed by the firmware).
3. Extract per-node data from my software's database: node ID, [basic, generic, specific] device class, and neighbours.
4. Rebuild the WHOLE node table IN PLACE in the native NVM image using @zwave-js/nvmedit's low-level NVM500 class (get/set EX_NVM_NODE_TABLE_START_far and EX_NVM_ROUTING_TABLE_START_far), preserving the native byte structure. Do NOT use nvm500ToJSON -> jsonToNVM500 (lossy; the firmware then zeroes the Home ID).
5. Write it RAW with controller.restoreNVMRaw — NEVER restoreNVM (its lossy migrate zeroes the Home ID). Power-cycle the stick. Verify Home ID + node list over the raw Serial API.

RULES FOR YOU (the assistant):
- The ONLY irreversible step is writing to thnt NVM (backupNVMRaw) BEFORE any write and keep
  it; verify the backup parses.
- Verify the ACTUAL state at each step (read r assume or fabricate. The first read right
  after a replug can be garbage — re-read onc
- Benign post-write messages ("requires a dripond after soft-reset", "controller instance is
  being destroyed") mean SUCCESS — just powerof 0x00000000 or a random value means the
  write was REJECTED — rebuild in place + res backup.
- Do NOT run a per-device "Sync" against a stevice classes — it corrupts the software's
  record for that device. Rebuild the correct
- Ask me for my specifics (serial ports, DB pessing. Go one step at a time.

The working scripts (backup / extract / injecin the forum post — use or adapt them. Start by confirming my controller model + firmware andel spare.
Late 2018 mini 10.14
mat
Posts: 776
Joined: Thu Nov 25, 2010 10:48 am
Location: Cambridgeshire - UK

Re: Revived dead Z-stick Gen 5 and Mac backup and restore

Post by mat »

PS - Matt/Jay, and anyone else - happy for you to of course use however you see fit.
Late 2018 mini 10.14
mat
Posts: 776
Joined: Thu Nov 25, 2010 10:48 am
Location: Cambridgeshire - UK

Re: Revived dead Z-stick Gen 5 without exclude/include + Mac backup and restore

Post by mat »

A totally dead old stick situation

The following has not been tested, and is theoretical based on the the first post. Claude again .....

The method above clones the Home ID off your old stick with the vendor Backup Tool. But what if the old stick is truly dead and can't be read at all?

You very likely still know your Home ID and S0 network key — your controller software stores them (Indigo keeps the network key in the Z-Wave plugin prefs, and every device record carries the Home ID; other controllers keep an equivalent config export). So instead of cloning, you write those known values straight into a fresh, same-model, same-firmware stick, alongside the node table.

Why this is plausible, not just hopeful: the vendor Backup Tool already changes a stick's Home ID via a native full-EEPROM write — that's exactly what "clone the old one onto the new one" does. So the firmware does not hard-protect the Home ID against a native write. The failures people hit come from non-native rebuilds (the lossy JSON→NVM path), which the in-place edit avoids.

⚠️ Honest status: UNTESTED via this exact from-blank path. I recovered mine by cloning a still-readable original, so I never had to write a Home ID from scratch. It should work by the logic above — and if the firmware rejects it, you're no worse off than before (re-pair). If you try it, please reply with the result — that would confirm the fully-dead-controller case for everyone.

Use the variant script below. First back up your fresh stick's own NVM (backup.js) to use as the base, then edit the CONFIG block with your Home ID / S0 key, build, and check the printed Home ID matches your target before writing:

Code: Select all

  // inject_from_blank.js — DEAD-ORIGINAL variant. Writes Home ID (both NVM copies) + S0 key
  // from your KNOWN values, plus the node table, onto a FRESH stick. UNTESTED via this exact path.
  const nv = require("@zwave-js/nvmedit"), fs = require("fs");
  const { NVMMemoryIO } = require(require.resolve("@zwave-js/nvmedit").replace(/index\.js$/,"lib/io/NVMMemoryIO.js"));
  const BASE=process.argv[2], NODESJSON=process.argv[3], OUT=process.argv[4]; // BASE = backup of a FRESH same-model stick
  // ---------- CONFIG (from your controller software's records) ----------
  const HOME_ID = 0x00000000;   // <-- YOUR network's Home ID, e.g. 0xE25CD171
  const S0_KEY  = [];           // <-- your 16-byte S0 key as ints, or [] if you have no secure devices
  const SECURE_NODES    = new Set([]);   // node IDs included with S0, else []
  const CLASS_OVERRIDES = {};            // {nodeId:[generic,specific]} to correct any wrong stored class
  // ----------------------------------------------------------------------
  (async()=>{
    if(!HOME_ID){ console.error("Set HOME_ID first."); process.exit(1); }
    const io=new NVMMemoryIO(Uint8Array.from(fs.readFileSync(BASE)));
    const nvm=new nv.NVM500(io); await nvm.init();
    for(const prop of ["EX_NVM_HOME_ID_far","NVM_HOMEID_far"]){        // Home ID — BOTH copies
      const cur=await nvm.get(prop); await nvm.set(prop, Array.isArray(cur)?[HOME_ID]:HOME_ID);
    }
  { const cur=await nvm.get("NVM_NODEID_far"); await nvm.set("NVM_NODEID_far", Array.isArray(cur)?[1]:1); }
  if(S0_KEY.length===16) await nvm.set("NVM_SECURITY0_KEY_far", Uint8Array.from(S0_KEY));
  const nt=await nvm.get("EX_NVM_NODE_TABLE_START_far");
  const rt=await nvm.get("EX_NVM_ROUTING_TABLE_START_far");
  const base=nt[0];
  const data=JSON.parse(fs.readFileSync(NODESJSON,"utf8"));
  const ids=Object.keys(data.nodes).map(Number).filter(n=>n>=2).sort((a,b)=>a-b);
  for(const id of ids){
    const d=data.nodes[String(id)]; const cls=Array.isArray(d.classIds)?d.classIds:[];
    let gen=cls.length>=2?cls[1]:(d.battery?0x20:0x10), spec=cls.length>=3?cls[2]:1;
    if(CLASS_OVERRIDES[id]){gen=CLASS_OVERRIDES[id][0];spec=CLASS_OVERRIDES[id][1];}
    nt[id-1]={isListening:!d.battery,isFrequentListening:false,isRouting:!d.battery,
      supportedDataRates:base.supportedDataRates,protocolVersion:base.protocolVersion,
      optionalFunctionality:false,nodeType:1,supportsSecurity:SECURE_NODES.has(id),
      supportsBeaming:true,genericDeviceClass:gen,specificDeviceClass:spec};
    rt[id-1]=(d.neighbors||[]).filter(x=>x>=1&&x<=232);
  }
  await nvm.set("EX_NVM_NODE_TABLE_START_far",nt);
  await nvm.set("EX_NVM_ROUTING_TABLE_START_far",rt);
  await nvm.set("EX_NVM_MAX_NODE_ID_far",[Math.max(...ids)]);
  await nvm.set("EX_NVM_LAST_USED_NODE_ID_START_far",[Math.max(...ids)]);
  fs.writeFileSync(OUT,Buffer.from(io._buffer));
  const chk=await nv.nvm500ToJSON(Buffer.from(io._buffer));
  console.log("built:",OUT,"| homeId:",chk.controller.ownHomeId,"| nodes:",Object.keys(chk.nodes).length);
  console.log("CHECK the homeId matches your target BEFORE writing. Then restore_raw + power-cycle, and verify it HELD.");
})().catch(e=>console.log("ERR",e.stack||e.message));
Then write it with the same restore_raw.js + power-cycle + verify steps as the main method.
Late 2018 mini 10.14
siclark
Posts: 2356
Joined: Tue Jun 13, 2017 5:08 am
Location: UK

Re: Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by siclark »

I skipped all the technical bits as don’t need them yet thankfully but undertand what you did and that’s amazing.
Didn’t think that was possible on a Mac, or rather before software existed to do it, but doesn’t mean it’s not possible. (My Claude recently debugged a serial 2 ip feed looking at raw bytes)
Great work and hopefully this helps others.
CliveS
Posts: 864
Joined: Sun Jan 10, 2016 5:31 am
Location: Medomsley, County Durham, UK

Re: Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by CliveS »

mat, thank you. Your write-up on bringing a dead Gen5 back without re-pairing 106 devices is
the whole reason this exists, and working out that a stick's node table could be rebuilt at all
was the hard part. The idea is yours, I have just carried it a bit further and wrapped it up so
nobody else has to touch a script.

Reading your post I realised I had no copy of my own stick's memory either, and that if mine
forgot its devices I would be the one up the stepladder with a torch. So I got Claude Fable 5.1 onto it.
it found that my Gen5 is on the older 1.01 software.
The zwave-js tools you used can only read and convert sticks on 1.02 or later, and getting a Gen5 up
to 1.02 needs a Windows PC and Aeotec's own updater, which Aeotec themselves warn can brick the
stick and I am not that brave. So rather than leave it as a script only I could run, I got Claude to built it
as a plugin.

Z-Wave Controller Backup reads the whole of the stick's memory twice, checks the two reads match, then
saves it with a small note file beside it recording which stick it came from and when.

Verify Last Backup reads the stick again and compares it against the last copy.

Restore Controller writes a copy back and then reads the stick over again to prove the copy
really took, before it lets you carry on. It will refuse a copy taken from a different model of
stick, or from one running a different software version, because a raw copy has to match the
layout exactly.

There is also a Show Plugin Info showing what the stick is, when it was
last backed up and whether the network has changed since, and it warns you in the Event Log
when a Z-Wave device is added or removed, so you know to take a fresh copy if you leave the plugin running

Indigo holds the stick open and there is no command a plugin can use to make it let go, so every
backup is two clicks. Start it from the plugin menu, then choose Interfaces, Z-Wave, Disable, and
the plugin notices within a couple of seconds and starts the backup. When it has finished it writes
a line to the log telling you to enable Z-Wave again. It should take about two minutes to complete the backup.

I have an Aeotec Gen5, thirty nodes, Indigo 2025.2, and I ran all three options myself to prove it all works.
Backup, Verify, and Restore and a final unplug and plug back in. Here is the Event Log for the three of them, unedited:

Backup
13:12:43.016 Z-Wave Controller Backup Backup: waiting for Z-Wave to be switched off. In the Indigo client choose Interfaces > Z-Wave > Disable. It starts the moment Z-Wave is off (waiting up to 10 minutes).
13:12:51.845 Application Disabling interface "Z-Wave 2025.2.0"
13:12:51.849 Application Stopping interface "Z-Wave 2025.2.0" (pid 40656)
13:12:52.490 Z-Wave closed connection to Z-Stick Gen5+ (ZW090)
13:12:53.355 Z-Wave Controller Backup Z-Wave is off. Opening /dev/cu.usbmodem101.
13:12:53.653 Application Stopped interface "Z-Wave 2025.2.0"
13:12:56.757 Z-Wave Controller Backup Reading Aeotec Z-Stick Gen5, Home ID E0BB7FA8, Z-Wave 4.54 (SDK 6.51.10), 30 nodes, 256 KB of memory. Two full reads, about a minute.
13:14:09.845 Z-Wave Controller Backup Backup complete in 73 s: .../Z-Wave Controller Backups/Aeotec-Z-Stick-Gen5_E0BB7FA8_2026-09-13_1314.bin (262144 bytes, both reads identical, sha256 959e5dc75446).
13:14:09.849 Z-Wave Controller Backup Switch Z-Wave back on now: Interfaces > Z-Wave > Enable in the Indigo client.
13:15:21.228 Application Enabling interface "Z-Wave 2025.2.0" using API v3.8
13:15:21.233 Application Starting interface "Z-Wave 2025.2.0" (pid 65217)
13:15:21.646 Application Started interface "Z-Wave 2025.2.0"
13:15:24.044 Z-Wave connected to Z-Stick Gen5+ (ZW090) interface on /dev/cu.usbmodem101 (firmware 1.01, minimum SDK 4.54.00)
13:15:25.466 Z-Wave Controller Backup Z-Wave is back on.
Verify
13:17:38.432 Z-Wave Controller Backup Verify: waiting for Z-Wave to be switched off. In the Indigo client choose Interfaces > Z-Wave > Disable. It starts the moment Z-Wave is off (waiting up to 10 minutes).
13:17:54.851 Application Disabling interface "Z-Wave 2025.2.0"
13:17:54.853 Application Stopping interface "Z-Wave 2025.2.0" (pid 65217)
13:17:55.758 Z-Wave closed connection to Z-Stick Gen5+ (ZW090)
13:17:56.982 Application Stopped interface "Z-Wave 2025.2.0"
13:17:56.998 Z-Wave Controller Backup Z-Wave is off. Opening /dev/cu.usbmodem101.
13:18:00.425 Z-Wave Controller Backup Reading the controller to compare with Aeotec-Z-Stick-Gen5_E0BB7FA8_2026-09-13_1314.bin.
13:18:36.353 Z-Wave Controller Backup Verified: the controller's memory matches the last backup byte for byte.
13:18:36.357 Z-Wave Controller Backup Switch Z-Wave back on now: Interfaces > Z-Wave > Enable in the Indigo client.
13:18:43.898 Application Enabling interface "Z-Wave 2025.2.0" using API v3.8
13:18:43.903 Application Starting interface "Z-Wave 2025.2.0" (pid 66829)
13:18:44.297 Application Started interface "Z-Wave 2025.2.0"
13:18:45.495 Z-Wave connected to Z-Stick Gen5+ (ZW090) interface on /dev/cu.usbmodem101 (firmware 1.01, minimum SDK 4.54.00)
13:18:46.626 Z-Wave Controller Backup Z-Wave is back on.
Restore
13:20:19.148 Z-Wave Controller Backup Restore: waiting for Z-Wave to be switched off. In the Indigo client choose Interfaces > Z-Wave > Disable. It starts the moment Z-Wave is off (waiting up to 10 minutes).
13:20:28.582 Application Disabling interface "Z-Wave 2025.2.0"
13:20:28.583 Application Stopping interface "Z-Wave 2025.2.0" (pid 66829)
13:20:29.627 Z-Wave closed connection to Z-Stick Gen5+ (ZW090)
13:20:30.740 Application Stopped interface "Z-Wave 2025.2.0"
13:20:31.536 Z-Wave Controller Backup Z-Wave is off. Opening /dev/cu.usbmodem101.
13:20:34.855 Z-Wave Controller Backup Writing Aeotec-Z-Stick-Gen5_E0BB7FA8_2026-09-13_1314.bin (262144 bytes) into Aeotec Z-Stick Gen5, Home ID E0BB7FA8. Do not unplug anything yet.
13:21:27.059 Z-Wave Controller Backup The controller refused to write 16 bytes at offset 262128, but they already hold the image's content, so nothing is lost.
13:21:27.059 Z-Wave Controller Backup Written 262144 bytes. Resetting the controller.
13:21:27.061 Z-Wave Controller Backup Now unplug the stick, wait three seconds, and plug it back in. I will then read it back to check the image took.
13:23:24.707 Z-Wave Controller Backup Restore verified: the controller's memory matches the image byte for byte, Home ID E0BB7FA8, 30 nodes.
13:23:24.710 Z-Wave Controller Backup Switch Z-Wave back on now: Interfaces > Z-Wave > Enable in the Indigo client.
13:23:37.260 Application Enabling interface "Z-Wave 2025.2.0" using API v3.8
13:23:37.262 Application Starting interface "Z-Wave 2025.2.0" (pid 69160)
13:23:37.645 Application Started interface "Z-Wave 2025.2.0"
13:23:37.692 Z-Wave connected to Z-Stick Gen5+ (ZW090) interface on /dev/cu.usbmodem101 (firmware 1.01, minimum SDK 4.54.00)
13:23:38.954 Z-Wave Controller Backup Z-Wave is back on.
So all three plugin options tested on my setup

The line above about 16 bytes is because a Gen5 will not accept a write to the last 16 bytes of its memory. Those bytes are blank in every
copy of it, so nothing whatever is lost, and the plugin reads them straight back, confirms they
already hold exactly what the copy holds and carries on. It is not a fault.

Info at:
https://github.com/Highsteads/ZWaveControllerBackup

Download at :
https://github.com/Highsteads/ZWaveCont ... Plugin.zip

Unzip it, double-click the plugin and Indigo installs it. Nothing else needed, no Node, no
Windows, no Terminal. MIT licence so copy it, fork it, bend it, break it, fix it, ship it. If it breaks, you get to keep both pieces.

500-series sticks only for now, the Gen5 and Gen5+, the Z-Wave.me UZB, the SmartStick+ and
similar. The 700 and 800 series read their memory a different way and I have none to test
against, so the plugin recognises one and says so.

If it misbehaves, please post in this thread rather than opening anything on GitHub. I do not do
GitHub errors, and I have turned the issues tab off so nothing quietly piles up somewhere I never
look. The Event Log lines are the useful bit, they name the stick, the sizes and exactly which
step stopped.

Thanks again mat, this one is as much yours as mine.
CliveS

Indigo 2024.2.1 : macOS Sequoia 15.6.1 : Mac Mini M2 : 8‑core CPU and 10‑core GPU : 8 GB : 256GB SSD

The best way to get the right answer on the Internet is not to ask a question, it's to post the wrong answer
mat
Posts: 776
Joined: Thu Nov 25, 2010 10:48 am
Location: Cambridgeshire - UK

Re: Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by mat »

Great work, and thanks - I'm not claiming anything, it was all Claude, who on a couple of occasions told me it wasn't possible.

Thanks Clive, that really tidies everything up! Great work.
Late 2018 mini 10.14
CliveS
Posts: 864
Joined: Sun Jan 10, 2016 5:31 am
Location: Medomsley, County Durham, UK

Re: Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by CliveS »

I also have to give Claude the credit, and it is frightening how AI has moved in the last year, when I last asked about doing this, when 4.5 was the bleading edge, it told me it was not possible.

Now almost anything is possible, we can now all write our own plugins with no coding, just an idea and several conversations, an Indigo MCP (and I wrote my own) makes it even easier and my coding skills are just basic.

As Claude writes at the bottom of my plugins
Vibed into existence by CliveS, who knew what he wanted, argued until he got it, and tested it on a real house. Typed at inhuman speed by Claude (Anthropic), who mostly did as it was told.
CliveS

Indigo 2024.2.1 : macOS Sequoia 15.6.1 : Mac Mini M2 : 8‑core CPU and 10‑core GPU : 8 GB : 256GB SSD

The best way to get the right answer on the Internet is not to ask a question, it's to post the wrong answer
autolog
Posts: 4077
Joined: Tue Sep 10, 2013 3:07 am
Location: West Sussex, UK
Contact:

Re: Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by autolog »

Z-Wave Controller Backup 1.1.0 — 700 and 800 series sticks now supported

Following on from the earlier discussion, Clive has merged and released 700/800 series support I (and my Claude; actually mostly Claude :wink: ) put together, so the plugin now backs up, verifies and restores the newer sticks as well as the 500 series it already handled: Zooz ZST10 700 and ZST39, Aeotec Z-Stick 7 and Z-Stick 10 Pro, Silicon Labs UZB-7 and similar.

Download: Z-Wave Controller Backup 1.1.0. The 500-series code is untouched, and Clive has run it on his Gen5 before releasing.

What to expect on a 700/800 stick
  • It's a port of what Z-Wave JS does for these sticks; the serial traffic was captured from Z-Wave JS UI and matched byte for byte, and the backups are identical to the ones Z-Wave JS UI produces.
  • A backup takes 15–20 seconds. The stick is soft-reset at the end of every visit (that's the documented way to leave its memory tidy) and Indigo reconnects to it by itself, in a couple of seconds on mine.
  • Restore does not need the unplug-and-replug a 500-series stick needs; the closing reset does the job and the read-back follows straight away.
  • These sticks answer the very last write of a restore with "end of file" and don't perform it. That's normal (Z-Wave JS sees the same, and those bytes hold nothing any file uses); the Event Log says so when it happens.
  • Because the stick's firmware adds its own housekeeping into free space whenever it restarts, a 700/800 read-back is compared everywhere the image holds data rather than byte for byte, and the node table and Home ID are checked as well. The Event Log reports what was added in free space.
Tested on: Zooz ZST39 LR (Z-Wave 7.24) and Aeotec Z-Stick 10 Pro (Z-Wave 7.23), each backup → verify → restore → verify; and the 700 series stick running my live Indigo 2025.2 network (Z-Wave 7.17, 108 node ids), backup from the plugin menu inside Indigo.

Two small fixes for everyone, found on my first run: a backup folder pasted with quotes around it used to end up as a relative path (and the image landed inside the plugin bundle) — it's now cleaned up, and the dialog refuses a non-absolute path; and the Home ID warning could fire falsely if Indigo's Z-Wave prefs still remember old networks — it now checks all of them.

If anything looks odd, post the Event Log lines from the plugin here; they name the controller, the sizes and exactly which step stopped.

Thanks to Clive for taking it in. :D
CliveS
Posts: 864
Joined: Sun Jan 10, 2016 5:31 am
Location: Medomsley, County Durham, UK

Re: Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by CliveS »

Thanks Jon. That is a proper piece of work, and a good deal more of it than I expected when the
pull request landed, so it is only right that the write-up is yours too.

The Gen5 run, since that was the one path you had no way to test. Two full reads took 72 seconds
and the verify 37, which is what yesterdays version 1.0.2 gave to the second, and the image came back with the same
sha256 as the one 1.0.2 had written the day before. Byte for byte the same stick, read by the new
code.

The other thing is that none of my testing was done by hand. Claude opened the plugin's Config > Back Up
Controller Now menu, pressed Start, went to Interfaces > Z-Wave > Disable, waited out the two reads
and switched Z-Wave back on when the Event Log asked for it, then ran the verify the same way.

That is all done through Claude Bridge, my own plugin that hands Claude live access to Indigo.
Driving the client's menus is just one of its 169 tools, and a great deal quicker and easier than
writing the osascript which is the only other way in. There is no API for it so the menu is the
only door, and it needs the Indigo client running and accessibility permission granted.

A good few of those tools go where the API does not reach either. It will read a trigger's real
conditions and action steps, which the IOM does not expose at all and which live only in Indigo's
own database, and it will tell you every trigger, schedule and action group that touches a given
device or variable, chasing the chain through any action groups they call. It will work out which
automation caused a device to change.

Where there is no API at all, Claude just uses the Indigo client. There is no call anywhere in Indigo
for making a new trigger, so it opens the dialog and fills it in, exactly as you would with a
mouse, and it does it in the background without taking control of the screen.. My "Lights Off When
Lux Level Is True" was built that way, start to finish: the variable chosen, the change set to
becomes true, the action step pointed at the script, and the whole thing dropped in the right
folder. Then Claude Bridge reads it straight back out of the database to prove it took, action
step and all, which the API could not have shown me. Between the two there is not much left in
Indigo that cannot be reached.

Thanks to autolog and mat and I think we all need to thank Anthropic for Claude

Download: https://github.com/Highsteads/ZWaveCont ... Plugin.zip
Read me: https://github.com/Highsteads/ZWaveCont ... kup#readme

Clive.
CliveS

Indigo 2024.2.1 : macOS Sequoia 15.6.1 : Mac Mini M2 : 8‑core CPU and 10‑core GPU : 8 GB : 256GB SSD

The best way to get the right answer on the Internet is not to ask a question, it's to post the wrong answer
mat
Posts: 776
Joined: Thu Nov 25, 2010 10:48 am
Location: Cambridgeshire - UK

Re: Replaced dead Z-stick Gen 5 without exclude/include & Mac backup and restore

Post by mat »

Great work guys.

I guess we now leave jay :lol: to build the plugin that recreates the data for the stick from indigo if you don’t have a backup, and replace the correct house ID.

Seems like a feature for the main code. No backup needed, just write to whatever stick you replace an old one with. The script is there for the 500. :idea:

So nice to see the community at work. Great job! Thanks. I now too have a backup!
Late 2018 mini 10.14
Post Reply

Return to “Z-Wave”