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.
- package/dist/accessories/ZWaveAccessory.d.ts +4 -0
- package/dist/accessories/ZWaveAccessory.js +18 -0
- package/dist/features/ZWaveFeature.d.ts +5 -0
- package/dist/features/ZWaveFeature.js +11 -0
- package/dist/platform/ZWaveUsbPlatform.js +7 -8
- package/dist/zwave/ZWaveController.js +2 -1
- package/homebridge-ui/public/index.html +98 -28
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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('
|
|
366
|
-
window.homebridge.toast.success(
|
|
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.
|
|
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": {
|