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.
- Other protocols: browse the MajorDom integrations.
- Create your own: start from the integration template.
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-serveryourself (commonly via Docker) on whatever machine has the Z-Wave USB controller attached. - Point this integration at it with the
ZWAVE_SERVER_URLenvironment variable (defaultws://localhost:3000) — seemajordom_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 atpair_devicetime — Z-Wave needs them during inclusion, before the device exists as a discovery; the discovery itself always expectsCredentialsType.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_errorsays so. secret— not supported.
The Hub does not pass credentials to
start_pairing_windowyet (the SDK'sdevelophas 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 usedependencies.hardware_interfaces; the radio is abstracted behind the server. - Third-party software services:
zwave-js-server(Node.js) must be running and reachable atZWAVE_SERVER_URL. - OS / permissions: network reachability (TCP) to the
zwave-js-serverhost: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 throughcontroller_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 →
unpairagainst 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 inzwave_spec.py - Efficient — subscription-based, not polling
- Diagnosable — per-area log tags (
[PAIR],[CONNECT], …), reasons inlast_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 fromzwave-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)
| File | Size | Uploaded | |
|---|---|---|---|
| majordom_zwave-0.1.1.tar.gz | 36.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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