developer-tools

Is Z-Wave JS Down? Real-Time Status & Outage Checker

Is Z-Wave JS Down? Real-Time Status & Outage Checker

Z-Wave JS is an open-source Z-Wave driver implementation with 1,500+ GitHub stars, originally created by Dominic Griesel and maintained by the Open Z-Wave community. It powers Z-Wave device control in Home Assistant through the Z-Wave JS UI add-on, supporting Z-Wave, Z-Wave Plus, and Z-Wave Long Range protocols. The project exposes a WebSocket server API, supports over-the-air firmware updates, and ships a web-based UI for device inclusion, exclusion, and management. It is the backbone for home automation enthusiasts running door locks, thermostats, motion sensors, and other Z-Wave smart home hardware.

A Z-Wave JS outage silently breaks entire categories of home automation: locks stop responding to schedules, thermostats ignore setpoints, and Home Assistant automations that depend on sensor state changes halt without obvious error messages. Because the failure surface spans USB hardware, Linux device nodes, a WebSocket process, and the Z-Wave radio mesh itself, diagnosing the root cause requires checking each layer independently.

Quick Status Check

#!/bin/bash
# Z-Wave JS health check
set -euo pipefail

ZWAVEJS_HOST="${ZWAVEJS_HOST:-localhost}"
ZWAVEJS_PORT="${ZWAVEJS_PORT:-3000}"
USB_PATHS=("/dev/ttyACM0" "/dev/ttyUSB0" "/dev/ttyAMA0")

echo "=== Z-Wave JS Status Check ==="

# 1. Check USB/serial controller presence
echo -n "Z-Wave USB controller: "
FOUND_USB=""
for path in "${USB_PATHS[@]}"; do
  if [ -e "$path" ]; then
    FOUND_USB="$path"
    echo "DETECTED ($path)"
    break
  fi
done
[ -z "$FOUND_USB" ] && echo "NOT FOUND — check USB connection and udev rules"

# 2. Check WebSocket/HTTP port reachability
echo -n "Z-Wave JS HTTP port $ZWAVEJS_PORT: "
if nc -z -w3 "$ZWAVEJS_HOST" "$ZWAVEJS_PORT" 2>/dev/null; then
  echo "OPEN"
else
  echo "CLOSED — service may be down"
fi

# 3. Check HTTP health endpoint
echo -n "Z-Wave JS health endpoint: "
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  --connect-timeout 5 \
  "http://${ZWAVEJS_HOST}:${ZWAVEJS_PORT}/health" 2>/dev/null || echo "000")
echo "HTTP $HTTP_STATUS"

# 4. Check Docker container if applicable
echo -n "Z-Wave JS Docker container: "
if command -v docker &>/dev/null; then
  CONTAINER=$(docker ps --filter "name=zwave" --format "{{.Names}}" 2>/dev/null | head -1)
  [ -n "$CONTAINER" ] && echo "RUNNING ($CONTAINER)" || echo "NOT RUNNING"
else
  echo "Docker not available — checking process"
  pgrep -f "zwave-js\|zwavejs2mqtt\|zwave-js-ui" &>/dev/null \
    && echo "  Process: RUNNING" || echo "  Process: NOT FOUND"
fi

# 5. Check MQTT bridge port if enabled
echo -n "MQTT bridge port 1883: "
nc -z -w2 "$ZWAVEJS_HOST" 1883 2>/dev/null \
  && echo "OPEN" || echo "CLOSED (MQTT bridge may be disabled)"

echo "=== Check complete ==="

Python Health Check

#!/usr/bin/env python3
"""
Z-Wave JS health check
Checks HTTP health endpoint, WebSocket port, controller state via WS API,
node count, and failed node detection.
"""

import asyncio
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.error

HOST = os.environ.get("ZWAVEJS_HOST", "localhost")
HTTP_PORT = int(os.environ.get("ZWAVEJS_PORT", "3000"))
WS_PORT = HTTP_PORT
TIMEOUT = 10

results = []

def log(label: str, status: str, detail: str = "") -> None:
    symbol = "OK" if status == "ok" else "FAIL"
    line = f"[{symbol}] {label}"
    if detail:
        line += f": {detail}"
    print(line)
    results.append({"label": label, "status": status, "detail": detail})


def check_http_health() -> None:
    url = f"http://{HOST}:{HTTP_PORT}/health"
    try:
        req = urllib.request.Request(url, headers={"Accept": "application/json"})
        with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
            body = resp.read().decode()
            data = json.loads(body) if body else {}
            log("HTTP health endpoint", "ok", f"HTTP 200, body={body[:80]}")
    except urllib.error.HTTPError as e:
        log("HTTP health endpoint", "fail", f"HTTP {e.code}")
    except Exception as e:
        log("HTTP health endpoint", "fail", str(e))


def check_ws_port() -> None:
    try:
        with socket.create_connection((HOST, WS_PORT), timeout=TIMEOUT):
            log("WebSocket port", "ok", f"{HOST}:{WS_PORT} reachable")
    except Exception as e:
        log("WebSocket port", "fail", f"{HOST}:{WS_PORT} — {e}")


async def check_ws_controller() -> None:
    """Connect via raw WebSocket and query controller state."""
    import websockets  # type: ignore

    uri = f"ws://{HOST}:{WS_PORT}"
    try:
        async with websockets.connect(uri, open_timeout=TIMEOUT) as ws:
            # Read server hello
            hello = await asyncio.wait_for(ws.recv(), timeout=TIMEOUT)
            hello_data = json.loads(hello)
            schema = hello_data.get("serverVersion", "unknown")
            log("WebSocket handshake", "ok", f"serverVersion={schema}")

            # Query controller state
            cmd = json.dumps({"messageId": "1", "command": "controller.getState"})
            await ws.send(cmd)
            resp_raw = await asyncio.wait_for(ws.recv(), timeout=TIMEOUT)
            resp = json.loads(resp_raw)

            if resp.get("success"):
                state = resp.get("result", {})
                nodes = state.get("nodes", [])
                node_count = len(nodes)
                failed = [n for n in nodes if n.get("status") == 6]  # status 6 = Dead
                log("Z-Wave node count", "ok" if node_count > 0 else "fail",
                    f"{node_count} nodes in mesh")
                if failed:
                    log("Failed nodes", "fail",
                        f"{len(failed)} dead node(s): IDs {[n.get('nodeId') for n in failed]}")
                else:
                    log("Failed nodes", "ok", "No dead nodes detected")
            else:
                log("Controller state query", "fail",
                    resp.get("errorCode", "unknown error"))
    except ImportError:
        log("WebSocket controller check", "fail",
            "websockets package not installed — run: pip install websockets")
    except Exception as e:
        log("WebSocket controller check", "fail", str(e))


def main() -> None:
    print(f"=== Z-Wave JS Health Check — {HOST}:{HTTP_PORT} ===\n")
    t0 = time.time()

    check_ws_port()
    check_http_health()
    asyncio.run(check_ws_controller())

    elapsed = time.time() - t0
    failures = [r for r in results if r["status"] != "ok"]
    print(f"\n--- Summary ({elapsed:.1f}s) ---")
    print(f"Checks passed: {len(results) - len(failures)}/{len(results)}")
    if failures:
        print("Failures:")
        for f in failures:
            print(f"  - {f['label']}: {f['detail']}")
        sys.exit(1)
    else:
        print("All checks passed.")


if __name__ == "__main__":
    main()

Common Z-Wave JS Outage Causes

SymptomLikely CauseResolution
Home Assistant shows "Z-Wave JS unavailable"USB stick not detected after host reboot; udev rules missing or stick physically unpluggedCheck /dev/ttyACM0 exists; add udev rule to assign a stable symlink; replug USB stick
New device cannot be added to the networkInclusion mode failing; interference or S2 security handshake rejectedMove device closer to controller during inclusion; check S2 PIN on device label; try S0 or no security
Battery-powered node shows "Dead"Device battery exhausted or out of radio range; no route through meshReplace battery; add a powered Z-Wave Plus device as repeater near the dead node
WebSocket connection drops; Home Assistant loses all Z-Wave controlZ-Wave JS UI process crashed or OOM-killedCheck container/process logs; increase memory limit; set restart policy to always
Controller firmware update stuck in loopPartial firmware flash interrupted; controller in bootloader modePower-cycle USB stick; use Z-Wave JS UI recovery mode; reflash controller firmware
New S2 device pairs but commands not acceptedS2 security handshake mismatch; DSK entered incorrectlyExclude device, re-include with correct 5-digit PIN from device label; check Z-Wave JS logs for security errors

Architecture Overview

ComponentFunctionFailure Impact
Z-Wave USB ControllerRadio transceiver; sends/receives Z-Wave frames over 908 MHzTotal loss of Z-Wave communication; all nodes unreachable
Serial Port (/dev/ttyACM0)Linux device node exposing USB stick to userspaceZ-Wave JS cannot open controller; startup fails
Z-Wave JS Driver ProcessParses Z-Wave protocol frames; manages node cache and routing tablesAll node state goes stale; Home Assistant entities become unavailable
WebSocket Server (port 3000)Exposes Z-Wave JS API to Home Assistant integration and UIHome Assistant loses real-time control and state updates
Z-Wave JS UI (Web UI)Device management, inclusion/exclusion, OTA updates, network mapManual device management unavailable; automations unaffected if WS server is still up
MQTT Bridge (optional)Publishes Z-Wave node state to MQTT broker for other integrationsMQTT consumers (Node-RED, custom scripts) lose Z-Wave state; HomeAssistant via WS unaffected

Uptime History

DateIncident TypeDurationImpact
2025-07-14USB controller disconnect after host kernel update changed ttyACM ordering3 hoursAll Z-Wave nodes unreachable; Home Assistant reported integration unavailable
2025-10-03Z-Wave JS process OOM-killed on 512 MB device after large network cache growth45 minutesWebSocket server down; all Z-Wave automations failed silently
2025-12-19S2 security regression in driver version caused new device inclusions to fail6 hoursNew devices could not be added to mesh; existing nodes unaffected
2026-02-08MQTT bridge disconnected from broker after broker TLS certificate renewal2 hoursMQTT consumers lost Z-Wave state; WebSocket-based control remained functional

Monitor Z-Wave JS Automatically

Z-Wave JS failures are especially hard to detect because Home Assistant may show stale entity states rather than explicit errors — your lock appears "unlocked" in the UI while the Z-Wave network is completely down. External monitoring catches this where internal dashboards cannot. ezmon.com monitors your Z-Wave JS endpoints from multiple external probes and alerts your team via Slack, PagerDuty, or SMS the moment the WebSocket port stops accepting connections or the health endpoint returns a non-200 response.

Set up Z-Wave JS monitoring free at ezmon.com →

zwave-jszwavehome-assistanthome-automationiotstatus-checker