homebridge-zwave-usb 2.3.0 → 2.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.
@@ -11,6 +11,10 @@ export declare class ZWaveAccessory {
11
11
  private initialized;
12
12
  constructor(platform: ZWaveUsbPlatform, node: IZWaveNode, homeId: number);
13
13
  addFeature(feature: ZWaveFeature): void;
14
+ /**
15
+ * Syncs the HomeKit name and service names when the node is renamed.
16
+ */
17
+ rename(newName: string): void;
14
18
  /**
15
19
  * HOT-RECOVERY FIX: Update stale node references.
16
20
  * When the driver restarts (hot-plug), the IZWaveNode instance changes.
@@ -82,6 +82,24 @@ class ZWaveAccessory {
82
82
  addFeature(feature) {
83
83
  this.features.push(feature);
84
84
  }
85
+ /**
86
+ * Syncs the HomeKit name and service names when the node is renamed.
87
+ */
88
+ rename(newName) {
89
+ this.platform.log.info(`Syncing HomeKit name for Node ${this.node.nodeId} -> ${newName}`);
90
+ this.platformAccessory.displayName = newName;
91
+ // Update the Model and Serial if they were using the generic name
92
+ const model = this.node.deviceConfig?.label || newName;
93
+ this.platformAccessory
94
+ .getService(this.platform.Service.AccessoryInformation)
95
+ .setCharacteristic(this.platform.Characteristic.Model, model);
96
+ // Update all features
97
+ for (const feature of this.features) {
98
+ feature.rename(newName);
99
+ }
100
+ // Notify Homebridge of changes
101
+ this.platform.api.updatePlatformAccessories([this.platformAccessory]);
102
+ }
85
103
  /**
86
104
  * HOT-RECOVERY FIX: Update stale node references.
87
105
  * When the driver restarts (hot-plug), the IZWaveNode instance changes.
@@ -9,6 +9,7 @@ export interface ZWaveFeature {
9
9
  getEndpointIndex(): number;
10
10
  stop(): void;
11
11
  updateNode(node: IZWaveNode, endpoint: Endpoint): void;
12
+ rename(newName: string): void;
12
13
  }
13
14
  export declare abstract class BaseFeature implements ZWaveFeature {
14
15
  protected readonly platform: ZWaveUsbPlatform;
@@ -21,6 +22,10 @@ export declare abstract class BaseFeature implements ZWaveFeature {
21
22
  abstract update(args?: ZWaveValueEvent): void;
22
23
  stop(): void;
23
24
  updateNode(node: IZWaveNode, endpoint: Endpoint): void;
25
+ /**
26
+ * Dynamically updates the Name characteristic of all managed services.
27
+ */
28
+ rename(newName: string): void;
24
29
  getServices(): Service[];
25
30
  getEndpointIndex(): number;
26
31
  /**
@@ -21,6 +21,17 @@ class BaseFeature {
21
21
  this.node = node;
22
22
  this.endpoint = endpoint;
23
23
  }
24
+ /**
25
+ * Dynamically updates the Name characteristic of all managed services.
26
+ */
27
+ rename(newName) {
28
+ const serviceName = this.endpoint.index > 0 ? `${newName} ${this.endpoint.index}` : newName;
29
+ for (const service of this.managedServices) {
30
+ if (service.testCharacteristic(this.platform.Characteristic.Name)) {
31
+ service.getCharacteristic(this.platform.Characteristic.Name).updateValue(serviceName);
32
+ }
33
+ }
34
+ }
24
35
  getServices() {
25
36
  return this.managedServices;
26
37
  }
@@ -140,9 +140,7 @@ class ZWaveUsbPlatform {
140
140
  // Update Homebridge Accessory Name
141
141
  const accessory = this.zwaveAccessories.get(nodeId);
142
142
  if (accessory) {
143
- this.log.info(`Renaming HomeKit accessory for Node ${nodeId} to: ${name}`);
144
- accessory.platformAccessory.displayName = name;
145
- this.api.updatePlatformAccessories([accessory.platformAccessory]);
143
+ accessory.rename(name);
146
144
  }
147
145
  return sendJson({ success: true });
148
146
  }
@@ -326,7 +324,8 @@ class ZWaveUsbPlatform {
326
324
  this.log.info(`Node ${node.nodeId} added to the network. Waiting for interview to complete...`);
327
325
  }
328
326
  handleNodeReady(node) {
329
- this.log.info(`Node ${node.nodeId} ready`);
327
+ const nodeName = node.name || node.deviceConfig?.label || `Node ${node.nodeId}`;
328
+ this.log.info(`Node ${node.nodeId} (${nodeName}) ready`);
330
329
  // Skip Node 1 (the controller itself) as it's handled by ControllerAccessory
331
330
  if (Number(node.nodeId) === 1) {
332
331
  this.log.info('System: Controller node (Node 1) identified. Skipping generic accessory creation.');
@@ -345,6 +344,9 @@ class ZWaveUsbPlatform {
345
344
  if (existing || this.discoveryInFlight.has(node.nodeId)) {
346
345
  if (existing) {
347
346
  existing.updateNode(node);
347
+ if (existing.platformAccessory.displayName !== nodeName) {
348
+ existing.rename(nodeName);
349
+ }
348
350
  existing.refresh();
349
351
  }
350
352
  return;
@@ -353,11 +355,8 @@ class ZWaveUsbPlatform {
353
355
  try {
354
356
  const accessory = AccessoryFactory_1.AccessoryFactory.create(this, node, homeId);
355
357
  this.zwaveAccessories.set(node.nodeId, accessory);
356
- const nodeName = node.name || node.deviceConfig?.label || `Node ${node.nodeId}`;
357
358
  if (accessory.platformAccessory.displayName !== nodeName) {
358
- this.log.info(`Renaming accessory ${accessory.platformAccessory.displayName} -> ${nodeName}`);
359
- accessory.platformAccessory.displayName = nodeName;
360
- this.api.updatePlatformAccessories([accessory.platformAccessory]);
359
+ accessory.rename(nodeName);
361
360
  }
362
361
  accessory.initialize();
363
362
  }
@@ -154,7 +154,8 @@ class ZWaveController extends events_1.EventEmitter {
154
154
  */
155
155
  const statusMap = ['Unknown', 'Asleep', 'Awake', 'Dead', 'Alive'];
156
156
  const status = statusMap[node.status] || node.status.toString();
157
- this.log.info(`Node ${node.nodeId} added to controller (Status: ${status}, Interview Stage: ${node.interviewStage})`);
157
+ const nodeName = node.name || node.deviceConfig?.label || `Node ${node.nodeId}`;
158
+ this.log.info(`Node ${node.nodeId} (${nodeName}) added to controller (Status: ${status}, Interview Stage: ${node.interviewStage})`);
158
159
  const onReady = () => {
159
160
  this.log.info(`Node ${node.nodeId} is ready (Interview Stage: ${node.interviewStage})`);
160
161
  this.emit('status updated', `Node ${node.nodeId} Ready`);
@@ -68,6 +68,29 @@
68
68
  }
69
69
 
70
70
  .btn-group .btn { margin-right: 2px; }
71
+
72
+ /* Custom Modal styles to replace native prompt/confirm */
73
+ .custom-modal-backdrop {
74
+ display: none;
75
+ position: fixed;
76
+ top: 0; left: 0; width: 100%; height: 100%;
77
+ background: rgba(0,0,0,0.5);
78
+ z-index: 1050;
79
+ }
80
+ .custom-modal {
81
+ display: none;
82
+ position: fixed;
83
+ top: 50%; left: 50%;
84
+ transform: translate(-50%, -50%);
85
+ background: var(--hb-background-color, #fff);
86
+ color: var(--hb-text-color, #212529);
87
+ padding: 20px;
88
+ border-radius: 8px;
89
+ box-shadow: 0 5px 15px rgba(0,0,0,0.3);
90
+ z-index: 1051;
91
+ width: 90%;
92
+ max-width: 400px;
93
+ }
71
94
  </style>
72
95
  </head>
73
96
  <body>
@@ -184,10 +207,35 @@
184
207
  </div>
185
208
  </div>
186
209
 
210
+ <!-- Custom Modals -->
211
+ <div id="modalBackdrop" class="custom-modal-backdrop"></div>
212
+
213
+ <div id="renameModal" class="custom-modal">
214
+ <h6>Rename Node</h6>
215
+ <div class="form-group">
216
+ <input type="text" class="form-control" id="newLabelInput" />
217
+ </div>
218
+ <div class="text-right">
219
+ <button class="btn btn-sm btn-secondary" id="cancelRename">Cancel</button>
220
+ <button class="btn btn-sm btn-primary" id="confirmRename">Save</button>
221
+ </div>
222
+ </div>
223
+
224
+ <div id="confirmModal" class="custom-modal">
225
+ <h6>Confirm Action</h6>
226
+ <p id="confirmMessage" class="small"></p>
227
+ <div class="text-right">
228
+ <button class="btn btn-sm btn-secondary" id="cancelConfirm">Cancel</button>
229
+ <button class="btn btn-sm btn-primary" id="executeConfirm">OK</button>
230
+ </div>
231
+ </div>
232
+
187
233
  <script>
188
234
  (async () => {
189
235
  let currentConfig = {};
190
236
  let isPromptOpen = false;
237
+ let activeNodeId = null;
238
+ let activeUpdateData = null;
191
239
 
192
240
  const getInt = (id, def) => {
193
241
  const el = document.getElementById(id);
@@ -289,6 +337,22 @@
289
337
  }
290
338
  };
291
339
 
340
+ // Modal helpers
341
+ const showModal = (modalId) => {
342
+ document.getElementById('modalBackdrop').style.display = 'block';
343
+ document.getElementById(modalId).style.display = 'block';
344
+ isPromptOpen = true;
345
+ };
346
+
347
+ const hideModals = () => {
348
+ document.getElementById('modalBackdrop').style.display = 'none';
349
+ document.getElementById('renameModal').style.display = 'none';
350
+ document.getElementById('confirmModal').style.display = 'none';
351
+ isPromptOpen = false;
352
+ activeNodeId = null;
353
+ activeUpdateData = null;
354
+ };
355
+
292
356
  // Event Delegation for Table Actions (Rename & Firmware)
293
357
  document.getElementById('nodeTableBody').addEventListener('click', async (e) => {
294
358
  const target = e.target.closest('button');
@@ -300,21 +364,9 @@
300
364
  // 1. Rename Logic
301
365
  if (target.classList.contains('rename-node')) {
302
366
  const nameCell = document.getElementById(`node-name-${nodeId}`);
303
- const currentName = nameCell ? nameCell.textContent : '';
304
-
305
- isPromptOpen = true;
306
- const newName = prompt(`Enter new name for Node ${nodeId}:`, currentName);
307
- isPromptOpen = false;
308
-
309
- if (newName !== null && newName !== currentName && newName.trim() !== '') {
310
- try {
311
- await window.homebridge.request('rename-node', { nodeId, name: newName.trim() });
312
- window.homebridge.toast.success(`Node ${nodeId} renamed to ${newName}`);
313
- loadNodes();
314
- } catch (err) {
315
- window.homebridge.toast.error(err.message);
316
- }
317
- }
367
+ activeNodeId = nodeId;
368
+ document.getElementById('newLabelInput').value = nameCell ? nameCell.textContent : '';
369
+ showModal('renameModal');
318
370
  return;
319
371
  }
320
372
 
@@ -349,29 +401,47 @@
349
401
 
350
402
  // 3. Start Update Logic
351
403
  if (target.classList.contains('start-update')) {
352
- const updateData = JSON.parse(target.getAttribute('data-update'));
353
- const warning = 'WARNING: Firmware updates carry risk. Do not unplug the bridge or device during the process. Proceed?';
354
-
355
- isPromptOpen = true;
356
- const confirmed = confirm(warning);
357
- isPromptOpen = false;
358
-
359
- if (!confirmed) return;
404
+ activeUpdateData = JSON.parse(target.getAttribute('data-update'));
405
+ activeNodeId = nodeId;
406
+ document.getElementById('confirmMessage').textContent = 'WARNING: Firmware updates carry risk. Do not unplug the bridge or device during the process. Proceed?';
407
+ showModal('confirmModal');
408
+ }
409
+ });
360
410
 
361
- target.disabled = true;
362
- target.innerHTML = 'Starting...';
411
+ // Modal Button Handlers
412
+ document.getElementById('cancelRename').addEventListener('click', hideModals);
413
+ document.getElementById('confirmRename').addEventListener('click', async () => {
414
+ const newName = document.getElementById('newLabelInput').value.trim();
415
+ const nodeId = activeNodeId;
416
+ hideModals();
363
417
 
418
+ if (newName !== '') {
364
419
  try {
365
- await window.homebridge.request('start-update', { nodeId, update: updateData });
366
- window.homebridge.toast.success('Firmware update started!');
420
+ await window.homebridge.request('rename-node', { nodeId, name: newName });
421
+ window.homebridge.toast.success(`Node ${nodeId} renamed to ${newName}`);
367
422
  loadNodes();
368
423
  } catch (err) {
369
424
  window.homebridge.toast.error(err.message);
370
- loadNodes();
371
425
  }
372
426
  }
373
427
  });
374
428
 
429
+ document.getElementById('cancelConfirm').addEventListener('click', hideModals);
430
+ document.getElementById('executeConfirm').addEventListener('click', async () => {
431
+ const nodeId = activeNodeId;
432
+ const updateData = activeUpdateData;
433
+ hideModals();
434
+
435
+ try {
436
+ await window.homebridge.request('start-update', { nodeId, update: updateData });
437
+ window.homebridge.toast.success('Firmware update started!');
438
+ loadNodes();
439
+ } catch (err) {
440
+ window.homebridge.toast.error(err.message);
441
+ loadNodes();
442
+ }
443
+ });
444
+
375
445
  const init = async () => {
376
446
  if (!window.homebridge) return;
377
447
 
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": "2.3.0",
4
+ "version": "2.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": {