homebridge-zwave-usb 2.2.0 → 2.3.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.
@@ -1,388 +1,479 @@
1
- <ul class="nav nav-tabs mb-3" id="mainTabs" role="tablist">
2
- <li class="nav-item">
3
- <a class="nav-link active" id="config-tab" href="#config" role="tab">Configuration</a>
4
- </li>
5
- <li class="nav-item">
6
- <a class="nav-link" id="maintenance-tab" href="#maintenance" role="tab">Maintenance</a>
7
- </li>
8
- </ul>
9
-
10
- <div class="tab-content" id="mainTabsContent">
11
- <!-- Configuration Tab -->
12
- <div class="tab-pane fade show active" id="config" role="tabpanel">
13
- <div class="card card-body">
14
- <form id="configForm">
15
- <div class="form-group">
16
- <label for="name">Name</label>
17
- <input type="text" class="form-control" id="name" placeholder="Homebridge Z-Wave USB" />
18
- </div>
19
- <div class="form-group">
20
- <label for="serialPort">Serial Port Path</label>
21
- <input type="text" class="form-control" id="serialPort" placeholder="/dev/ttyACM0" required />
22
- <small class="form-text text-muted">
23
- WARNING: Do not use /dev/ttyUSB0. Use /dev/serial/by-id/...
24
- </small>
25
- </div>
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Z-Wave USB Plugin</title>
6
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
7
+ <style>
8
+ /* Homebridge Theme Compatibility */
9
+ :root {
10
+ --primary-color: #007bff;
11
+ --secondary-color: #6c757d;
12
+ --success-color: #28a745;
13
+ }
14
+
15
+ body {
16
+ background-color: transparent !important;
17
+ }
26
18
 
27
- <fieldset>
28
- <legend>Z-Wave Security Keys</legend>
29
- <p class="text-muted small">Required for secure devices (Locks, Sensors).</p>
30
- <button type="button" class="btn btn-outline-primary btn-sm mb-3" id="generateKeys">
31
- Auto-Generate Keys
32
- </button>
19
+ .card {
20
+ margin-bottom: 20px;
21
+ background-color: var(--hb-background-color, #fff);
22
+ color: var(--hb-text-color, #212529);
23
+ border: 1px solid rgba(0,0,0,.125);
24
+ }
33
25
 
34
- <div class="form-group">
35
- <label for="S0_Legacy">S0 Legacy</label>
36
- <input type="text" class="form-control" id="S0_Legacy" maxlength="32" />
37
- </div>
38
- <div class="form-group">
39
- <label for="S2_Unauthenticated">S2 Unauthenticated</label>
40
- <input type="text" class="form-control" id="S2_Unauthenticated" maxlength="32" />
41
- </div>
42
- <div class="form-group">
43
- <label for="S2_Authenticated">S2 Authenticated</label>
44
- <input type="text" class="form-control" id="S2_Authenticated" maxlength="32" />
45
- </div>
46
- <div class="form-group">
47
- <label for="S2_AccessControl">S2 Access Control</label>
48
- <input type="text" class="form-control" id="S2_AccessControl" maxlength="32" />
49
- </div>
26
+ .table {
27
+ color: inherit;
28
+ }
50
29
 
51
- <p class="text-muted small mt-3">
52
- Optional: Long Range specific keys (defaults to Classic S2 if empty)
53
- </p>
54
- <div class="form-group">
55
- <label for="S2_Authenticated_LR">S2 Authenticated (LR)</label>
56
- <input type="text" class="form-control" id="S2_Authenticated_LR" maxlength="32" />
57
- </div>
58
- <div class="form-group">
59
- <label for="S2_AccessControl_LR">S2 Access Control (LR)</label>
60
- <input type="text" class="form-control" id="S2_AccessControl_LR" maxlength="32" />
61
- </div>
62
- </fieldset>
30
+ .table thead th {
31
+ border-bottom: 2px solid var(--hb-border-color, #dee2e6);
32
+ color: var(--hb-text-color, #212529);
33
+ }
63
34
 
64
- <fieldset>
65
- <legend>Advanced</legend>
66
- <div class="form-group">
67
- <label for="inclusionTimeoutSeconds">Inclusion Timeout (Seconds)</label>
68
- <input type="number" class="form-control" id="inclusionTimeoutSeconds" placeholder="60" />
69
- </div>
35
+ .table td {
36
+ border-top: 1px solid var(--hb-border-color, #dee2e6);
37
+ vertical-align: middle;
38
+ color: var(--hb-text-color, #212529);
39
+ }
70
40
 
71
- <div class="form-check mt-3">
72
- <input type="checkbox" class="form-check-input" id="debug" />
73
- <label class="form-check-label" for="debug">Enable Verbose Driver Logging</label>
74
- </div>
75
- </fieldset>
76
- </form>
77
- </div>
78
- </div>
41
+ .badge {
42
+ padding: 0.4em 0.6em;
43
+ border-radius: 0.25rem;
44
+ font-weight: 700;
45
+ color: #fff !important; /* Force white text for badges */
46
+ }
47
+
48
+ .badge-success { background-color: var(--success-color); }
49
+ .badge-secondary { background-color: var(--secondary-color); }
50
+ .badge-light { background-color: #f8f9fa; color: #212529 !important; }
51
+
52
+ .nav-tabs { margin-bottom: 20px; border-bottom: 1px solid var(--hb-border-color, #dee2e6); }
53
+ .nav-link { color: var(--hb-text-color, #007bff); }
54
+ .nav-link.active {
55
+ background-color: var(--hb-background-color, #fff) !important;
56
+ color: var(--hb-text-color, #495057) !important;
57
+ border-color: var(--hb-border-color, #dee2e6) var(--hb-border-color, #dee2e6) transparent !important;
58
+ }
59
+
60
+ fieldset { border: 1px solid var(--hb-border-color, #ddd); padding: 15px; border-radius: 4px; margin-bottom: 15px; }
61
+ legend { width: auto; padding: 0 10px; font-size: 1.1rem; font-weight: bold; color: var(--hb-text-color, inherit); }
62
+
63
+ .text-muted { color: #6c757d !important; }
64
+ .form-control {
65
+ background-color: var(--hb-background-color, #fff);
66
+ color: var(--hb-text-color, #495057);
67
+ border: 1px solid var(--hb-border-color, #ced4da);
68
+ }
69
+
70
+ .btn-group .btn { margin-right: 2px; }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <div class="container-fluid p-3">
75
+ <ul class="nav nav-tabs mb-3" id="mainTabs" role="tablist">
76
+ <li class="nav-item">
77
+ <a class="nav-link active" id="config-tab" href="#config" role="tab">Configuration</a>
78
+ </li>
79
+ <li class="nav-item">
80
+ <a class="nav-link" id="maintenance-tab" href="#maintenance" role="tab">Maintenance</a>
81
+ </li>
82
+ </ul>
83
+
84
+ <div class="tab-content" id="mainTabsContent">
85
+ <!-- Configuration Tab -->
86
+ <div class="tab-pane fade show active" id="config" role="tabpanel">
87
+ <div class="card card-body">
88
+ <form id="configForm">
89
+ <div class="form-group">
90
+ <label for="name">Name</label>
91
+ <input type="text" class="form-control" id="name" placeholder="Homebridge Z-Wave USB" />
92
+ </div>
93
+ <div class="form-group">
94
+ <label for="serialPort">Serial Port Path</label>
95
+ <input type="text" class="form-control" id="serialPort" placeholder="/dev/ttyACM0" required />
96
+ <small class="form-text text-muted">
97
+ WARNING: Do not use /dev/ttyUSB0. Use /dev/serial/by-id/...
98
+ </small>
99
+ </div>
100
+
101
+ <fieldset>
102
+ <legend>Z-Wave Security Keys</legend>
103
+ <p class="text-muted small">Required for secure devices (Locks, Sensors).</p>
104
+ <button type="button" class="btn btn-outline-primary btn-sm mb-3" id="generateKeys">
105
+ Auto-Generate Keys
106
+ </button>
107
+
108
+ <div class="form-group">
109
+ <label for="S0_Legacy">S0 Legacy</label>
110
+ <input type="text" class="form-control" id="S0_Legacy" maxlength="32" />
111
+ </div>
112
+ <div class="form-group">
113
+ <label for="S2_Unauthenticated">S2 Unauthenticated</label>
114
+ <input type="text" class="form-control" id="S2_Unauthenticated" maxlength="32" />
115
+ </div>
116
+ <div class="form-group">
117
+ <label for="S2_Authenticated">S2 Authenticated</label>
118
+ <input type="text" class="form-control" id="S2_Authenticated" maxlength="32" />
119
+ </div>
120
+ <div class="form-group">
121
+ <label for="S2_AccessControl">S2 Access Control</label>
122
+ <input type="text" class="form-control" id="S2_AccessControl" maxlength="32" />
123
+ </div>
124
+
125
+ <p class="text-muted small mt-3">
126
+ Optional: Long Range specific keys (defaults to Classic S2 if empty)
127
+ </p>
128
+ <div class="form-group">
129
+ <label for="S2_Authenticated_LR">S2 Authenticated (LR)</label>
130
+ <input type="text" class="form-control" id="S2_Authenticated_LR" maxlength="32" />
131
+ </div>
132
+ <div class="form-group">
133
+ <label for="S2_AccessControl_LR">S2 Access Control (LR)</label>
134
+ <input type="text" class="form-control" id="S2_AccessControl_LR" maxlength="32" />
135
+ </div>
136
+ </fieldset>
137
+
138
+ <fieldset>
139
+ <legend>Advanced</legend>
140
+ <div class="form-group">
141
+ <label for="inclusionTimeoutSeconds">Inclusion Timeout (Seconds)</label>
142
+ <input type="number" class="form-control" id="inclusionTimeoutSeconds" placeholder="60" />
143
+ </div>
79
144
 
80
- <!-- Maintenance Tab -->
81
- <div class="tab-pane fade" id="maintenance" role="tabpanel">
82
- <div class="card card-body">
83
- <h5>Z-Wave Node Maintenance</h5>
84
- <p class="text-muted small">
85
- View and manage your Z-Wave nodes. Check for official firmware updates via Z-Wave JS Service.
86
- </p>
87
- <div class="alert alert-warning py-2 small" role="alert">
88
- <strong>Beta Feature:</strong> Z-Wave Firmware Updates are provided as-is. Use at your own risk. Feedback is welcome!
145
+ <div class="form-check mt-3">
146
+ <input type="checkbox" class="form-check-input" id="debug" />
147
+ <label class="form-check-label" for="debug">Enable Verbose Driver Logging</label>
148
+ </div>
149
+ </fieldset>
150
+ </form>
151
+ </div>
89
152
  </div>
90
- <div class="table-responsive">
91
- <table class="table table-sm" id="nodeTable">
92
- <thead>
93
- <tr>
94
- <th>ID</th>
95
- <th>Name</th>
96
- <th>Status</th>
97
- <th>Firmware</th>
98
- <th>Action</th>
99
- </tr>
100
- </thead>
101
- <tbody id="nodeTableBody">
102
- <tr>
103
- <td colspan="5" class="text-center">Loading nodes...</td>
104
- </tr>
105
- </tbody>
106
- </table>
153
+
154
+ <!-- Maintenance Tab -->
155
+ <div class="tab-pane fade" id="maintenance" role="tabpanel">
156
+ <div class="card card-body">
157
+ <h5>Z-Wave Node Maintenance</h5>
158
+ <p class="text-muted small">
159
+ View and manage your Z-Wave nodes. Check for official firmware updates via Z-Wave JS Service.
160
+ </p>
161
+ <div class="alert alert-warning py-2 small" role="alert">
162
+ <strong>Beta Feature:</strong> Z-Wave Firmware Updates are provided as-is. Use at your own risk. Feedback is welcome!
163
+ </div>
164
+ <div class="table-responsive">
165
+ <table class="table table-sm" id="nodeTable">
166
+ <thead>
167
+ <tr>
168
+ <th>ID</th>
169
+ <th>Name</th>
170
+ <th>Status</th>
171
+ <th>Firmware</th>
172
+ <th>Action</th>
173
+ </tr>
174
+ </thead>
175
+ <tbody id="nodeTableBody">
176
+ <tr>
177
+ <td colspan="5" class="text-center">Loading nodes...</td>
178
+ </tr>
179
+ </tbody>
180
+ </table>
181
+ </div>
182
+ </div>
107
183
  </div>
108
184
  </div>
109
185
  </div>
110
- </div>
111
-
112
- <script>
113
- (async () => {
114
- let currentConfig = {};
115
-
116
- const getInt = (id, def) => {
117
- const el = document.getElementById(id);
118
- if (!el) return def;
119
- const val = parseInt(el.value, 10);
120
- return isNaN(val) ? def : val;
121
- };
122
-
123
- const getConfig = () => {
124
- const securityKeys = {
125
- S0_Legacy: document.getElementById('S0_Legacy').value,
126
- S2_Unauthenticated: document.getElementById('S2_Unauthenticated').value,
127
- S2_Authenticated: document.getElementById('S2_Authenticated').value,
128
- S2_AccessControl: document.getElementById('S2_AccessControl').value,
129
- S2_Authenticated_LR: document.getElementById('S2_Authenticated_LR').value,
130
- S2_AccessControl_LR: document.getElementById('S2_AccessControl_LR').value,
131
- };
132
186
 
133
- const newConfig = {
134
- ...currentConfig,
135
- name: document.getElementById('name').value,
136
- platform: 'ZWaveUSB',
137
- serialPort: document.getElementById('serialPort').value,
138
- inclusionTimeoutSeconds: getInt('inclusionTimeoutSeconds', 60),
139
- debug: document.getElementById('debug').checked,
140
- };
187
+ <script>
188
+ (async () => {
189
+ let currentConfig = {};
190
+ let isPromptOpen = false;
141
191
 
142
- if (Object.values(securityKeys).some((k) => k && k.length > 0)) {
143
- newConfig.securityKeys = securityKeys;
144
- } else {
145
- delete newConfig.securityKeys;
146
- }
192
+ const getInt = (id, def) => {
193
+ const el = document.getElementById(id);
194
+ if (!el) return def;
195
+ const val = parseInt(el.value, 10);
196
+ return isNaN(val) ? def : val;
197
+ };
147
198
 
148
- return newConfig;
149
- };
199
+ const getConfig = () => {
200
+ const securityKeys = {
201
+ S0_Legacy: document.getElementById('S0_Legacy').value,
202
+ S2_Unauthenticated: document.getElementById('S2_Unauthenticated').value,
203
+ S2_Authenticated: document.getElementById('S2_Authenticated').value,
204
+ S2_AccessControl: document.getElementById('S2_AccessControl').value,
205
+ S2_Authenticated_LR: document.getElementById('S2_Authenticated_LR').value,
206
+ S2_AccessControl_LR: document.getElementById('S2_AccessControl_LR').value,
207
+ };
150
208
 
151
- const updateConfig = async () => {
152
- if (window.homebridge) {
153
- const newConfig = getConfig();
154
- await window.homebridge.updatePluginConfig([newConfig]);
155
- }
156
- };
209
+ const newConfig = {
210
+ ...currentConfig,
211
+ name: document.getElementById('name').value,
212
+ platform: 'ZWaveUSB',
213
+ serialPort: document.getElementById('serialPort').value,
214
+ inclusionTimeoutSeconds: getInt('inclusionTimeoutSeconds', 60),
215
+ debug: document.getElementById('debug').checked,
216
+ };
157
217
 
158
- const statusMap = ['Unknown', 'Asleep', 'Awake', 'Dead', 'Alive'];
218
+ if (Object.values(securityKeys).some((k) => k && k.length > 0)) {
219
+ newConfig.securityKeys = securityKeys;
220
+ } else {
221
+ delete newConfig.securityKeys;
222
+ }
159
223
 
160
- const loadNodes = async () => {
161
- try {
162
- const nodes = await window.homebridge.request('get-nodes');
163
- const tbody = document.getElementById('nodeTableBody');
164
- tbody.innerHTML = '';
224
+ return newConfig;
225
+ };
165
226
 
166
- if (!Array.isArray(nodes)) {
167
- tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Plugin not responding. Is it running?</td></tr>';
168
- return;
227
+ const updateConfig = async () => {
228
+ if (window.homebridge) {
229
+ const newConfig = getConfig();
230
+ await window.homebridge.updatePluginConfig([newConfig]);
169
231
  }
232
+ };
170
233
 
171
- nodes.forEach((node) => {
172
- const tr = document.createElement('tr');
173
- const status = statusMap[node.status] || 'Unknown';
174
- const progress = node.firmwareProgress;
175
- const nodeName = node.name || node.label || 'Node ' + node.nodeId;
176
-
177
- let actionHtml = '';
178
- if (progress) {
179
- const pct = Math.round((progress.sent / progress.total) * 100);
180
- actionHtml = `
181
- <div class="progress" style="height: 20px;">
182
- <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: ${pct}%">${pct}%</div>
183
- </div>
184
- `;
185
- } else if (node.nodeId === 1) {
186
- actionHtml = `<span class="badge badge-light">Controller</span>`;
187
- } else {
188
- actionHtml = `
189
- <div class="btn-group">
190
- <button class="btn btn-sm btn-outline-secondary rename-node" data-nodeid="${node.nodeId}" data-name="${nodeName}">Rename</button>
191
- <button class="btn btn-sm btn-outline-primary check-update" data-nodeid="${node.nodeId}">Check Update</button>
192
- </div>
193
- `;
194
- }
234
+ const statusMap = ['Unknown', 'Asleep', 'Awake', 'Dead', 'Alive'];
195
235
 
196
- tr.innerHTML = `
197
- <td>${node.nodeId}</td>
198
- <td id="node-name-${node.nodeId}">${nodeName}</td>
199
- <td><span class="badge badge-${status === 'Alive' || status === 'Awake' ? 'success' : 'secondary'}">${status}</span></td>
200
- <td>${node.firmwareVersion || 'Unknown'}</td>
201
- <td id="node-action-${node.nodeId}">${actionHtml}</td>
202
- `;
203
- tbody.appendChild(tr);
204
- });
236
+ const loadNodes = async () => {
237
+ if (isPromptOpen) return;
238
+ try {
239
+ const nodes = await window.homebridge.request('get-nodes');
240
+ const tbody = document.getElementById('nodeTableBody');
241
+ if (!tbody) return;
242
+
243
+ tbody.innerHTML = '';
244
+
245
+ if (!Array.isArray(nodes)) {
246
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center text-danger">Plugin not responding. Is it running?</td></tr>';
247
+ return;
248
+ }
205
249
 
206
- // Add event listeners for rename buttons
207
- document.querySelectorAll('.rename-node').forEach((btn) => {
208
- btn.addEventListener('click', async (e) => {
209
- const nodeId = e.target.getAttribute('data-nodeid');
210
- const currentName = e.target.getAttribute('data-name');
211
- const newName = prompt(`Enter new name for Node ${nodeId}:`, currentName);
212
-
213
- if (newName !== null && newName !== currentName && newName.trim() !== '') {
214
- try {
215
- await window.homebridge.request('rename-node', { nodeId, name: newName.trim() });
216
- window.homebridge.toast.success(`Node ${nodeId} renamed to ${newName}`);
217
- loadNodes();
218
- } catch (err) {
219
- window.homebridge.toast.error(err.message);
220
- }
250
+ nodes.forEach((node) => {
251
+ const tr = document.createElement('tr');
252
+ const status = statusMap[node.status] || 'Unknown';
253
+ const progress = node.firmwareProgress;
254
+ const nodeName = node.name || node.label || 'Node ' + node.nodeId;
255
+
256
+ let actionHtml = '';
257
+ if (progress) {
258
+ const pct = Math.round((progress.sent / progress.total) * 100);
259
+ actionHtml = `
260
+ <div class="progress" style="height: 20px;">
261
+ <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: ${pct}%">${pct}%</div>
262
+ </div>
263
+ `;
264
+ } else if (node.nodeId === 1) {
265
+ actionHtml = `<span class="badge badge-light">Controller</span>`;
266
+ } else {
267
+ actionHtml = `
268
+ <div class="btn-group">
269
+ <button class="btn btn-sm btn-outline-secondary rename-node" data-nodeid="${node.nodeId}">Rename</button>
270
+ <button class="btn btn-sm btn-outline-primary check-update" data-nodeid="${node.nodeId}">Check Update</button>
271
+ </div>
272
+ `;
221
273
  }
274
+
275
+ tr.innerHTML = `
276
+ <td>${node.nodeId}</td>
277
+ <td id="node-name-${node.nodeId}">${nodeName}</td>
278
+ <td><span class="badge badge-${status === 'Alive' || status === 'Awake' ? 'success' : 'secondary'}">${status}</span></td>
279
+ <td>${node.firmwareVersion || 'Unknown'}</td>
280
+ <td id="node-action-${node.nodeId}">${actionHtml}</td>
281
+ `;
282
+ tbody.appendChild(tr);
222
283
  });
223
- });
284
+ } catch (err) {
285
+ const tbody = document.getElementById('nodeTableBody');
286
+ if (tbody) {
287
+ tbody.innerHTML = `<tr><td colspan="5" class="text-center text-danger">${err.message}</td></tr>`;
288
+ }
289
+ }
290
+ };
224
291
 
225
- // Add event listeners for check buttons
226
- document.querySelectorAll('.check-update').forEach((btn) => {
227
- btn.addEventListener('click', async (e) => {
228
- const nodeId = e.target.getAttribute('data-nodeid');
229
- const node = nodes.find(n => n.nodeId == nodeId);
230
-
231
- e.target.disabled = true;
232
- e.target.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
233
-
292
+ // Event Delegation for Table Actions (Rename & Firmware)
293
+ document.getElementById('nodeTableBody').addEventListener('click', async (e) => {
294
+ const target = e.target.closest('button');
295
+ if (!target) return;
296
+
297
+ const nodeId = target.getAttribute('data-nodeid');
298
+ if (!nodeId) return;
299
+
300
+ // 1. Rename Logic
301
+ if (target.classList.contains('rename-node')) {
302
+ 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() !== '') {
234
310
  try {
235
- const updates = await window.homebridge.request('check-firmware', nodeId);
236
- if (updates && updates.length > 0) {
237
- const update = updates[0];
238
- const actionCell = document.getElementById(`node-action-${nodeId}`);
239
- actionCell.innerHTML = `
240
- <button class="btn btn-sm btn-success start-update" data-nodeid="${nodeId}" data-update='${JSON.stringify(update)}'>
241
- Update to v${update.version}
242
- </button>
243
- <div class="small text-muted mt-1">Found update!</div>
244
- `;
245
-
246
- actionCell.querySelector('.start-update').addEventListener('click', async (e) => {
247
- const isBattery = !node.isListening && !node.isFrequentListening;
248
- const warning = isBattery
249
- ? 'WARNING: This is a battery-powered device. You MUST manually wake it up (usually by pressing a button on the device) immediately after clicking OK to receive the update. Proceed?'
250
- : 'WARNING: Firmware updates carry risk. Do not unplug the bridge or device during the process. Proceed?';
251
-
252
- if (!confirm(warning)) return;
253
-
254
- const target = e.currentTarget;
255
- const u = JSON.parse(target.getAttribute('data-update'));
256
- target.disabled = true;
257
- target.innerHTML = 'Starting...';
258
-
259
- try {
260
- await window.homebridge.request('start-update', { nodeId, update: u });
261
- window.homebridge.toast.success('Firmware update started!');
262
- loadNodes();
263
- } catch (err) {
264
- window.homebridge.toast.error(err.message);
265
- loadNodes();
266
- }
267
- });
268
- } else {
269
- window.homebridge.toast.info('No updates available for this device.');
270
- e.target.disabled = false;
271
- e.target.innerHTML = 'Check Update';
272
- }
311
+ await window.homebridge.request('rename-node', { nodeId, name: newName.trim() });
312
+ window.homebridge.toast.success(`Node ${nodeId} renamed to ${newName}`);
313
+ loadNodes();
273
314
  } catch (err) {
274
315
  window.homebridge.toast.error(err.message);
275
- e.target.disabled = false;
276
- e.target.innerHTML = 'Check Update';
277
316
  }
278
- });
279
- });
280
- } catch (err) {
281
- const tbody = document.getElementById('nodeTableBody');
282
- tbody.innerHTML = `<tr><td colspan="5" class="text-center text-danger">${err.message}</td></tr>`;
283
- }
284
- };
285
-
286
- const init = async () => {
287
- if (!window.homebridge) return;
288
-
289
- // Load Config
290
- const pluginConfig = await window.homebridge.getPluginConfig();
291
- currentConfig = pluginConfig[0] || { platform: 'ZWaveUSB' };
292
- const config = currentConfig;
293
-
294
- // Populate Form
295
- document.getElementById('name').value = config.name || 'Homebridge Z-Wave USB';
296
- document.getElementById('serialPort').value = config.serialPort || '';
297
-
298
- // Keys
299
- const keys = config.securityKeys || {};
300
- document.getElementById('S0_Legacy').value = keys.S0_Legacy || '';
301
- document.getElementById('S2_Unauthenticated').value = keys.S2_Unauthenticated || '';
302
- document.getElementById('S2_Authenticated').value = keys.S2_Authenticated || '';
303
- document.getElementById('S2_AccessControl').value = keys.S2_AccessControl || '';
304
- document.getElementById('S2_Authenticated_LR').value = keys.S2_Authenticated_LR || '';
305
- document.getElementById('S2_AccessControl_LR').value = keys.S2_AccessControl_LR || '';
306
-
307
- // Advanced
308
- document.getElementById('inclusionTimeoutSeconds').value =
309
- config.inclusionTimeoutSeconds || 60;
310
- document.getElementById('debug').checked = config.debug || false;
311
-
312
- // Auto-save on change
313
- document.getElementById('configForm').addEventListener('input', updateConfig);
314
- document.getElementById('configForm').addEventListener('change', updateConfig);
315
-
316
- // Generate Keys
317
- document.getElementById('generateKeys').addEventListener('click', () => {
318
- if (document.getElementById('S0_Legacy').value && !confirm('Overwrite existing keys?'))
317
+ }
319
318
  return;
319
+ }
320
320
 
321
- const gen = () => {
322
- const arr = new Uint8Array(16);
323
- window.crypto.getRandomValues(arr);
324
- return Array.from(arr, (b) => b.toString(16).padStart(2, '0'))
325
- .join('')
326
- .toUpperCase();
327
- };
321
+ // 2. Check Update Logic
322
+ if (target.classList.contains('check-update')) {
323
+ target.disabled = true;
324
+ target.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
325
+
326
+ try {
327
+ const updates = await window.homebridge.request('check-firmware', nodeId);
328
+ if (updates && updates.length > 0) {
329
+ const update = updates[0];
330
+ const actionCell = document.getElementById(`node-action-${nodeId}`);
331
+ actionCell.innerHTML = `
332
+ <button class="btn btn-sm btn-success start-update" data-nodeid="${nodeId}" data-update='${JSON.stringify(update)}'>
333
+ Update to v${update.version}
334
+ </button>
335
+ <div class="small text-muted mt-1">Found update!</div>
336
+ `;
337
+ } else {
338
+ window.homebridge.toast.info('No updates available for this device.');
339
+ target.disabled = false;
340
+ target.innerHTML = 'Check Update';
341
+ }
342
+ } catch (err) {
343
+ window.homebridge.toast.error(err.message);
344
+ target.disabled = false;
345
+ target.innerHTML = 'Check Update';
346
+ }
347
+ return;
348
+ }
328
349
 
329
- document.getElementById('S0_Legacy').value = gen();
330
- document.getElementById('S2_Unauthenticated').value = gen();
331
- document.getElementById('S2_Authenticated').value = gen();
332
- document.getElementById('S2_AccessControl').value = gen();
333
- document.getElementById('S2_Authenticated_LR').value = gen();
334
- document.getElementById('S2_AccessControl_LR').value = gen();
350
+ // 3. Start Update Logic
351
+ 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?';
335
354
 
336
- updateConfig();
337
- window.homebridge.toast.success('Keys generated and configuration updated!');
338
- });
355
+ isPromptOpen = true;
356
+ const confirmed = confirm(warning);
357
+ isPromptOpen = false;
358
+
359
+ if (!confirmed) return;
339
360
 
340
- // Save event listener as a backup
341
- window.homebridge.addEventListener('submitting', async () => {
342
- await updateConfig();
343
- });
361
+ target.disabled = true;
362
+ target.innerHTML = 'Starting...';
344
363
 
345
- // Initialize nodes
346
- loadNodes();
347
- setInterval(() => {
348
- if (document.getElementById('maintenance').classList.contains('active')) {
349
- loadNodes();
364
+ try {
365
+ await window.homebridge.request('start-update', { nodeId, update: updateData });
366
+ window.homebridge.toast.success('Firmware update started!');
367
+ loadNodes();
368
+ } catch (err) {
369
+ window.homebridge.toast.error(err.message);
370
+ loadNodes();
371
+ }
350
372
  }
351
- }, 5000);
373
+ });
352
374
 
353
- // Manual tab switching logic for environments without full Bootstrap JS
354
- document.querySelectorAll('#mainTabs .nav-link').forEach((tab) => {
355
- tab.addEventListener('click', (e) => {
356
- e.preventDefault();
357
- const targetId = tab.getAttribute('href').replace('#', '');
358
-
359
- // Update Tab Active State
360
- document.querySelectorAll('#mainTabs .nav-link').forEach((t) => t.classList.remove('active'));
361
- tab.classList.add('active');
375
+ const init = async () => {
376
+ if (!window.homebridge) return;
377
+
378
+ // Load Config
379
+ const pluginConfig = await window.homebridge.getPluginConfig();
380
+ currentConfig = pluginConfig[0] || { platform: 'ZWaveUSB' };
381
+ const config = currentConfig;
382
+
383
+ // Populate Form
384
+ document.getElementById('name').value = config.name || 'Homebridge Z-Wave USB';
385
+ document.getElementById('serialPort').value = config.serialPort || '';
386
+
387
+ // Keys
388
+ const keys = config.securityKeys || {};
389
+ document.getElementById('S0_Legacy').value = keys.S0_Legacy || '';
390
+ document.getElementById('S2_Unauthenticated').value = keys.S2_Unauthenticated || '';
391
+ document.getElementById('S2_Authenticated').value = keys.S2_Authenticated || '';
392
+ document.getElementById('S2_AccessControl').value = keys.S2_AccessControl || '';
393
+ document.getElementById('S2_Authenticated_LR').value = keys.S2_Authenticated_LR || '';
394
+ document.getElementById('S2_AccessControl_LR').value = keys.S2_AccessControl_LR || '';
395
+
396
+ // Advanced
397
+ document.getElementById('inclusionTimeoutSeconds').value =
398
+ config.inclusionTimeoutSeconds || 60;
399
+ document.getElementById('debug').checked = config.debug || false;
400
+
401
+ // Auto-save on change
402
+ document.getElementById('configForm').addEventListener('input', updateConfig);
403
+ document.getElementById('configForm').addEventListener('change', updateConfig);
404
+
405
+ // Generate Keys
406
+ document.getElementById('generateKeys').addEventListener('click', () => {
407
+ if (document.getElementById('S0_Legacy').value && !confirm('Overwrite existing keys?'))
408
+ return;
409
+
410
+ const gen = () => {
411
+ const arr = new Uint8Array(16);
412
+ window.crypto.getRandomValues(arr);
413
+ return Array.from(arr, (b) => b.toString(16).padStart(2, '0'))
414
+ .join('')
415
+ .toUpperCase();
416
+ };
417
+
418
+ document.getElementById('S0_Legacy').value = gen();
419
+ document.getElementById('S2_Unauthenticated').value = gen();
420
+ document.getElementById('S2_Authenticated').value = gen();
421
+ document.getElementById('S2_AccessControl').value = gen();
422
+ document.getElementById('S2_Authenticated_LR').value = gen();
423
+ document.getElementById('S2_AccessControl_LR').value = gen();
424
+
425
+ updateConfig();
426
+ window.homebridge.toast.success('Keys generated and configuration updated!');
427
+ });
362
428
 
363
- // Update Content Active State
364
- document.querySelectorAll('.tab-pane').forEach((pane) => {
365
- pane.classList.remove('show', 'active');
366
- });
367
- const targetPane = document.getElementById(targetId);
368
- if (targetPane) {
369
- targetPane.classList.add('show', 'active');
370
- }
429
+ // Save event listener as a backup
430
+ window.homebridge.addEventListener('submitting', async () => {
431
+ await updateConfig();
432
+ });
371
433
 
372
- if (targetId === 'maintenance') {
434
+ // Initialize nodes
435
+ loadNodes();
436
+ setInterval(() => {
437
+ if (document.getElementById('maintenance').classList.contains('active')) {
373
438
  loadNodes();
374
439
  }
440
+ }, 5000);
441
+
442
+ // Manual tab switching logic for environments without full Bootstrap JS
443
+ document.querySelectorAll('#mainTabs .nav-link').forEach((tab) => {
444
+ tab.addEventListener('click', (e) => {
445
+ e.preventDefault();
446
+ const targetId = tab.getAttribute('href').replace('#', '');
447
+
448
+ // Update Tab Active State
449
+ document.querySelectorAll('#mainTabs .nav-link').forEach((t) => t.classList.remove('active'));
450
+ tab.classList.add('active');
451
+
452
+ // Update Content Active State
453
+ document.querySelectorAll('.tab-pane').forEach((pane) => {
454
+ pane.classList.remove('show', 'active');
455
+ });
456
+ const targetPane = document.getElementById(targetId);
457
+ if (targetPane) {
458
+ targetPane.classList.add('show', 'active');
459
+ }
460
+
461
+ if (targetId === 'maintenance') {
462
+ loadNodes();
463
+ }
464
+ });
375
465
  });
376
- });
377
- };
378
-
379
- // Try to initialize
380
- if (window.homebridge && window.homebridge.getPluginConfig) {
381
- init();
382
- } else if (window.homebridge) {
383
- window.homebridge.addEventListener('ready', init);
384
- } else {
385
- window.addEventListener('homebridgeReady', init);
386
- }
387
- })();
388
- </script>
466
+ };
467
+
468
+ // Try to initialize
469
+ if (window.homebridge && window.homebridge.getPluginConfig) {
470
+ init();
471
+ } else if (window.homebridge) {
472
+ window.homebridge.addEventListener('ready', init);
473
+ } else {
474
+ window.addEventListener('homebridgeReady', init);
475
+ }
476
+ })();
477
+ </script>
478
+ </body>
479
+ </html>
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.2.0",
4
+ "version": "2.3.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": {