Z-Wave
Connects Athena to an already-running zwave-js-server instance (the
plain-WebSocket protocol Z-Wave JS UI bundles and exposes, commonly on port 3000)
rather than owning a Z-Wave USB stick's serial protocol directly - the same "bridge to a
companion process"
architecture Athena's own Matter integration uses, since there's no mature pure-Go Z-Wave stack
to embed.
Running zwave-js-server (Z-Wave JS UI)
This isn't bundled with Athena's own compose files - run it as its own separate container,
anywhere on your LAN that can see the USB stick (any Z-Wave stick works, including
Home Assistant's own Connect ZWA-2). Athena's own repo already ships this as a
ready-made docker-compose-zwave.yml (with a matching .env.zwave.example)
- copy both, fill in the .env, and skip straight to docker compose up -d
below. Or save the snippet here as your own docker-compose.yml in a new folder:
# docker-compose.yml - zwave-js-ui, standalone services: zwave-js-ui: container_name: zwave-js-ui image: zwavejs/zwave-js-ui:latest restart: unless-stopped tty: true stop_signal: SIGINT devices: # Use /dev/serial/by-id/... (see `ls /dev/serial/by-id/`), not /dev/ttyUSB0 - that mapping # can shift across reboots if more than one USB-serial adapter is plugged in. - '/dev/serial/by-id/<your-stick-here>:/dev/zwave' volumes: - zwave-config:/usr/src/app/store ports: - '8091:8091' # Z-Wave JS UI's own web UI - open this once to run the inclusion/exclusion wizard - '3000:3000' # the actual zwave-js-server websocket - this is the port Athena connects to volumes: zwave-config:
Run docker compose up -d (or docker compose -f docker-compose-zwave.yml up
-d if you're using the ready-made file, which doesn't auto-discover the way a bare
docker-compose.yml does), then open http://<that-host>:8091 once
to walk through Z-Wave JS UI's own setup (it auto-detects the stick over /dev/zwave
and enables its bundled Z-Wave JS server automatically). From then on, Athena only ever needs
port 3000 - the web UI on 8091 is just for pairing new devices and
isn't required to be reachable for Athena itself to work.
The devices: mapping above pins one specific stick - fine for most setups, but
Docker resolves it to one fixed device node at container start, so unplugging the stick (or
swapping it for a different one) isn't guaranteed to keep working without running
docker compose up -d again. For real hot-plug support instead, replace that
devices: block with - /dev:/dev and add:
device_cgroup_rules: - 'c 188:* rmw' # ttyUSB* - FTDI/CP210x/CH340-style USB-serial adapters - 'c 166:* rmw' # ttyACM* - USB CDC-ACM, most Z-Wave sticks including the Connect ZWA-2
This grants access to every USB-serial device instead of one pinned path, so the stick keeps
working across an unplug/replug or a swap to a different one, with no container restart -
Athena's own docker-compose.yml documents the identical tradeoff for its Zigbee
EZSP mount.
Adding a gateway
Devices & Services > Z-Wave > "Add gateway" takes your zwave-js-server
instance's address and port. Once connected, every node the server already knows about (id and
name) is offered as a live picker in "Add device" - real discovery, not a hand-typed address.
Connecting also hands over the server's full current network state immediately, so after that
point everything is push-driven over the same WebSocket, no polling involved.
What's supported
Command classes are auto-mapped to entity domains using the metadata zwave-js-server already reports for each value - no per-device manual classification step:
- Switches - Binary Switch, controllable.
- Dimmers - Multilevel Switch, kept in Z-Wave's own native 0-99 range.
- Covers & shutters - also Multilevel Switch, but on a node whose own device class identifies it as a shutter/blind motor (e.g. Shelly's own Wave Shutter) - shown as a real position slider on the same 0-1 scale every other cover integration in Athena uses, instead of a plain dimmer number.
- Locks - Door Lock, using the same lock domain every other integration's Lock/Unlock control already uses.
- Motion & binary sensors - Notification and Binary Sensor command classes, read-only. Motion feeds Area occupancy automatically, the same way it does for every other motion-capable integration.
- Numeric sensors & meters - Multilevel Sensor and Meter, unit and label taken straight from the value's own reported metadata rather than a fixed per-property table - this is what makes an arbitrary Z-Wave sensor value work without enumerating every sensor type up front.
Every supported entity works with Dashboard cards and Automations exactly like any other integration.
This integration is unverified against a live zwave-js-server instance - built directly from that project's own published WebSocket protocol, not smoke-tested during development. If something doesn't match once tested against a real server, it's a small, self-contained fix.