homebridge-zwave-usb 1.0.4 → 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 +15 -83
- package/config.schema.json +20 -39
- package/dist/accessories/ControllerAccessory.js +6 -0
- package/dist/platform/ZWaveUsbPlatform.js +31 -35
- package/dist/zwave/ZWaveController.d.ts +1 -5
- package/dist/zwave/ZWaveController.js +56 -37
- package/dist/zwave/interfaces.d.ts +1 -0
- package/homebridge-ui/public/index.html +4 -26
- package/package.json +4 -4
- package/dist/zwave/ZWaveRemoteController.d.ts +0 -47
- package/dist/zwave/ZWaveRemoteController.js +0 -301
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.
|
|
4
|
+
> **ALPHA SOFTWARE:** This plugin is in early development. Expect bugs and missing features.
|
|
5
5
|
|
|
6
6
|
[](https://github.com/user/homebridge-zwave-usb/actions/workflows/build.yml)
|
|
7
7
|
|
|
8
|
-
A high-performance
|
|
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
|
|
15
|
-
* 🔌 **
|
|
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
|
-
* 🔐 **
|
|
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
|
-
##
|
|
22
|
-
|
|
23
|
-
Choose the setup that fits your needs:
|
|
19
|
+
## 📦 Prerequisites
|
|
24
20
|
|
|
25
|
-
|
|
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. **
|
|
54
|
-
|
|
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
|
|
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
|
|
104
|
-
|
|
105
|
-
---
|
|
60
|
+
* **Heal Network:** Turn ON to rebuild mesh routes.
|
|
106
61
|
|
|
107
|
-
|
|
62
|
+
For advanced management (firmware updates, configuration parameters, network graphs), use the **Z-Wave JS UI** web interface.
|
|
108
63
|
|
|
109
|
-
|
|
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
|
package/config.schema.json
CHANGED
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"default": "ZWaveUSB",
|
|
19
19
|
"const": "ZWaveUSB"
|
|
20
20
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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).
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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,
|
|
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:
|
|
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": "
|
|
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
|
|
25
|
+
this.log.error('No serial port configured. Please set "serialPort" in your configuration.');
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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,32 @@ class ZWaveController extends events_1.EventEmitter {
|
|
|
36
32
|
securityKeys.S2_AccessControl = Buffer.from(this.options.securityKeys.S2_AccessControl, 'hex');
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
57
|
+
logConfig,
|
|
58
|
+
storage: {
|
|
59
|
+
cacheDir: storagePath + '/zwave-js-cache',
|
|
60
|
+
deviceConfigPriorityDir: storagePath + '/zwave-js-config',
|
|
45
61
|
},
|
|
46
62
|
features: {
|
|
47
63
|
softReset: false,
|
|
@@ -104,11 +120,30 @@ class ZWaveController extends events_1.EventEmitter {
|
|
|
104
120
|
return;
|
|
105
121
|
}
|
|
106
122
|
this.nodes.set(node.nodeId, node);
|
|
107
|
-
const onReady = () =>
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
});
|
|
110
142
|
node.on('ready', onReady);
|
|
111
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})`);
|
|
112
147
|
if (node.ready) {
|
|
113
148
|
this.emit('node ready', node);
|
|
114
149
|
}
|
|
@@ -118,6 +153,12 @@ class ZWaveController extends events_1.EventEmitter {
|
|
|
118
153
|
if (listeners) {
|
|
119
154
|
node.off('ready', listeners.ready);
|
|
120
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
|
+
}
|
|
121
162
|
this.nodeListeners.delete(node.nodeId);
|
|
122
163
|
}
|
|
123
164
|
this.nodes.delete(node.nodeId);
|
|
@@ -137,30 +178,9 @@ class ZWaveController extends events_1.EventEmitter {
|
|
|
137
178
|
await this.driver.start();
|
|
138
179
|
this.log.info('Z-Wave driver started');
|
|
139
180
|
await readyPromise;
|
|
140
|
-
if (this.options.server?.enabled) {
|
|
141
|
-
try {
|
|
142
|
-
this.log.info(`Starting Z-Wave JS Server (Host Mode) on port ${this.options.server.port}...`);
|
|
143
|
-
this.server = new server_1.ZwavejsServer(this.driver, {
|
|
144
|
-
port: this.options.server.port,
|
|
145
|
-
logger: this.log,
|
|
146
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
|
-
});
|
|
148
|
-
await this.server.start();
|
|
149
|
-
this.log.info('Z-Wave JS Server started successfully.');
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
this.log.error('Failed to start Z-Wave JS Server:', err);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
181
|
}
|
|
156
182
|
async stop() {
|
|
157
|
-
// 1.
|
|
158
|
-
if (this.server) {
|
|
159
|
-
this.log.info('Stopping Z-Wave JS Server...');
|
|
160
|
-
await this.server.destroy();
|
|
161
|
-
this.server = undefined;
|
|
162
|
-
}
|
|
163
|
-
// 2. Remove Node Listeners
|
|
183
|
+
// 1. Remove Node Listeners
|
|
164
184
|
for (const [nodeId, node] of this.nodes) {
|
|
165
185
|
const listeners = this.nodeListeners.get(nodeId);
|
|
166
186
|
if (listeners) {
|
|
@@ -169,10 +189,9 @@ class ZWaveController extends events_1.EventEmitter {
|
|
|
169
189
|
}
|
|
170
190
|
}
|
|
171
191
|
this.nodeListeners.clear();
|
|
172
|
-
//
|
|
192
|
+
// 2. Remove Controller/Driver Listeners
|
|
173
193
|
this.driver.removeAllListeners();
|
|
174
194
|
try {
|
|
175
|
-
// Use a property check to avoid throwing if driver is not ready
|
|
176
195
|
if (this.driver.controller) {
|
|
177
196
|
for (const [event, listener] of Object.entries(this.controllerListeners)) {
|
|
178
197
|
this.driver.controller.off(event, listener); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
@@ -182,10 +201,10 @@ class ZWaveController extends events_1.EventEmitter {
|
|
|
182
201
|
catch {
|
|
183
202
|
// Ignore errors if controller is not available
|
|
184
203
|
}
|
|
185
|
-
//
|
|
204
|
+
// 3. Destroy Driver
|
|
186
205
|
await this.driver.destroy();
|
|
187
206
|
this.log.info('Z-Wave driver stopped');
|
|
188
|
-
//
|
|
207
|
+
// 4. Clear Local State
|
|
189
208
|
this.nodes.clear();
|
|
190
209
|
this.removeAllListeners();
|
|
191
210
|
}
|
|
@@ -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
|
|
9
|
-
<input type="text" class="form-control" id="serialPort" placeholder="/dev/ttyACM0
|
|
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
|
|
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
|
|
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.
|
|
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;
|