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.