Contents

Zwave-JS-UI and the Mesh That Finally Behaved

Everything I got wrong about Z-Wave before I could see what the mesh was doing

Contents

For about a year I believed Z-Wave was unreliable. The evidence seemed solid: a lock that took eight seconds to respond, a bedroom sensor that dropped off the network every few weeks, and a multisensor at the far end of the flat that reported temperature perfectly and refused to report motion.

Every one of those was my fault. I found that out the day I moved from Home Assistant’s built-in Z-Wave integration to Z-Wave JS UI and could finally see the routing table. The mesh had been telling me exactly what was wrong for a year; I had no window to look through.

What Z-Wave JS UI actually is

Advertisement

The naming here confuses people, so it’s worth untangling before anything else.

Z-Wave JS is the driver — a JavaScript implementation of the Z-Wave protocol that talks to your USB stick and understands command classes. It replaced OpenZWave and it is the thing that made Z-Wave on Linux good.

Z-Wave JS UI (formerly ZWaveJS2MQTT) is Z-Wave JS plus a control panel plus an MQTT gateway plus a WebSocket server. Home Assistant connects to that WebSocket server exactly as it would to the bare driver.

The Home Assistant Z-Wave integration bundles Z-Wave JS with no control panel. It works. It also gives you no view of the mesh, no way to run a heal, no interview log, no way to set a device’s configuration parameters outside what the integration chose to expose, and no NVM backup button. The functionality difference is small; the visibility difference is enormous, and visibility is the entire problem with Z-Wave.

Running it looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
services:
  zwave-js-ui:
    container_name: zwave-js-ui
    image: zwavejs/zwave-js-ui:9.20.0
    restart: unless-stopped
    tty: true
    stop_signal: SIGINT
    environment:
      SESSION_SECRET: "${ZUI_SESSION_SECRET}"
      TZ: Europe/Copenhagen
    devices:
      - "/dev/serial/by-id/usb-0658_0200-if00:/dev/zwave"
    volumes:
      - ./store:/usr/src/app/store
    ports:
      - "8091:8091"
      - "3000:3000"

That by-id path is load-bearing. Use /dev/ttyACM0 and the first time you plug in anything else with a serial interface — a Zigbee coordinator, an Arduino, a UPS — the kernel will renumber them on the next boot and your Z-Wave stick will become /dev/ttyACM1. The container will then start, find something that is definitely a serial device, fail to speak Z-Wave to it, and produce error messages that suggest hardware failure. The by-id path is stable across reboots and reshuffles.

Point Home Assistant’s Z-Wave integration at ws://192.168.1.20:3000 and it behaves identically to the built-in version, with a control panel now available on port 8091.

The stick matters more than the software

A quick detour, because the controller is the one component you cannot fix later with configuration.

Older 500-series sticks work fine and are cheap on the used market. The 700 and 800 series added better range, S2 support in hardware, and — the reason I would pay the difference — Z-Wave Long Range on 800-series controllers, which reaches a single distant node point-to-point without any mesh at all. That is the answer to the shed at the bottom of the garden.

Whatever you buy, put it on a USB extension cable. Every Z-Wave stick I have owned performs measurably worse plugged directly into the back of a machine, because USB 3.0 ports radiate broadband noise across a swathe of spectrum and a small antenna sitting two centimetres away is going to hear it. A one-metre extension that gets the stick out into open air is the cheapest range improvement available. This is equally true of Zigbee coordinators, for the same physics.

The three things I had wrong

Advertisement

Mains-powered devices are the network. Battery devices are leaves.

This is the part I misunderstood most expensively. Z-Wave is a mesh, and everyone repeats that, so I assumed adding more devices made it stronger. Then I added a battery-powered multisensor at the far end of the flat and wondered why it was unreliable.

Battery devices do not route. They cannot — they spend 99.9% of their life with the radio off, so nothing can relay through them. Only mains-powered devices (plugs, switches, in-wall dimmers) act as repeaters. A network of twelve battery sensors and one controller is a star with a dozen weak spokes, and calling it a mesh does not make it one.

My far-end multisensor became reliable the day I put a £25 smart plug in the hallway between it and the controller. The plug does nothing except be a repeater. It is the best value device on my network.

Include devices where they will live

The old advice was to bring every device next to the controller for inclusion, then put it in place. That advice predates Z-Wave Plus and it is now actively harmful. Modern inclusion negotiates security and routing based on the RF conditions at the moment of inclusion. Include a device six inches from the controller and it will confidently record a direct route that does not exist once the device is thirty feet away behind two walls.

Include in place. If the device is too far to include in place, that is diagnostic information: you have a coverage gap, and moving the device to the controller hides the gap without fixing it. Modern sticks support S2 inclusion with a DSK PIN, which works fine at distance.

Heal is not a cure, and the ordering matters

The “heal network” button asks every node to rediscover its neighbours and rebuild its routing table. People mash it whenever something misbehaves, and it usually helps nothing, because it discovers the same bad topology it discovered last time.

Heal is useful in one situation: after you have physically changed the network. Added a repeater, moved the controller, removed a device. Then heal, then wait — a full heal on a 30-node network takes tens of minutes and hammers the mesh while it runs. Heal at 3am, or you will spend the evening wondering why everything is slow.

Also: heal the repeaters first, then everything else. A battery device healing against a mesh whose repeaters have stale tables just records the stale topology.

Reading the routing table

This is what you bought Z-Wave JS UI for. The control panel’s node table shows, per node, the protocol, security class, statistics, and — the useful part — the actual route in use.

1
2
3
4
5
6
7
8
$ curl -s -X POST http://192.168.1.20:8091/api/zwave/getNodes \
    -H 'Content-Type: application/json' | jq -r '
    .result[] | [.id, .name, .isListening, .rssi, .statistics.commandsDroppedRX] | @tsv'
1	Controller	true	-	0
4	Hall Plug	true	-52	0
7	Bedroom Sensor	false	-89	31
9	Front Door Lock	false	-74	2
12	Kitchen Dimmer	true	-48	0

Node 7 is the story. RSSI of -89 dBm and 31 dropped commands means it is barely audible and half its messages die in flight. No amount of healing fixes -89 dBm. It needs a repeater between it and the nearest listening node, or it needs to move.

The other number worth watching is routeChanges in the per-node statistics. A stable device changes route rarely. A device that has changed route forty times this week is oscillating between two marginal paths and will feel unreliable in exactly the intermittent, unreproducible way that makes people blame the protocol.

Troubleshooting

A device joins, then shows as “unknown” with no name and no entities. The interview did not finish. Z-Wave JS asks a new node to enumerate every command class it supports, and battery devices sleep through the process. Wake the device manually — usually a button press pattern in its manual — and the interview resumes. A device stuck at “unknown” for days will re-interview on wake; there is a “Re-interview” button and it is worth pressing before you exclude anything.

Everything is slow after adding one device. Suspect a chatty node. A power-metering plug reporting instantaneous wattage every second saturates a network whose entire budget is 100 kbit/s shared across every node. Find its reporting-interval configuration parameters and set them to something a human can perceive — 30 seconds, or a 5-watt change threshold. This single mistake accounts for a startling proportion of “Z-Wave is slow” complaints.

Exclusion fails on a device you have physically removed. Use “Remove failed node”, which requires the controller to have already marked it as failed. Ping it first to force the controller to notice. This is the correct way to deal with a device that has gone in a skip.

The stick is missing after a host reboot. The by-id path, as above. Also check that another process has not grabbed it — ModemManager on many distributions will happily open any serial device and try to talk AT commands at it, which does nothing good to a Z-Wave stick. A udev rule excluding the device from ModemManager, or removing ModemManager entirely, ends that.

Everything died and the controller is empty. This is why the NVM backup exists. Z-Wave JS UI can dump the controller’s non-volatile memory — the entire network including security keys — to a file. Do that after every inclusion. Restoring an NVM backup onto a replacement stick of the same chipset brings the whole network back without touching a single device. Without it, a dead stick means physically excluding and re-including every node in the house. This backup belongs in your normal backup rotation alongside everything else; the argument for that is in the 3-2-1 backup rule actually implemented.

Secure devices are slower than insecure ones. True, and unavoidable. S0 security in particular triples the message count for every command — three frames where an insecure device uses one. S2 is far more efficient and modern devices support it. If you have an old S0 lock, that eight-second response is S0 doing its nonce dance across a marginal route. Re-include with S2 if the device supports it, and only use security where it earns its cost: locks and garage doors, yes; a lamp dimmer, no.

Where MQTT comes in

Z-Wave JS UI can publish every node’s state to an MQTT broker as well as serving the WebSocket API. Home Assistant does not need this — the WebSocket path is better in every way, with proper device registry entries and no topic mapping to maintain.

The MQTT gateway earns its place when something else needs the data. A Node-RED flow doing something Home Assistant’s automation engine expresses awkwardly, a logging pipeline, a script on a different machine. Publishing to the broker gives every one of those consumers the same event stream without any of them holding a WebSocket to your controller. If the broker side is unfamiliar territory, MQTT explained for people who just want sensors covers the model.

Run both if you want. They coexist without complaint.

Verdict

Z-Wave JS UI is worth installing the moment you have more than about five Z-Wave devices, and it is worth installing on day one if you intend to have more than five. The control panel costs you a container and gives you the routing table, the interview logs, the full configuration parameter set for every device, and the NVM backup button that will one day save your evening.

Z-Wave itself has earned about half its reputation. The protocol is slow by modern standards, S0 security is genuinely bad, and the mesh is unforgiving of a topology built entirely from battery devices. Against that: it runs at 868 MHz in Europe where the walls are kinder and the band is not shared with every Wi-Fi network in the building, the certification means devices from different vendors actually interoperate, and my locks and mains switches have run for years without a single re-pairing. The comparison against the alternatives is in Zigbee vs Z-Wave vs Matter if you are choosing a radio from scratch.

My mesh behaves now. It behaves because I put three mains-powered repeaters in the right places, turned off a plug that was shouting its wattage every second, and stopped including devices next to the controller. Z-Wave JS UI did none of that. It just showed me what was happening, which turned out to be the only thing I had been missing.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.