homebridge-zwave-usb 1.0.3 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,76 +1,33 @@
1
- # Homebridge Z-Wave USB
1
+ # Homebridge Z-Wave USB (Z-Wave JS UI Client)
2
2
 
3
3
  > [!CAUTION]
4
- > **ALPHA SOFTWARE:** This plugin is in early development. It is **NOT READY FOR PRODUCTION USE**. Expect breaking changes, bugs, and missing features. Use at your own risk.
4
+ > **ALPHA SOFTWARE:** This plugin is in early development. Expect bugs and missing features.
5
5
 
6
6
  [![Build and Test](https://github.com/sapireli/homebridge-zwave-usb/actions/workflows/build.yml/badge.svg)](https://github.com/user/homebridge-zwave-usb/actions/workflows/build.yml)
7
7
 
8
- A high-performance, native HomeKit integration for Z-Wave networks.
9
-
10
- Unlike legacy plugins, **Homebridge Z-Wave USB** is built on the modern **Z-Wave JS** driver (the same stack used by Home Assistant), offering unmatched speed, reliability, and device support.
8
+ A high-performance HomeKit integration for Z-Wave networks, designed to work exclusively as a client for **Z-Wave JS UI**.
11
9
 
12
10
  ## Why this plugin?
13
11
 
14
- * 🚀 **Performance:** Direct communication with your USB stick. No intermediate MQTT brokers or slow translation layers.
15
- * 🔌 **Flexibility:** Run it standalone ("Direct Mode") OR connect it to an existing **Z-Wave JS UI** instance ("Remote Mode").
12
+ * 🚀 **Performance:** Direct WebSocket communication with Z-Wave JS UI.
13
+ * 🔌 **Decoupled:** Keep your Z-Wave network running even when Homebridge restarts.
16
14
  * 🏠 **Native Feel:** Devices appear in Apple Home exactly as you expect. Multi-sensor devices are grouped logically.
17
- * 🔐 **Secure:** Full support for S2 Security (Locks, Garage Doors) with auto-generated keys.
15
+ * 🔐 **Manage Anywhere:** Use the powerful Z-Wave JS UI control panel for inclusion, exclusion, and network mapping.
18
16
 
19
17
  ---
20
18
 
21
- ## 🛠️ One Plugin, Three Architectures
22
-
23
- Choose the setup that fits your needs:
19
+ ## 📦 Prerequisites
24
20
 
25
- | Mode | Best For... | Description |
26
- | :--- | :--- | :--- |
27
- | **Direct** | ⚡ **Speed & Simplicity** | The plugin owns the USB stick. Zero external dependencies. Ideal for Raspberry Pi setups. |
28
- | **Remote** | 🐳 **Docker / NAS** | Connects to **Z-Wave JS UI** running in a separate container. Perfect if you want the advanced Z-Wave control panel. |
29
- | **Host** | 🎛️ **Hybrid** | The plugin runs the Z-Wave network but exposes a server for **Z-Wave JS UI** to connect to for management/graphs. |
30
-
31
- ---
32
-
33
- ## 📦 Installation
34
-
35
- 1. **Install Homebridge**: [Official Instructions](https://github.com/homebridge/homebridge/wiki).
36
- 2. **Install Plugin**:
37
- ```bash
38
- npm install -g homebridge-zwave-usb
39
- ```
40
- 3. **Permissions (Linux/Pi)**: Ensure the user running Homebridge has access to serial ports:
41
- ```bash
42
- sudo usermod -a -G dialout $(whoami)
43
- ```
21
+ 1. **Z-Wave JS UI**: You must have an instance of [Z-Wave JS UI](https://zwave-js.github.io/zwave-js-ui/) running.
22
+ 2. **Enable WS Server**: In Z-Wave JS UI, go to **Settings** -> **Home Assistant** (or **WS Server**) and ensure the **WebSocket Server** is enabled (usually on port 3000).
44
23
 
45
24
  ---
46
25
 
47
26
  ## ⚙️ Configuration
48
27
 
49
- ### Option A: Direct Mode (Recommended)
50
- *The simplest way to get started.*
51
-
52
28
  1. Go to **Settings** in Homebridge UI.
53
- 2. **Serial Port**: Enter the persistent path to your stick.
54
- > [!WARNING]
55
- > **Do not use `/dev/ttyACM0`**. Use the stable path found in `/dev/serial/by-id/...`
56
- 3. **Security Keys**: Click the **"Auto-Generate Keys"** button.
57
- 4. **Save & Restart**.
58
-
59
- ### Option B: Remote Mode (Z-Wave JS UI)
60
- *Connect to an existing Z-Wave JS UI instance.*
61
-
62
- 1. Ensure **Z-Wave JS UI** is running and "WS Server" is enabled (Settings -> Home Assistant / WS).
63
- 2. In Homebridge Plugin Settings:
64
- * **Serial Port**: Enter the WebSocket URL (e.g., `ws://localhost:3000` or `ws://192.168.1.50:3000`).
65
- 3. **Keys**: Leave blank. (Keys are managed inside Z-Wave JS UI).
66
- 4. **Save & Restart**.
67
-
68
- ### Option C: Host Mode
69
- *Homebridge runs the network, but you want to view the graph in Z-Wave JS UI.*
70
-
71
- 1. Follow "Direct Mode" setup.
72
- 2. Check **"Enable Z-Wave Server"**.
73
- 3. In **Z-Wave JS UI**, set the driver mode to "Remote" and point it to `ws://YOUR_HOMEBRIDGE_IP:3000`.
29
+ 2. **Z-Wave JS UI WebSocket URL**: Enter the URL of your Z-Wave JS UI instance (e.g., `ws://192.168.1.50:3000`).
30
+ 3. **Save & Restart**.
74
31
 
75
32
  ---
76
33
 
@@ -96,45 +53,20 @@ The plugin automatically maps Z-Wave capabilities to Apple HomeKit services:
96
53
 
97
54
  ## 🎮 Managing Your Network
98
55
 
99
- This plugin exposes a **"Z-Wave Controller"** accessory in Apple Home. You can use it to manage your mesh right from your phone:
56
+ This plugin exposes a **"Z-Wave Controller"** accessory in Apple Home. You can use it to manage basic operations:
100
57
 
101
58
  * **Inclusion Mode:** Turn ON to pair a new device (timeout: 60s).
102
59
  * **Exclusion Mode:** Turn ON to unpair/remove a device.
103
- * **Heal Network:** Turn ON to rebuild mesh routes (do this after moving devices).
104
-
105
- ---
60
+ * **Heal Network:** Turn ON to rebuild mesh routes.
106
61
 
107
- ## Troubleshooting
62
+ For advanced management (firmware updates, configuration parameters, network graphs), use the **Z-Wave JS UI** web interface.
108
63
 
109
- * **Device not showing up?** Battery devices sleep. Wake them up (press the button) repeatedly after pairing until the interview completes.
110
- * **Port Busy Error?** Ensure no other software (like Z-Wave JS UI) is trying to use the USB stick at the same time. If you want both, use **Remote Mode**.
111
- * **Secure Inclusion Failed?** Ensure you have generated Security Keys in the settings.
64
+ ---
112
65
 
113
66
  ## Contributing
114
67
 
115
68
  Pull requests are welcome! Please ensure all changes pass `npm test` and `npm run build`.
116
69
 
117
- ## Legal & Disclaimer
118
-
119
- **Homebridge Z-Wave USB** is an independent project and is not affiliated with, endorsed by, or sponsored by any of the following trademark owners:
120
-
121
- - **Apple, HomeKit, and Apple Home** are trademarks of Apple Inc., registered in the U.S. and other countries.
122
- - **Z-Wave and Z-Wave Plus** are registered trademarks of Silicon Labs and its subsidiaries.
123
- - **Homebridge** is a registered trademark of the Homebridge project.
124
- - **Aeotec, Zooz, and Home Assistant** are trademarks of their respective owners.
125
-
126
- This project is **not affiliated with, maintained, or supported by** the official **Z-Wave JS** or **Z-Wave JS UI** (formerly zwavejs2mqtt) projects. It is an independent wrapper for Homebridge.
127
-
128
- **DISCLAIMER:**
129
- This software is provided "as is", without warranty of any kind. The authors are not responsible for any damage, data loss, or security breaches that may occur.
130
- **You use this software at your own risk.**
131
- This includes, but is not limited to, the risk of:
132
- - Smart locks unlocking unexpectedly.
133
- - Z-Wave devices failing to respond.
134
- - Loss of configuration data.
135
-
136
- Always ensure you have backups of your Z-Wave controller keys and Homebridge configuration.
137
-
138
70
  ## License
139
71
 
140
72
  MIT
@@ -18,12 +18,12 @@
18
18
  "default": "ZWaveUSB",
19
19
  "const": "ZWaveUSB"
20
20
  },
21
- "serialPort": {
22
- "title": "Serial Port or WebSocket URL",
23
- "type": "string",
24
- "required": true,
25
- "description": "Path to the serial port (e.g., /dev/ttyACM0) or WebSocket URL (e.g., ws://localhost:3000)."
26
- },
21
+ "serialPort": {
22
+ "title": "Serial Port Path",
23
+ "type": "string",
24
+ "required": true,
25
+ "description": "Path to your Z-Wave USB stick (e.g., /dev/ttyACM0 or /dev/serial/by-id/...)."
26
+ },
27
27
  "securityKeys": {
28
28
  "title": "Security Keys",
29
29
  "type": "object",
@@ -34,7 +34,7 @@
34
34
  "minLength": 32,
35
35
  "maxLength": 32,
36
36
  "pattern": "^[0-9a-fA-F]+$",
37
- "description": "16-byte hex string (32 characters). REQUIRED for legacy secure devices."
37
+ "description": "16-byte hex string (32 characters)."
38
38
  },
39
39
  "S2_Unauthenticated": {
40
40
  "title": "S2 Unauthenticated Key",
@@ -62,38 +62,19 @@
62
62
  }
63
63
  }
64
64
  },
65
- "inclusionTimeoutSeconds": {
66
- "title": "Inclusion Timeout (Seconds)",
67
- "type": "integer",
68
- "default": 60,
69
- "minimum": 30,
70
- "maximum": 300,
71
- "description": "How long the inclusion mode stays active."
72
- },
73
- "enableServer": {
74
- "title": "Enable Z-Wave Server (Host Mode)",
75
- "type": "boolean",
76
- "default": false,
77
- "description": "Expose the Z-Wave Driver via WebSocket. Allows Z-Wave JS UI to connect to this plugin."
78
- },
79
- "serverPort": {
80
- "title": "Server Port",
81
- "type": "integer",
82
- "default": 3000,
83
- "minimum": 1024,
84
- "maximum": 65535,
85
- "description": "Port for the Z-Wave WebSocket Server (only if enabled).",
86
- "condition": {
87
- "function": {
88
- "enableServer": true
89
- }
90
- }
91
- },
92
- "debug": {
93
- "title": "Enable Debug Logging",
65
+ "inclusionTimeoutSeconds": {
66
+ "title": "Inclusion Timeout (Seconds)",
67
+ "type": "integer",
68
+ "default": 60,
69
+ "minimum": 30,
70
+ "maximum": 300,
71
+ "description": "How long the inclusion mode stays active."
72
+ },
73
+ "debug": {
74
+ "title": "Enable Verbose Driver Logging",
94
75
  "type": "boolean",
95
76
  "default": false,
96
- "description": "If true, enables verbose logging for troubleshooting."
77
+ "description": "If true, redirects all Z-Wave JS driver logs to Homebridge."
97
78
  }
98
79
  }
99
80
  },
@@ -101,13 +82,13 @@
101
82
  "name",
102
83
  {
103
84
  "key": "serialPort",
104
- "description": "WARNING: Do not use /dev/ttyUSBX or /dev/ttyACMX serial devices, as those mappings can change over time. Instead, use the /dev/serial/by-id/X persistent path for your Z-Wave stick. On macOS, use /dev/tty.usbmodem..."
85
+ "description": "WARNING: Use persistent paths like /dev/serial/by-id/... on Linux or /dev/tty.usbmodem... on macOS."
105
86
  },
106
87
  {
107
88
  "type": "fieldset",
108
89
  "title": "Z-Wave Security Keys",
109
90
  "expandable": true,
110
- "description": "These keys are used to connect securely to compatible devices. \n\nDirect/Host Mode: Use the 'Auto-Generate Keys' button in the settings UI.\n\nRemote Mode: These keys are IGNORED (set them in Z-Wave JS UI instead).\n\nWARNING: Backup your keys! If you lose them, you will have to factory reset your devices to re-pair them.",
91
+ "description": "Required for secure devices (Locks, Sensors). Use the 'Auto-Generate' button in settings UI.",
111
92
  "items": [
112
93
  "securityKeys.S0_Legacy",
113
94
  "securityKeys.S2_Unauthenticated",
@@ -28,12 +28,18 @@ class ControllerAccessory {
28
28
  this.inclusionService =
29
29
  this.platformAccessory.getService('Inclusion Mode') ||
30
30
  this.platformAccessory.addService(this.platform.Service.Switch, 'Inclusion Mode', 'Inclusion');
31
+ this.inclusionService.setCharacteristic(this.platform.Characteristic.Name, 'Inclusion Mode');
32
+ this.inclusionService.setCharacteristic(this.platform.Characteristic.ConfiguredName, 'Inclusion Mode');
31
33
  this.exclusionService =
32
34
  this.platformAccessory.getService('Exclusion Mode') ||
33
35
  this.platformAccessory.addService(this.platform.Service.Switch, 'Exclusion Mode', 'Exclusion');
36
+ this.exclusionService.setCharacteristic(this.platform.Characteristic.Name, 'Exclusion Mode');
37
+ this.exclusionService.setCharacteristic(this.platform.Characteristic.ConfiguredName, 'Exclusion Mode');
34
38
  this.healService =
35
39
  this.platformAccessory.getService('Heal Network') ||
36
40
  this.platformAccessory.addService(this.platform.Service.Switch, 'Heal Network', 'Heal');
41
+ this.healService.setCharacteristic(this.platform.Characteristic.Name, 'Heal Network');
42
+ this.healService.setCharacteristic(this.platform.Characteristic.ConfiguredName, 'Heal Network');
37
43
  this.inclusionService
38
44
  .getCharacteristic(this.platform.Characteristic.On)
39
45
  .onSet(this.handleSetInclusion.bind(this))
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZWaveUsbPlatform = void 0;
4
4
  const ZWaveController_1 = require("../zwave/ZWaveController");
5
- const ZWaveRemoteController_1 = require("../zwave/ZWaveRemoteController");
6
5
  const AccessoryFactory_1 = require("../accessories/AccessoryFactory");
7
6
  const ControllerAccessory_1 = require("../accessories/ControllerAccessory");
8
7
  class ZWaveUsbPlatform {
@@ -23,33 +22,15 @@ class ZWaveUsbPlatform {
23
22
  this.Characteristic = this.api.hap.Characteristic;
24
23
  this.log.debug('Finished initializing platform:', this.config.name);
25
24
  if (!this.config.serialPort) {
26
- this.log.error('No serial port or WebSocket URL configured. Please set "serialPort" in your configuration.');
25
+ this.log.error('No serial port configured. Please set "serialPort" in your configuration.');
27
26
  return;
28
27
  }
29
- // Initialize controller with config
30
- if (this.config.serialPort.startsWith('ws://') || this.config.serialPort.startsWith('wss://')) {
31
- this.log.info(`Initializing Remote Controller connecting to ${this.config.serialPort}`);
32
- if (this.config.securityKeys) {
33
- this.log.info('Note: Security keys configured in Homebridge are IGNORED in Remote Mode. Ensure keys are set in Z-Wave JS UI.');
34
- }
35
- this.zwaveController = new ZWaveRemoteController_1.ZWaveRemoteController(this.log, this.config.serialPort, {
36
- debug: this.config.debug
37
- });
38
- }
39
- else {
40
- this.log.info(`Initializing Local Driver on ${this.config.serialPort}`);
41
- if (!this.config.securityKeys) {
42
- this.log.warn('WARNING: No security keys configured! Secure devices (Locks, Garage Doors, etc.) will NOT pair securely. Please generate keys in Settings.');
43
- }
44
- this.zwaveController = new ZWaveController_1.ZWaveController(this.log, this.config.serialPort, {
45
- securityKeys: this.config.securityKeys,
46
- debug: this.config.debug,
47
- server: {
48
- enabled: this.config.enableServer,
49
- port: this.config.serverPort || 3000
50
- },
51
- });
52
- }
28
+ this.log.info(`Initializing Z-Wave Local Driver on ${this.config.serialPort}`);
29
+ this.zwaveController = new ZWaveController_1.ZWaveController(this.log, this.config.serialPort, {
30
+ securityKeys: this.config.securityKeys,
31
+ debug: this.config.debug,
32
+ storagePath: this.api.user.storagePath(),
33
+ });
53
34
  // Setup listeners
54
35
  this.zwaveController.on('node added', (node) => this.handleNodeAdded(node));
55
36
  this.zwaveController.on('node ready', (node) => this.handleNodeReady(node));
@@ -57,9 +38,7 @@ class ZWaveUsbPlatform {
57
38
  this.zwaveController.on('value updated', (node) => this.handleValueUpdated(node));
58
39
  this.api.on('didFinishLaunching', async () => {
59
40
  this.log.debug('Executed didFinishLaunching callback');
60
- if (this.config.serialPort) {
61
- await this.connectToZWaveController();
62
- }
41
+ await this.connectToZWaveController();
63
42
  });
64
43
  this.api.on('shutdown', async () => {
65
44
  this.log.info('Shutting down Z-Wave controller...');
@@ -80,13 +59,34 @@ class ZWaveUsbPlatform {
80
59
  await this.zwaveController.start();
81
60
  this.log.info('Z-Wave controller connected successfully.');
82
61
  this.controllerAccessory = new ControllerAccessory_1.ControllerAccessory(this, this.zwaveController);
62
+ // Reconciliation: Remove orphaned cached accessories
63
+ setTimeout(() => {
64
+ const managedUuids = new Set();
65
+ if (this.controllerAccessory) {
66
+ managedUuids.add(this.controllerAccessory.platformAccessory.UUID);
67
+ }
68
+ for (const acc of this.zwaveAccessories.values()) {
69
+ managedUuids.add(acc.platformAccessory.UUID);
70
+ }
71
+ const orphaned = this.accessories.filter(acc => !managedUuids.has(acc.UUID));
72
+ if (orphaned.length > 0) {
73
+ this.log.info(`Removing ${orphaned.length} orphaned accessories from cache...`);
74
+ this.api.unregisterPlatformAccessories('homebridge-zwave-usb', 'ZWaveUSB', orphaned);
75
+ for (const orphan of orphaned) {
76
+ const index = this.accessories.indexOf(orphan);
77
+ if (index !== -1) {
78
+ this.accessories.splice(index, 1);
79
+ }
80
+ }
81
+ }
82
+ }, 5000);
83
83
  }
84
84
  catch (err) {
85
85
  this.log.error('Failed to connect to Z-Wave controller:', err);
86
86
  }
87
87
  }
88
88
  handleNodeAdded(node) {
89
- this.log.info(`Node ${node.nodeId} added.`);
89
+ this.log.info(`Node ${node.nodeId} added to the network. Waiting for interview to complete...`);
90
90
  }
91
91
  handleNodeReady(node) {
92
92
  this.log.info(`Node ${node.nodeId} ready`);
@@ -95,16 +95,12 @@ class ZWaveUsbPlatform {
95
95
  this.log.error(`Cannot create accessory for Node ${node.nodeId} - Home ID not available`);
96
96
  return;
97
97
  }
98
- // Check if accessory already exists
99
- let accessory = this.zwaveAccessories.get(node.nodeId);
100
- // 2. Check if we already have this accessory wrapper in memory
101
98
  if (this.zwaveAccessories.has(node.nodeId)) {
102
99
  this.zwaveAccessories.get(node.nodeId)?.refresh();
103
100
  return;
104
101
  }
105
- accessory = AccessoryFactory_1.AccessoryFactory.create(this, node, homeId);
102
+ const accessory = AccessoryFactory_1.AccessoryFactory.create(this, node, homeId);
106
103
  this.zwaveAccessories.set(node.nodeId, accessory);
107
- // Metadata Sync
108
104
  const nodeName = node.name || node.deviceConfig?.label || `Node ${node.nodeId}`;
109
105
  if (accessory.platformAccessory.displayName !== nodeName) {
110
106
  this.log.info(`Renaming accessory ${accessory.platformAccessory.displayName} -> ${nodeName}`);
@@ -4,23 +4,19 @@ import { EventEmitter } from 'events';
4
4
  import { IZWaveController } from './interfaces';
5
5
  export interface ZWaveControllerOptions {
6
6
  debug?: boolean;
7
+ storagePath?: string;
7
8
  securityKeys?: {
8
9
  S0_Legacy?: string;
9
10
  S2_Unauthenticated?: string;
10
11
  S2_Authenticated?: string;
11
12
  S2_AccessControl?: string;
12
13
  };
13
- server?: {
14
- enabled: boolean;
15
- port: number;
16
- };
17
14
  }
18
15
  export declare class ZWaveController extends EventEmitter implements IZWaveController {
19
16
  private readonly log;
20
17
  private readonly serialPort;
21
18
  private readonly options;
22
19
  private driver;
23
- private server;
24
20
  readonly nodes: Map<number, ZWaveNode>;
25
21
  private nodeListeners;
26
22
  private controllerListeners;
@@ -2,17 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZWaveController = void 0;
4
4
  const zwave_js_1 = require("zwave-js");
5
- const server_1 = require("@zwave-js/server");
6
5
  const events_1 = require("events");
7
6
  class ZWaveController extends events_1.EventEmitter {
8
7
  log;
9
8
  serialPort;
10
9
  options;
11
10
  driver;
12
- server;
13
11
  nodes = new Map();
14
- // Store listeners for cleanup
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
12
  nodeListeners = new Map();
17
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
14
  controllerListeners = {};
@@ -36,12 +32,35 @@ class ZWaveController extends events_1.EventEmitter {
36
32
  securityKeys.S2_AccessControl = Buffer.from(this.options.securityKeys.S2_AccessControl, 'hex');
37
33
  }
38
34
  }
39
- const logLevel = this.options.debug ? 'debug' : 'info';
35
+ // Redirect Z-Wave JS logs to Homebridge
36
+ const customLogTransport = {
37
+ write: (info) => {
38
+ const message = info.message || info;
39
+ const label = info.label ? `[${info.label}] ` : '';
40
+ const output = `${label}${message}`;
41
+ if (this.options.debug || info.level === 'error' || info.level === 'warn') {
42
+ this.log.info(`[Z-Wave JS] ${output}`);
43
+ }
44
+ else {
45
+ this.log.debug(`[Z-Wave JS] ${output}`);
46
+ }
47
+ }
48
+ };
49
+ const logConfig = {
50
+ enabled: true,
51
+ level: this.options.debug ? 'debug' : 'info',
52
+ transports: [customLogTransport] // eslint-disable-line @typescript-eslint/no-explicit-any
53
+ };
54
+ const storagePath = this.options.storagePath || process.cwd();
40
55
  this.driver = new zwave_js_1.Driver(this.serialPort, {
41
56
  securityKeys: Object.keys(securityKeys).length > 0 ? securityKeys : undefined,
42
- logConfig: {
43
- level: logLevel,
44
- logToFile: !!this.options.debug,
57
+ logConfig,
58
+ storage: {
59
+ cacheDir: storagePath + '/zwave-js-cache',
60
+ deviceConfigPriorityDir: storagePath + '/zwave-js-config',
61
+ },
62
+ features: {
63
+ softReset: false,
45
64
  },
46
65
  });
47
66
  this.driver.on('error', (err) => {
@@ -101,11 +120,30 @@ class ZWaveController extends events_1.EventEmitter {
101
120
  return;
102
121
  }
103
122
  this.nodes.set(node.nodeId, node);
104
- const onReady = () => this.emit('node ready', node);
105
- const onValueUpdated = () => this.emit('value updated', node);
106
- this.nodeListeners.set(node.nodeId, { ready: onReady, value: onValueUpdated });
123
+ const onReady = () => {
124
+ this.log.info(`Node ${node.nodeId} is ready (Interview Stage: ${node.interviewStage})`);
125
+ this.emit('node ready', node);
126
+ };
127
+ const onValueUpdated = (_args) => {
128
+ this.emit('value updated', node);
129
+ };
130
+ const onInterviewStageCompleted = (stage) => {
131
+ this.log.info(`Node ${node.nodeId} interview stage completed: ${stage}`);
132
+ };
133
+ const onInterviewFailed = (args) => {
134
+ this.log.error(`Node ${node.nodeId} interview failed: ${args.errorMessage}`);
135
+ };
136
+ this.nodeListeners.set(node.nodeId, {
137
+ ready: onReady,
138
+ value: onValueUpdated,
139
+ interviewStageCompleted: onInterviewStageCompleted,
140
+ interviewFailed: onInterviewFailed
141
+ });
107
142
  node.on('ready', onReady);
108
143
  node.on('value updated', onValueUpdated);
144
+ node.on('interview stage completed', onInterviewStageCompleted);
145
+ node.on('interview failed', onInterviewFailed);
146
+ this.log.info(`Node ${node.nodeId} added (Interview Stage: ${node.interviewStage})`);
109
147
  if (node.ready) {
110
148
  this.emit('node ready', node);
111
149
  }
@@ -115,6 +153,12 @@ class ZWaveController extends events_1.EventEmitter {
115
153
  if (listeners) {
116
154
  node.off('ready', listeners.ready);
117
155
  node.off('value updated', listeners.value);
156
+ if (listeners.interviewStageCompleted) {
157
+ node.off('interview stage completed', listeners.interviewStageCompleted);
158
+ }
159
+ if (listeners.interviewFailed) {
160
+ node.off('interview failed', listeners.interviewFailed);
161
+ }
118
162
  this.nodeListeners.delete(node.nodeId);
119
163
  }
120
164
  this.nodes.delete(node.nodeId);
@@ -134,30 +178,9 @@ class ZWaveController extends events_1.EventEmitter {
134
178
  await this.driver.start();
135
179
  this.log.info('Z-Wave driver started');
136
180
  await readyPromise;
137
- if (this.options.server?.enabled) {
138
- try {
139
- this.log.info(`Starting Z-Wave JS Server (Host Mode) on port ${this.options.server.port}...`);
140
- this.server = new server_1.ZwavejsServer(this.driver, {
141
- port: this.options.server.port,
142
- logger: this.log,
143
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
- });
145
- await this.server.start();
146
- this.log.info('Z-Wave JS Server started successfully.');
147
- }
148
- catch (err) {
149
- this.log.error('Failed to start Z-Wave JS Server:', err);
150
- }
151
- }
152
181
  }
153
182
  async stop() {
154
- // 1. Stop Server
155
- if (this.server) {
156
- this.log.info('Stopping Z-Wave JS Server...');
157
- await this.server.destroy();
158
- this.server = undefined;
159
- }
160
- // 2. Remove Node Listeners
183
+ // 1. Remove Node Listeners
161
184
  for (const [nodeId, node] of this.nodes) {
162
185
  const listeners = this.nodeListeners.get(nodeId);
163
186
  if (listeners) {
@@ -166,10 +189,9 @@ class ZWaveController extends events_1.EventEmitter {
166
189
  }
167
190
  }
168
191
  this.nodeListeners.clear();
169
- // 3. Remove Controller/Driver Listeners
192
+ // 2. Remove Controller/Driver Listeners
170
193
  this.driver.removeAllListeners();
171
194
  try {
172
- // Use a property check to avoid throwing if driver is not ready
173
195
  if (this.driver.controller) {
174
196
  for (const [event, listener] of Object.entries(this.controllerListeners)) {
175
197
  this.driver.controller.off(event, listener); // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -179,10 +201,10 @@ class ZWaveController extends events_1.EventEmitter {
179
201
  catch {
180
202
  // Ignore errors if controller is not available
181
203
  }
182
- // 4. Destroy Driver
204
+ // 3. Destroy Driver
183
205
  await this.driver.destroy();
184
206
  this.log.info('Z-Wave driver stopped');
185
- // 5. Clear Local State
207
+ // 4. Clear Local State
186
208
  this.nodes.clear();
187
209
  this.removeAllListeners();
188
210
  }
@@ -18,6 +18,7 @@ export interface IZWaveNode {
18
18
  label?: string;
19
19
  };
20
20
  ready: boolean;
21
+ interviewStage: any;
21
22
  getValue(valueId: any): any;
22
23
  setValue(valueId: any, value: any): Promise<boolean | undefined>;
23
24
  supportsCC(cc: number): boolean;
@@ -5,8 +5,8 @@
5
5
  <input type="text" class="form-control" id="name" placeholder="Z-Wave USB">
6
6
  </div>
7
7
  <div class="form-group">
8
- <label for="serialPort">Serial Port or WebSocket URL</label>
9
- <input type="text" class="form-control" id="serialPort" placeholder="/dev/ttyACM0 or ws://localhost:3000" required>
8
+ <label for="serialPort">Serial Port Path</label>
9
+ <input type="text" class="form-control" id="serialPort" placeholder="/dev/ttyACM0" required>
10
10
  <small class="form-text text-muted">
11
11
  WARNING: Do not use /dev/ttyUSB0. Use /dev/serial/by-id/...
12
12
  </small>
@@ -15,7 +15,7 @@
15
15
  <fieldset>
16
16
  <legend>Z-Wave Security Keys</legend>
17
17
  <p class="text-muted small">
18
- Required for Direct/Host Mode. Ignored for Remote Mode.
18
+ Required for secure devices (Locks, Sensors).
19
19
  </p>
20
20
  <button type="button" class="btn btn-outline-primary btn-sm mb-3" id="generateKeys">
21
21
  Auto-Generate Keys
@@ -46,19 +46,9 @@
46
46
  <input type="number" class="form-control" id="inclusionTimeoutSeconds" placeholder="60">
47
47
  </div>
48
48
 
49
- <div class="form-check">
50
- <input type="checkbox" class="form-check-input" id="enableServer">
51
- <label class="form-check-label" for="enableServer">Enable Z-Wave Server (Host Mode)</label>
52
- </div>
53
-
54
- <div class="form-group mt-2" id="serverPortGroup" style="display:none;">
55
- <label for="serverPort">Server Port</label>
56
- <input type="number" class="form-control" id="serverPort" placeholder="3000">
57
- </div>
58
-
59
49
  <div class="form-check mt-3">
60
50
  <input type="checkbox" class="form-check-input" id="debug">
61
- <label class="form-check-label" for="debug">Enable Debug Logging</label>
51
+ <label class="form-check-label" for="debug">Enable Verbose Driver Logging</label>
62
52
  </div>
63
53
  </fieldset>
64
54
  </form>
@@ -86,8 +76,6 @@
86
76
  platform: 'ZWaveUSB',
87
77
  serialPort: document.getElementById('serialPort').value,
88
78
  inclusionTimeoutSeconds: getInt('inclusionTimeoutSeconds', 60),
89
- enableServer: document.getElementById('enableServer').checked,
90
- serverPort: getInt('serverPort', 3000),
91
79
  debug: document.getElementById('debug').checked
92
80
  };
93
81
 
@@ -125,18 +113,8 @@
125
113
 
126
114
  // Advanced
127
115
  document.getElementById('inclusionTimeoutSeconds').value = config.inclusionTimeoutSeconds || 60;
128
- document.getElementById('enableServer').checked = config.enableServer || false;
129
- document.getElementById('serverPort').value = config.serverPort || 3000;
130
116
  document.getElementById('debug').checked = config.debug || false;
131
117
 
132
- // UI Logic
133
- function toggleServerPort() {
134
- const enabled = document.getElementById('enableServer').checked;
135
- document.getElementById('serverPortGroup').style.display = enabled ? 'block' : 'none';
136
- }
137
- document.getElementById('enableServer').addEventListener('change', toggleServerPort);
138
- toggleServerPort();
139
-
140
118
  // Auto-save on change
141
119
  document.getElementById('configForm').addEventListener('input', updateConfig);
142
120
  document.getElementById('configForm').addEventListener('change', updateConfig);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "homebridge-zwave-usb",
3
3
  "displayName": "Z-Wave USB",
4
- "version": "1.0.3",
5
- "description": "A Homebridge dynamic platform plugin for Z-Wave USB controllers.",
4
+ "version": "1.2.2",
5
+ "description": "A Homebridge dynamic platform plugin for Z-Wave USB controllers using zwave-js.",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/sapireli/homebridge-zwave-usb.git"
@@ -30,7 +30,6 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@types/ws": "^8.18.1",
33
- "@zwave-js/server": "^3.5.0",
34
33
  "homebridge-lib": "^6.0.0",
35
34
  "ws": "^8.19.0",
36
35
  "zwave-js": "^15.20.0"
@@ -45,7 +44,8 @@
45
44
  "jest": "^30.2.0",
46
45
  "rimraf": "^5.0.10",
47
46
  "ts-jest": "^29.4.6",
48
- "typescript": "^5.3.3"
47
+ "typescript": "^5.3.3",
48
+ "zwave-js": "^15.20.0"
49
49
  },
50
50
  "homebridge": {
51
51
  "pluginAlias": "ZWaveUSB",
@@ -1,47 +0,0 @@
1
- import { Logger } from 'homebridge';
2
- import { EventEmitter } from 'events';
3
- import { IZWaveController, IZWaveNode } from './interfaces';
4
- interface RemoteControllerOptions {
5
- debug?: boolean;
6
- }
7
- export declare class VirtualZWaveNode extends EventEmitter implements IZWaveNode {
8
- readonly nodeId: number;
9
- private readonly controller;
10
- ready: boolean;
11
- private values;
12
- private metadata;
13
- constructor(nodeId: number, controller: ZWaveRemoteController, initialState?: any);
14
- private getKey;
15
- updateValue(valueObj: any, emitEvent?: boolean): void;
16
- getValue(valueId: any): any;
17
- getValueMetadata(valueId: any): any;
18
- getDefinedValueIDs(): any[];
19
- setValue(valueId: any, value: any): Promise<boolean | undefined>;
20
- supportsCC(cc: number): boolean;
21
- getAllEndpoints(): any[];
22
- }
23
- export declare class ZWaveRemoteController extends EventEmitter implements IZWaveController {
24
- private readonly log;
25
- private readonly url;
26
- private readonly options;
27
- private ws;
28
- readonly nodes: Map<number, VirtualZWaveNode>;
29
- private messageId;
30
- private pendingRequests;
31
- homeId: number | undefined;
32
- constructor(log: Logger, url: string, options?: RemoteControllerOptions);
33
- start(): Promise<void>;
34
- stop(): Promise<void>;
35
- private handleMessage;
36
- private handleEvent;
37
- private addNode;
38
- sendCommand(command: string, args?: any): Promise<any>;
39
- sendValue(nodeId: number, valueId: any, value: any): Promise<boolean>;
40
- startInclusion(): Promise<boolean>;
41
- stopInclusion(): Promise<boolean>;
42
- startExclusion(): Promise<boolean>;
43
- stopExclusion(): Promise<boolean>;
44
- startHealing(): Promise<boolean>;
45
- stopHealing(): Promise<boolean>;
46
- }
47
- export {};
@@ -1,301 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ZWaveRemoteController = exports.VirtualZWaveNode = void 0;
7
- const events_1 = require("events");
8
- const ws_1 = __importDefault(require("ws"));
9
- class VirtualZWaveNode extends events_1.EventEmitter {
10
- nodeId;
11
- controller;
12
- ready = true;
13
- values = new Map(); // eslint-disable-line @typescript-eslint/no-explicit-any
14
- metadata = new Map(); // eslint-disable-line @typescript-eslint/no-explicit-any
15
- constructor(nodeId, controller,
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- initialState) {
18
- super();
19
- this.nodeId = nodeId;
20
- this.controller = controller;
21
- if (initialState) {
22
- // Hydrate from initial state if provided
23
- if (initialState.values) {
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- initialState.values.forEach((v) => this.updateValue(v, false));
26
- }
27
- }
28
- }
29
- // Helper to generate a key for value map
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- getKey(valueId) {
32
- return `${valueId.commandClass}-${valueId.endpoint || 0}-${valueId.property}-${valueId.propertyKey}`;
33
- }
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- updateValue(valueObj, emitEvent = true) {
36
- const key = this.getKey(valueObj);
37
- // Normalize: Events use 'newValue', State uses 'value'
38
- const storedValue = { ...valueObj };
39
- if ('newValue' in valueObj) {
40
- storedValue.value = valueObj.newValue;
41
- }
42
- this.values.set(key, storedValue);
43
- if (emitEvent) {
44
- this.emit('value updated', this, storedValue);
45
- }
46
- }
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
- getValue(valueId) {
49
- const key = this.getKey(valueId);
50
- return this.values.get(key)?.value;
51
- }
52
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
- getValueMetadata(valueId) {
54
- const key = this.getKey(valueId);
55
- return this.values.get(key)?.metadata;
56
- }
57
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
- getDefinedValueIDs() {
59
- return Array.from(this.values.values());
60
- }
61
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
- async setValue(valueId, value) {
63
- return this.controller.sendValue(this.nodeId, valueId, value);
64
- }
65
- supportsCC(cc) {
66
- // Check if we have any values for this CC
67
- // This is a rough approximation. A proper sync would include cc list.
68
- for (const v of this.values.values()) {
69
- if (v.commandClass === cc)
70
- return true;
71
- }
72
- return false;
73
- }
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- getAllEndpoints() {
76
- // Aggregate endpoints from values
77
- const endpoints = new Set();
78
- for (const v of this.values.values()) {
79
- endpoints.add(v.endpoint || 0);
80
- }
81
- return Array.from(endpoints).map(i => ({
82
- index: i,
83
- supportsCC: (cc) => {
84
- for (const v of this.values.values()) {
85
- if (v.endpoint === i && v.commandClass === cc)
86
- return true;
87
- }
88
- return false;
89
- }
90
- }));
91
- }
92
- }
93
- exports.VirtualZWaveNode = VirtualZWaveNode;
94
- class ZWaveRemoteController extends events_1.EventEmitter {
95
- log;
96
- url;
97
- options;
98
- ws;
99
- nodes = new Map();
100
- messageId = 0;
101
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
- pendingRequests = new Map();
103
- homeId;
104
- constructor(log, url, options = {}) {
105
- super();
106
- this.log = log;
107
- this.url = url;
108
- this.options = options;
109
- }
110
- async start() {
111
- this.log.info(`Connecting to Z-Wave JS Server at ${this.url}...`);
112
- return new Promise((resolve, reject) => {
113
- this.ws = new ws_1.default(this.url);
114
- this.ws.on('open', async () => {
115
- this.log.info('Connected to Z-Wave JS Server.');
116
- try {
117
- // 1. Start Listening
118
- const result = await this.sendCommand('startListening');
119
- // 2. Hydrate State
120
- if (result && result.state) {
121
- this.homeId = result.state.controller.homeId;
122
- this.log.info(`Remote Controller Home ID: ${this.homeId}`);
123
- if (result.state.nodes) {
124
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
125
- result.state.nodes.forEach((nodeState) => {
126
- this.addNode(nodeState);
127
- });
128
- }
129
- }
130
- resolve();
131
- }
132
- catch (err) {
133
- reject(err);
134
- }
135
- });
136
- this.ws.on('message', (data) => {
137
- try {
138
- const msg = JSON.parse(data);
139
- this.handleMessage(msg);
140
- }
141
- catch (err) {
142
- this.log.error('Error parsing WS message', err);
143
- }
144
- });
145
- this.ws.on('error', (err) => {
146
- this.log.error('WebSocket error:', err);
147
- if (!this.homeId)
148
- reject(err);
149
- });
150
- this.ws.on('close', () => {
151
- this.log.warn('WebSocket closed.');
152
- });
153
- });
154
- }
155
- async stop() {
156
- // 1. Clear all pending timeouts
157
- for (const req of this.pendingRequests.values()) {
158
- clearTimeout(req.timeout);
159
- // We don't intentionally reject here to avoid unhandled rejections crashing tests
160
- // The promises will remain pending but the process can exit because timers are cleared
161
- }
162
- this.pendingRequests.clear();
163
- // 2. Close WS
164
- if (this.ws) {
165
- this.ws.removeAllListeners();
166
- this.ws.close();
167
- this.ws = undefined;
168
- }
169
- // 3. Clear nodes and listeners
170
- for (const node of this.nodes.values()) {
171
- node.removeAllListeners();
172
- }
173
- this.nodes.clear();
174
- this.removeAllListeners();
175
- }
176
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
- handleMessage(msg) {
178
- if (msg.type === 'result') {
179
- const p = this.pendingRequests.get(msg.messageId);
180
- if (p) {
181
- clearTimeout(p.timeout);
182
- this.pendingRequests.delete(msg.messageId);
183
- if (msg.success)
184
- p.resolve(msg.result);
185
- else
186
- p.reject(new Error(msg.message || 'Unknown error'));
187
- }
188
- return;
189
- }
190
- if (msg.type === 'event') {
191
- this.handleEvent(msg.event);
192
- }
193
- }
194
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
- handleEvent(event) {
196
- if (event.source === 'node') {
197
- const node = this.nodes.get(event.nodeId);
198
- if (!node)
199
- return;
200
- if (event.event === 'value updated') {
201
- node.updateValue(event.args);
202
- this.emit('value updated', node); // Re-emit for platform
203
- }
204
- }
205
- if (event.source === 'controller') {
206
- if (event.event === 'node added') {
207
- this.log.info(`Remote Node added: ${event.node.nodeId}`);
208
- this.addNode(event.node);
209
- }
210
- if (event.event === 'node removed') {
211
- this.log.info(`Remote Node removed: ${event.nodeId}`);
212
- const node = this.nodes.get(event.nodeId);
213
- if (node) {
214
- this.nodes.delete(event.nodeId);
215
- this.emit('node removed', node);
216
- }
217
- }
218
- if (event.event === 'inclusion started') {
219
- this.emit('inclusion started', event.secure);
220
- }
221
- if (event.event === 'inclusion stopped') {
222
- this.emit('inclusion stopped');
223
- }
224
- if (event.event === 'exclusion started') {
225
- this.emit('exclusion started');
226
- }
227
- if (event.event === 'exclusion stopped') {
228
- this.emit('exclusion stopped');
229
- }
230
- }
231
- }
232
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
- addNode(nodeState) {
234
- const node = new VirtualZWaveNode(nodeState.nodeId, this, nodeState);
235
- this.nodes.set(node.nodeId, node);
236
- this.emit('node ready', node); // Simulate node ready
237
- }
238
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
239
- async sendCommand(command, args = {}) {
240
- if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
241
- throw new Error('Not connected');
242
- const messageId = ++this.messageId;
243
- const payload = {
244
- command,
245
- messageId,
246
- ...args,
247
- };
248
- return new Promise((resolve, reject) => {
249
- const timeout = setTimeout(() => {
250
- if (this.pendingRequests.has(messageId)) {
251
- this.pendingRequests.delete(messageId);
252
- reject(new Error('Timeout'));
253
- }
254
- }, 10000);
255
- this.pendingRequests.set(messageId, {
256
- resolve,
257
- reject,
258
- timeout
259
- });
260
- this.ws.send(JSON.stringify(payload));
261
- });
262
- }
263
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
- async sendValue(nodeId, valueId, value) {
265
- await this.sendCommand('node.setValue', {
266
- nodeId,
267
- valueId,
268
- value
269
- });
270
- return true;
271
- }
272
- async startInclusion() {
273
- await this.sendCommand('controller.beginInclusion', { strategy: 'Default' }); // Simplified
274
- this.emit('inclusion started', false);
275
- return true;
276
- }
277
- async stopInclusion() {
278
- await this.sendCommand('controller.stopInclusion');
279
- this.emit('inclusion stopped');
280
- return true;
281
- }
282
- async startExclusion() {
283
- await this.sendCommand('controller.beginExclusion');
284
- this.emit('exclusion started');
285
- return true;
286
- }
287
- async stopExclusion() {
288
- await this.sendCommand('controller.stopExclusion');
289
- this.emit('exclusion stopped');
290
- return true;
291
- }
292
- async startHealing() {
293
- await this.sendCommand('controller.beginRebuildingRoutes');
294
- return true;
295
- }
296
- async stopHealing() {
297
- await this.sendCommand('controller.stopRebuildingRoutes');
298
- return true;
299
- }
300
- }
301
- exports.ZWaveRemoteController = ZWaveRemoteController;