homebridge-zwave-usb 3.3.4 → 3.4.0

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
@@ -4,6 +4,7 @@
4
4
  [![NPM Downloads](https://img.shields.io/npm/dt/homebridge-zwave-usb.svg)](https://www.npmjs.com/package/homebridge-zwave-usb)
5
5
  [![Build and Test](https://github.com/sapireli/homebridge-zwave-usb/actions/workflows/build.yml/badge.svg)](https://github.com/sapireli/homebridge-zwave-usb/actions/workflows/build.yml)
6
6
  [![License: Polyform Noncommercial](https://img.shields.io/badge/License-Polyform_Noncommercial-blue.svg)](LICENSE)
7
+ [![Sponsor](https://img.shields.io/badge/Sponsor-❤️-ea4aaa?logo=github)](https://www.paypal.me/sapir)
7
8
 
8
9
  A high-performance, production-grade [Homebridge](https://homebridge.io) integration for Z-Wave networks. Built directly on the modern [Z-Wave JS](https://zwave-js.github.io/node-zwave-js/) driver, this plugin provides direct communication with USB controllers for ultra-low latency and maximum reliability.
9
10
 
@@ -2,6 +2,8 @@ import { IZWaveNode } from '../zwave/interfaces';
2
2
  import { ZWaveAccessory } from './ZWaveAccessory';
3
3
  import { ZWaveUsbPlatform } from '../platform/ZWaveUsbPlatform';
4
4
  export declare class AccessoryFactory {
5
- static create(platform: ZWaveUsbPlatform, node: IZWaveNode, homeId: number): ZWaveAccessory;
5
+ static create(platform: ZWaveUsbPlatform, node: IZWaveNode, homeId: number, options?: {
6
+ forceUuidSeed?: string;
7
+ }): ZWaveAccessory;
6
8
  private static attachFeatures;
7
9
  }
@@ -20,8 +20,8 @@ const GarageDoorFeature_1 = require("../features/GarageDoorFeature");
20
20
  const ColorSwitchFeature_1 = require("../features/ColorSwitchFeature");
21
21
  const SirenFeature_1 = require("../features/SirenFeature");
22
22
  class AccessoryFactory {
23
- static create(platform, node, homeId) {
24
- const accessory = new ZWaveAccessory_1.ZWaveAccessory(platform, node, homeId);
23
+ static create(platform, node, homeId, options = {}) {
24
+ const accessory = new ZWaveAccessory_1.ZWaveAccessory(platform, node, homeId, options);
25
25
  const endpoints = node.getAllEndpoints();
26
26
  const isMultiEndpoint = endpoints.length > 1;
27
27
  // Track which CCs are handled by non-zero endpoints to avoid duplicates on root
@@ -21,7 +21,6 @@ export declare class ControllerAccessory {
21
21
  private handlers;
22
22
  constructor(platform: ZWaveUsbPlatform, controller: IZWaveController);
23
23
  private setupControllerHandlers;
24
- private syncConfiguredName;
25
24
  private setupPinEntryCharacteristic;
26
25
  private handleSetInclusion;
27
26
  private handleSetExclusion;
@@ -111,7 +111,6 @@ class ControllerAccessory {
111
111
  this.statusService =
112
112
  this.platformAccessory.getService(settings_1.MANAGER_SERVICE_UUID) ||
113
113
  this.platformAccessory.addService(new managerServiceType('System Status', 'Status'));
114
- this.syncConfiguredName(this.statusService, 'System Status');
115
114
  this.statusService
116
115
  .getCharacteristic(this.platform.Characteristic.Name)
117
116
  .setProps({
@@ -152,7 +151,6 @@ class ControllerAccessory {
152
151
  this.inclusionService =
153
152
  this.platformAccessory.getServiceById(this.platform.Service.Switch, 'Inclusion') ||
154
153
  this.platformAccessory.addService(this.platform.Service.Switch, 'Inclusion Mode', 'Inclusion');
155
- this.syncConfiguredName(this.inclusionService, 'Inclusion Mode');
156
154
  this.inclusionService
157
155
  .getCharacteristic(this.platform.Characteristic.Name)
158
156
  .setProps({
@@ -173,7 +171,6 @@ class ControllerAccessory {
173
171
  this.exclusionService =
174
172
  this.platformAccessory.getServiceById(this.platform.Service.Switch, 'Exclusion') ||
175
173
  this.platformAccessory.addService(this.platform.Service.Switch, 'Exclusion Mode', 'Exclusion');
176
- this.syncConfiguredName(this.exclusionService, 'Exclusion Mode');
177
174
  this.exclusionService
178
175
  .getCharacteristic(this.platform.Characteristic.Name)
179
176
  .setProps({
@@ -193,7 +190,6 @@ class ControllerAccessory {
193
190
  this.healService =
194
191
  this.platformAccessory.getServiceById(this.platform.Service.Switch, 'Heal') ||
195
192
  this.platformAccessory.addService(this.platform.Service.Switch, 'Heal Network', 'Heal');
196
- this.syncConfiguredName(this.healService, 'Heal Network');
197
193
  this.healService
198
194
  .getCharacteristic(this.platform.Characteristic.Name)
199
195
  .setProps({
@@ -213,7 +209,6 @@ class ControllerAccessory {
213
209
  this.pruneService =
214
210
  this.platformAccessory.getServiceById(this.platform.Service.Switch, 'Prune') ||
215
211
  this.platformAccessory.addService(this.platform.Service.Switch, 'Prune Dead Nodes', 'Prune');
216
- this.syncConfiguredName(this.pruneService, 'Prune Dead Nodes');
217
212
  this.pruneService
218
213
  .getCharacteristic(this.platform.Characteristic.Name)
219
214
  .setProps({
@@ -325,15 +320,6 @@ class ControllerAccessory {
325
320
  this.controller.on(event, handler);
326
321
  }
327
322
  }
328
- syncConfiguredName(service, value) {
329
- const configuredNameValue = value || service.displayName;
330
- if (!service.testCharacteristic(this.platform.Characteristic.ConfiguredName)) {
331
- service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
332
- }
333
- service
334
- .getCharacteristic(this.platform.Characteristic.ConfiguredName)
335
- .updateValue(configuredNameValue);
336
- }
337
323
  setupPinEntryCharacteristic(service) {
338
324
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
339
325
  const pinCharType = this.platform.Characteristic.S2PinEntry;
@@ -6,13 +6,17 @@ export declare class ZWaveAccessory {
6
6
  readonly platform: ZWaveUsbPlatform;
7
7
  node: IZWaveNode;
8
8
  readonly homeId: number;
9
+ private readonly options;
9
10
  readonly platformAccessory: PlatformAccessory;
10
11
  private features;
11
12
  private initialized;
12
- constructor(platform: ZWaveUsbPlatform, node: IZWaveNode, homeId: number);
13
+ constructor(platform: ZWaveUsbPlatform, node: IZWaveNode, homeId: number, options?: {
14
+ forceUuidSeed?: string;
15
+ });
13
16
  addFeature(feature: ZWaveFeature): void;
14
17
  /**
15
- * Syncs the HomeKit name and service names when the node is renamed.
18
+ * Applies a plugin-controlled rename to the default accessory metadata.
19
+ * This is intentionally conservative and is only used for explicit recreate flows.
16
20
  */
17
21
  rename(newName: string): void;
18
22
  /**
@@ -23,7 +27,7 @@ export declare class ZWaveAccessory {
23
27
  */
24
28
  updateNode(newNode: IZWaveNode): void;
25
29
  initialize(): void;
26
- private setupConfiguredName;
30
+ private seedConfiguredNameOnPrimaryService;
27
31
  refresh(args?: ZWaveValueEvent): void;
28
32
  stop(): void;
29
33
  }
@@ -7,17 +7,20 @@ class ZWaveAccessory {
7
7
  platform;
8
8
  node;
9
9
  homeId;
10
+ options;
10
11
  platformAccessory;
11
12
  features = [];
12
13
  initialized = false;
13
- constructor(platform, node, homeId) {
14
+ constructor(platform, node, homeId, options = {}) {
14
15
  this.platform = platform;
15
16
  this.node = node;
16
17
  this.homeId = homeId;
18
+ this.options = options;
17
19
  // WARNING: This UUID generation string MUST NOT BE CHANGED!
18
20
  // This deterministic string ensures that devices maintain the same HomeKit identity across restarts.
19
21
  // This is the stable UUID generation scheme.
20
- const stableUuid = this.platform.api.hap.uuid.generate(`homebridge-zwave-usb-${this.homeId}-${this.node.nodeId}`);
22
+ const stableId = `homebridge-zwave-usb-${this.homeId}-${this.node.nodeId}`;
23
+ const stableUuid = this.platform.api.hap.uuid.generate(stableId);
21
24
  /**
22
25
  * MIGRATION PATH: Automated Legacy UUID Adoption.
23
26
  * To prevent user automations from breaking after a plugin update that changed UUID schemes,
@@ -39,14 +42,22 @@ class ZWaveAccessory {
39
42
  break;
40
43
  }
41
44
  }
42
- const existingAccessory = this.platform.accessories.find((accessory) => accessory.UUID === uuid);
45
+ const existingAccessory = this.platform.accessories.find((accessory) => {
46
+ const context = accessory.context;
47
+ return context?.nodeId === this.node.nodeId && context?.homeId === this.homeId;
48
+ }) ||
49
+ this.platform.accessories.find((accessory) => accessory.UUID === uuid);
43
50
  const nodeName = this.node.name || this.node.deviceConfig?.label || `Node ${this.node.nodeId}`;
51
+ const forceUuidSeed = this.options.forceUuidSeed?.trim();
44
52
  if (existingAccessory) {
45
53
  this.platformAccessory = existingAccessory;
46
54
  }
47
55
  else {
56
+ const creationUuid = forceUuidSeed
57
+ ? this.platform.api.hap.uuid.generate(`${stableId}-rename-${forceUuidSeed}`)
58
+ : uuid;
48
59
  this.platform.log.info(`Creating new accessory for ${nodeName} (UUID: ${uuid})`);
49
- this.platformAccessory = new this.platform.api.platformAccessory(nodeName, uuid);
60
+ this.platformAccessory = new this.platform.api.platformAccessory(nodeName, creationUuid);
50
61
  this.platform.api.registerPlatformAccessories('homebridge-zwave-usb', 'ZWaveUSB', [
51
62
  this.platformAccessory,
52
63
  ]);
@@ -62,6 +73,18 @@ class ZWaveAccessory {
62
73
  .setCharacteristic(this.platform.Characteristic.Model, model)
63
74
  .setCharacteristic(this.platform.Characteristic.SerialNumber, serial)
64
75
  .setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.node.firmwareVersion || '1.0.0');
76
+ if (!existingAccessory) {
77
+ // Seed the initial HomeKit-facing name once for freshly created accessories.
78
+ this.platformAccessory.displayName = nodeName;
79
+ infoService.setCharacteristic(this.platform.Characteristic.Name, nodeName);
80
+ }
81
+ const context = (this.platformAccessory.context || {});
82
+ this.platformAccessory.context = context;
83
+ context.nodeId = this.node.nodeId;
84
+ context.homeId = this.homeId;
85
+ if (forceUuidSeed) {
86
+ context.renameGeneration = forceUuidSeed;
87
+ }
65
88
  /**
66
89
  * Helper to normalize UUIDs for reliable comparison during metadata pruning.
67
90
  */
@@ -86,22 +109,19 @@ class ZWaveAccessory {
86
109
  this.features.push(feature);
87
110
  }
88
111
  /**
89
- * Syncs the HomeKit name and service names when the node is renamed.
112
+ * Applies a plugin-controlled rename to the default accessory metadata.
113
+ * This is intentionally conservative and is only used for explicit recreate flows.
90
114
  */
91
115
  rename(newName) {
92
116
  this.platform.log.info(`Syncing HomeKit name for Node ${this.node.nodeId} -> ${newName}`);
93
117
  this.platformAccessory.displayName = newName;
94
118
  const infoService = this.platformAccessory.getService(this.platform.Service.AccessoryInformation);
95
119
  if (infoService) {
96
- const configuredNameChar = infoService.getCharacteristic(this.platform.Characteristic.ConfiguredName);
97
- if (configuredNameChar) {
98
- configuredNameChar.updateValue(newName);
99
- }
120
+ infoService.setCharacteristic(this.platform.Characteristic.Name, newName);
100
121
  }
101
122
  for (const feature of this.features) {
102
123
  feature.rename(newName);
103
124
  }
104
- this.platform.api.updatePlatformAccessories([this.platformAccessory]);
105
125
  }
106
126
  /**
107
127
  * HOT-RECOVERY FIX: Update stale node references.
@@ -149,32 +169,25 @@ class ZWaveAccessory {
149
169
  this.platformAccessory.removeService(service);
150
170
  }
151
171
  });
152
- // Bind ConfiguredName only on AccessoryInformation (HAP-defined location).
153
- const infoService = this.platformAccessory.getService(this.platform.Service.AccessoryInformation);
154
- if (infoService) {
155
- this.setupConfiguredName(infoService);
156
- }
172
+ this.seedConfiguredNameOnPrimaryService();
157
173
  this.platform.api.updatePlatformAccessories([this.platformAccessory]);
158
174
  this.refresh();
159
175
  }
160
- setupConfiguredName(service) {
161
- if (!service.testCharacteristic(this.platform.Characteristic.ConfiguredName)) {
162
- service.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
163
- }
164
- this.platform.log.debug(`[ConfiguredName] Binding on service "${service.displayName}" (${service.UUID}) for Node ${this.node.nodeId}`);
165
- const configuredNameChar = service.getCharacteristic(this.platform.Characteristic.ConfiguredName);
166
- const existingName = this.platformAccessory.displayName ||
167
- this.node.name ||
168
- this.node.deviceConfig?.label ||
169
- `Node ${this.node.nodeId}`;
170
- if (!configuredNameChar.value) {
171
- configuredNameChar.updateValue(existingName);
172
- }
173
- configuredNameChar.onSet((value) => {
174
- const newName = String(value);
175
- this.platform.log.info(`[ConfiguredName:onSet] Service "${service.displayName}" Node ${this.node.nodeId}: ${this.platformAccessory.displayName} -> ${newName}`);
176
- this.rename(newName);
177
- });
176
+ seedConfiguredNameOnPrimaryService() {
177
+ const primaryService = this.features.flatMap((feature) => feature.getServices())[0];
178
+ if (!primaryService) {
179
+ return;
180
+ }
181
+ if (typeof primaryService.setPrimaryService === 'function') {
182
+ primaryService.setPrimaryService(true);
183
+ }
184
+ if (!primaryService.testCharacteristic(this.platform.Characteristic.ConfiguredName)) {
185
+ primaryService.addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);
186
+ }
187
+ const configuredName = primaryService.getCharacteristic(this.platform.Characteristic.ConfiguredName);
188
+ if (!configuredName.value) {
189
+ configuredName.updateValue(primaryService.displayName || this.platformAccessory.displayName);
190
+ }
178
191
  }
179
192
  refresh(args) {
180
193
  /**
@@ -68,28 +68,31 @@ class BaseFeature {
68
68
  ? `${this.accessory.displayName} ${this.endpoint.index}`
69
69
  : this.accessory.displayName);
70
70
  let service;
71
+ let wasCreated = false;
71
72
  if (subType) {
72
73
  service =
73
- this.accessory.getServiceById(serviceType, subType) ||
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- this.accessory.addService(new serviceType(serviceName, subType));
74
+ this.accessory.getServiceById(serviceType, subType);
75
+ if (!service) {
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ service = this.accessory.addService(new serviceType(serviceName, subType));
78
+ wasCreated = true;
79
+ }
76
80
  }
77
81
  else {
78
82
  service =
79
- this.accessory.getService(serviceType) ||
80
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
- this.accessory.addService(new serviceType(serviceName));
83
+ this.accessory.getService(serviceType);
84
+ if (!service) {
85
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ service = this.accessory.addService(new serviceType(serviceName));
87
+ wasCreated = true;
88
+ }
82
89
  }
83
90
  // Sync internal property
84
91
  service.displayName = serviceName;
85
- // 1. Standard Name: Notify removed
86
- if (service.testCharacteristic(this.platform.Characteristic.Name)) {
92
+ // Seed the service Name only when the service is first created.
93
+ if (wasCreated && service.testCharacteristic(this.platform.Characteristic.Name)) {
87
94
  const nameChar = service.getCharacteristic(this.platform.Characteristic.Name);
88
- // Only update if current value is generic "Node X" to respect user overrides
89
- const currentValue = nameChar.value;
90
- if (!currentValue || currentValue.startsWith('Node ')) {
91
- nameChar.updateValue(serviceName);
92
- }
95
+ nameChar.updateValue(serviceName);
93
96
  }
94
97
  // Add Service Label Index for multi-endpoint devices to help with ordering/naming
95
98
  if (this.endpoint.index > 0) {
@@ -27,6 +27,7 @@ export declare class ZWaveUsbPlatform implements DynamicPlatformPlugin {
27
27
  private handleNodeAdded;
28
28
  private handleNodeReady;
29
29
  private handleNodeRemoved;
30
+ private recreateNodeAccessory;
30
31
  /**
31
32
  * Event-Driven Updates:
32
33
  * Instead of refreshing every accessory on every change, we route the specific
@@ -130,7 +130,7 @@ class ZWaveUsbPlatform {
130
130
  method === 'POST') {
131
131
  let body = '';
132
132
  req.on('data', (chunk) => (body += chunk));
133
- req.on('end', () => {
133
+ req.on('end', async () => {
134
134
  try {
135
135
  const parts = normalizedUrl.split('/');
136
136
  const nodeId = parseInt(parts[2], 10);
@@ -138,11 +138,10 @@ class ZWaveUsbPlatform {
138
138
  if (!this.zwaveController) {
139
139
  throw new Error('Controller not initialized');
140
140
  }
141
- this.zwaveController.setNodeName(nodeId, name);
142
- // Update Homebridge Accessory Name
143
- const accessory = this.zwaveAccessories.get(nodeId);
144
- if (accessory) {
145
- accessory.rename(name);
141
+ await this.zwaveController.setNodeName(nodeId, name);
142
+ const node = this.zwaveController.nodes.get(nodeId);
143
+ if (node) {
144
+ this.recreateNodeAccessory(node, Date.now().toString(36));
146
145
  }
147
146
  return sendJson({ success: true });
148
147
  }
@@ -382,6 +381,27 @@ class ZWaveUsbPlatform {
382
381
  }
383
382
  }
384
383
  }
384
+ recreateNodeAccessory(node, uuidSeed) {
385
+ const existing = this.zwaveAccessories.get(node.nodeId);
386
+ if (existing) {
387
+ existing.stop();
388
+ this.api.unregisterPlatformAccessories('homebridge-zwave-usb', 'ZWaveUSB', [
389
+ existing.platformAccessory,
390
+ ]);
391
+ this.zwaveAccessories.delete(node.nodeId);
392
+ const index = this.accessories.indexOf(existing.platformAccessory);
393
+ if (index !== -1) {
394
+ this.accessories.splice(index, 1);
395
+ }
396
+ }
397
+ const homeId = this.zwaveController?.homeId;
398
+ if (!homeId) {
399
+ throw new Error(`Cannot recreate accessory for Node ${node.nodeId} - Home ID not available`);
400
+ }
401
+ const recreated = AccessoryFactory_1.AccessoryFactory.create(this, node, homeId, { forceUuidSeed: uuidSeed });
402
+ this.zwaveAccessories.set(node.nodeId, recreated);
403
+ recreated.initialize();
404
+ }
385
405
  /**
386
406
  * Event-Driven Updates:
387
407
  * Instead of refreshing every accessory on every change, we route the specific
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-zwave-usb",
3
3
  "displayName": "Homebridge Z-Wave USB",
4
- "version": "3.3.4",
4
+ "version": "3.4.0",
5
5
  "description": "A Homebridge dynamic platform plugin for Z-Wave USB controllers using zwave-js.",
6
6
  "license": "Polyform-Noncommercial-1.0.0",
7
7
  "funding": {
@@ -14,7 +14,11 @@
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://github.com/sapireli/homebridge-zwave-usb.git"
17
+ "url": "https://github.com/sapireli/homebridge-zwave-usb"
18
+ },
19
+ "publishConfig": {
20
+ "registry": "https://registry.npmjs.org/",
21
+ "access": "public"
18
22
  },
19
23
  "main": "dist/index.js",
20
24
  "scripts": {