Skip to main content
Part of MajorDom — the next-gen smart home

integration-zwave

A MajorDom integration — bridges Z-Wave devices into the MajorDom language.

Built for the MajorDom Hub, but it doesn't need it: this is a standalone, standardized library for Z-Wave that you can use on its own (see Run it standalone below). Built on the MajorDom Integration SDK. The entry point is ZwaveController (majordom_zwave/controller.py), which the Hub — or the SDK's dev runner — instantiates and drives through its lifecycle: pairing → commands → teardown.

Documentation

Full integration-author docs — the controller lifecycle, data models, storing data, discovery, and a worked example — live at docs.majordom.io.

Development

poetry install && poetry run poe install
Task Description
poe check Full quality pipeline (ruff, ty, pytest, poetry build/check)
poe check --ci Same, plus git diff --exit-code

Work lands on develop; master is protected and released via Actions → Release.

Tests drive the controller with the SDK's test doubles against a mock Z-Wave network: zwave-js's own mock controller and mock devices, with a real zwave-js driver and a real zwave-js-server in front (tests/mock) — the integration connects to it exactly as to a real server, no radio or stick required. It needs Node.js 20+:

npm ci --prefix tests/mock && poetry run pytest

Command-class catalogue and canary

tests/test_catalogue.py accounts for every command class zwave-js knows (125 today): each is hosted by a mock device (a recipe in tests/mock/catalogue.mjs: 34 of them, all their capabilities switched on), is protocol plumbing, or is listed in tests/catalogue.py with the reason the mock cannot host it. Each hosted one must map exactly as EXPECTED says — name, type, role, visibility, unit, main parameter, reviewed value by value against the docs' parameter UX — pass the SDK's parameter_audit, and round-trip every writable value through the device. Seven more mock devices carry every variant zwave-js knows (88 sensor types and their scales, 14 binary sensors, 5 meters with all their scales, 22 notification types with all their states and events, 12 setpoints, 16 thermostat modes), checked against rules. Fast checks fail on a new command class, value metadata type or unit.

The monthly canary (.github/workflows/canary.yml) runs all of it against the latest zwave-js, zwave-js-server and zwave-js-server-python: red means upstream shipped something to support, or to explain.

Run it standalone (without the Hub)

majordom-zwave is a standalone library — import it into your own app, or run just this integration interactively (discover, pair, control, and inspect devices from a prompt) with no Hub. It needs a reachable zwave-js-server instance (see below).

See Standalone mode for the interactive CLI, watch mode, and the programmatic API.

Connecting to zwave-js-server

Unlike a radio-based integration, this one never talks to the Z-Wave stick directly. It connects over WebSocket, via zwave-js-server-python, to a running zwave-js-server process (Node.js) — the same server Home Assistant's Z-Wave JS integration uses. zwave-js-server is what actually owns the serial connection to the Z-Wave USB controller and speaks the Z-Wave protocol; this integration just talks JSON over a WebSocket to it.

  • Run zwave-js-server yourself (commonly via Docker) on whatever machine has the Z-Wave USB controller attached.
  • Point this integration at it with the ZWAVE_SERVER_URL environment variable (default ws://localhost:3000) — see majordom_zwave/config.py.
  • The integration keeps reconnecting (1 → 30 s backoff) while the server is unreachable; paired devices are reported unavailable meanwhile, with the reason as their last_error.

About this integration

  • Protocol / platform: Z-Wave, via zwave-js-server / zwave-js-server-python.

  • Transport(s): Z-Wave (sub-GHz RF mesh).

  • Supported devices: any Z-Wave / Z-Wave Plus device exposing standard Command Classes (Binary/Multilevel Switch, Indicator, etc.) that Z-Wave JS supports.

  • Credentials needed to pair: given when opening the pairing window (start_pairing_window), not at pair_device time — Z-Wave needs them during inclusion, before the device exists as a discovery; the discovery itself always expects CredentialsType.none.

    • code — the S2 PIN (the first 5 digits of the DSK on the device's label), or the whole DSK.
    • qr — the device's QR code (90…); malformed codes are rejected. An S2 code includes the device in the window; a SmartStart code provisions it instead: zwave-js includes it securely, with no PIN, whenever it is powered on (hours later, after installation), and it then arrives as a discovery. Unpairing removes it from the provisioning list, or it would join again.
    • none — the device joins, but without the S2 Authenticated / Access Control classes (they need the PIN); its discovery's last_error says so.
    • secret — not supported.

    The Hub does not pass credentials to start_pairing_window yet (the SDK's develop has the parameter).

Required harness

  • Hardware adapters: a Z-Wave USB controller (e.g. Aeotec/Zooz Z-Stick) — but it's attached to whatever host runs zwave-js-server, not necessarily the Hub. This integration doesn't use dependencies.hardware_interfaces; the radio is abstracted behind the server.
  • Third-party software services: zwave-js-server (Node.js) must be running and reachable at ZWAVE_SERVER_URL.
  • OS / permissions: network reachability (TCP) to the zwave-js-server host:port. No special permissions needed on the Hub's own host.

Protocol stack

Layer Protocol Implemented by
MajorDom integration maps Z-Wave devices/values ↔ MajorDom domain model this repo, always
Z-Wave JS Server API zwave-js-server's WebSocket/JSON-RPC protocol library (zwave-js-server-python)
Application Z-Wave Command Classes (CC) zwave-js-server (external Node.js service)
Network / MAC / PHY Z-Wave mesh, sub-GHz RF Z-Wave USB controller + its driver (harness)

Manual testing (real hardware)

Verified against real hardware, on the version before the rework of 2026-10 (the rework is tested against the mock network only; to be re-run on hardware before release):

  • Z-Wave controller: Z-Stick 7 (ZWA010)
  • Test device: HKZW-RGB01 v1.0 (RGB bulb)

Mapping

Z-Wave MajorDom
Device device_uuid("<home id>-<node id>") (SDK helper): node ids repeat across networks and are reused after a device leaves
Value One parameter, parameter_uuid(device, value id); a current/target pair (currentValue/targetValue, currentMode/targetMode, …) is one parameter: commanded through the target, state from the current one
Multilevel Switch / Basic level, Window Covering position (0–99) Percentage 0–100 (99 is 100)
Multilevel Sensor, Meter, setpoints, Energy Production decimal (scaled decimals by spec), whatever the first reading
states enum with labels in valid_values (integer when the device allows other values too, or when they only label special values: sound volume, wake-up interval)
Write-only action ("Reset", "Identify", "Restore previous value") none: a button
Units Every zwave-js scale unit is mapped, converted (°F → °C, imperial → metric, Wh → kWh, mV → V, …) or deliberately plain; a few CCs give units only in labels (Door Lock times, wake-up interval)
Central Scene, Scene Activation, stateful: false event role (sent by zwave-js as "value notification"); their settings stay controls
Notifications (zwave-js "notification" events) Read-only event parameters: Keypad (the event type: Enter, Away, …) and Keypad code (what was typed, system); one <type> event per Notification CC type with events (Access control, Home security, …) and its details (user id…, JSON, system); a remote's Level change; Battery replacement; Powerlevel test. Enum labels are what the device announced; events it did not are dropped
Visibility "When in doubt, hide it": a curated list of everyday controls and live readings is user (switches, levels, colour, sensors, meters, locks, thermostats, covers, garage doors, battery, …); every other value of a reviewed CC is a setting; wiring, metadata, secrets, durations and opaque data are system, and so is every value of a CC nobody reviewed (still usable by automations and the API)
Names Written for people, not zwave-js's labels: Power, Level, Color, Lock, Electric energy (not Electric Consumption [kWh]), Motion (not Sensor state (Motion)), Heating setpoint, Scene 1; a name several values share gets what tells them apart (Sensor status (Water alarm)); camelCase enum labels become words (Unsecured with timeout)
Unsupervised set (delivered, not confirmed) Read back from the device ~6 s later unless zwave-js already did: the Hub sees what the device did
Main parameter Binary Switch (toggle) › Multilevel Switch (off / 100 %) › Barrier (closed / open) › Door Lock (unsecured / secured) › Lock (toggle) › Window Covering (closed / open) › Basic, when it is the only control (off / 100 %)

Progress

Implementation — makes the integration functional:

  • Discovery of joining devices ("node added"), and of nodes already on the network but not paired
  • Discovery of already-paired devices on (re)connect — matched by home id and node id, and checked against the device's fingerprint (a node id reused by another device is not taken for the paired one)
  • start_pairing_window (default, S2 PIN or DSK, S2 QR, SmartStart)
  • Device pairing (completes the device the Hub created; fails cleanly and stays discovered when the device does not finish its interview)
  • Device schema mapped (see Mapping)
  • Hub → Device control (send_command); the state comes back as the device's report, never echoed
  • Device → Hub events ("value updated", "value notification", "notification": keypads, Notification CC events, remote dimming, low battery, radio tests)
  • identify — Indicator CC v3 identify; a light blinks; any other device is left alone (a relay or a motor may drive a heater, a pump or a blind)
  • unpair — a failed node is removed without the device; a live one when this device confirms the exclusion
  • fetch
  • Availability — "dead"/"alive", the server connection, a device removed with another tool
  • Graceful shutdown in stop
  • Tests pass against a simulated network (mock zwave-js controller and devices)

Quality — makes it reliable and maintainable (the bar for release):

  • Recovers automatically from a lost or restarted zwave-js-server (reconnects with backoff)
  • No exception escapes background work — event handlers run as logged tasks; Hub calls raise after recording the reason
  • Failures are surfaced, not swallowed — the reason is the device's or discovery's last_error; an unreachable server at start is reported through controller_did_encounter_error
  • Re-authenticates automatically — not applicable (Z-Wave has no expiring credential)
  • Fully asynchronous
  • Stable identity — the SDK helpers, scoped by the network's home id
  • End-to-end tests drive pair → command → fetch → events → unpair against the mock network
  • Failure paths tested — unreachable or lost server, unresponsive and refusing devices, interview timeout, exclusion timeout, the wrong device excluded, a reused node id, a missing PIN
  • Broad device coverage — the catalogue hosts 33 command classes (switches, dimmers, lights, sensors, meters, locks, thermostats, covers, garage doors, scene controllers, sirens, user codes, child locks, humidifiers, keypads, smoke/CO alarms, sleeping devices, …); S2 by PIN, DSK, QR and SmartStart. The others are plumbing or explained (see Command-class catalogue and canary)
  • Fully typed (ty) and clean (poe check)
  • Readable & structured — mapping in mapper.py, static tables in zwave_spec.py
  • Efficient — subscription-based, not polling
  • Diagnosable — per-area log tags ([PAIR], [CONNECT], …), reasons in last_error, causes chained
  • Rich parameter metadata — role, visibility, units, limits, enum labels, main parameter; names and buckets reviewed against the docs' parameter UX for every hosted CC, and audited (parameter_audit)
  • Owned — add a listed maintainer
  • nice-to-have: localizable naming · firmware updates · user-facing supported-device docs

Notes

  • Z-Wave discovery doesn't use zeroconf/ssdp/ble: nodes are surfaced from zwave-js-server's own events.
  • Command classes zwave-js's mock cannot host (rare ones: tariffs, IP gateways, AV, schedules, irrigation) are not reviewed: their values map by the generic rules and are hidden (system), still usable by automations and the API.
  • What zwave-js's mock cannot do, so the catalogue does not round-trip it: decode a Color Switch Set, keep an Indicator's state; its Thermostat Setpoint Set refuses every value above the minimum, so the harness replaces it.

License

See LICENSE. For commercial licensing or partnership inquiries regarding MajorDom, contact us via parker-industries.org/partnership.

Metadata

Release files for majordom-zwave 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for majordom-zwave 0.1.1
File Size Uploaded
majordom_zwave-0.1.1.tar.gz 36.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for majordom-zwave 0.1.1
File Interpreter ABI Platform
majordom_zwave-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 68.7 kB

Release files / majordom_zwave-0.1.1.tar.gz

Download URL majordom_zwave-0.1.1.tar.gz
Size 36.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f45a4fce246c1ad17e2bd369712d1d8b17528b2cc326c60cd4d17ba67d7189d8
BLAKE2b-256 checksum
How to use checksums
446b83b5a10dd9eaf87fb7f12910291a33011901e822b97c19e4769c49847c89
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release files / majordom_zwave-0.1.1-py3-none-any.whl

Download URL majordom_zwave-0.1.1-py3-none-any.whl
Size 32.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cb5c3aa0a6563c8917ca9c086b9af62afcc8bae164e4584777414ae86efd507c
BLAKE2b-256 checksum
How to use checksums
03f88d3a0dfe21d9e3453fb4a9c15d48b36b4d585f20ef0b3d92b9b69264798a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.2

2 release files

This release

0.1.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page