homebridge-zwave-usb 2.1.0 → 2.2.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
@@ -15,6 +15,7 @@ A high-performance, production-grade [Homebridge](https://homebridge.io) integra
15
15
  - **Reactive Architecture**: Real-time status updates and automatic hardware recovery (hot-plugging support).
16
16
  - **Advanced Metadata Repair**: Automatically cleans up obsolete HomeKit services/characteristics during network updates.
17
17
  - **Log Piping**: Internal Z-Wave JS driver logs are piped directly into the Homebridge terminal for seamless debugging.
18
+ - **OTA Firmware Updates**: Check for and install official manufacturer firmware updates directly from the Homebridge UI.
18
19
 
19
20
  ---
20
21
 
@@ -136,6 +137,15 @@ The plugin provides additional tools for network maintenance:
136
137
  - **Dual S2 PIN Entry**: When a device requires a PIN, you can enter it via the Homebridge terminal (`echo "12345" > s2_pin.txt`) or via the `S2 PIN Entry` characteristic in third-party HomeKit apps.
137
138
  - **Automated Reconciliation**: Orphaned accessories are automatically removed from HomeKit 60 seconds after startup if the node is no longer present in the Z-Wave network.
138
139
 
140
+ ## 🛠️ Maintenance & Firmware Updates
141
+
142
+ The plugin includes a dedicated **Maintenance** tab within the Homebridge custom UI (accessible via the plugin settings).
143
+
144
+ - **Node Overview**: View a complete list of all Z-Wave nodes in your network, including their status and current firmware versions.
145
+ - **Official Updates**: Click "Check Update" to query the official Z-Wave JS Firmware Update Service. If a manufacturer-approved update is available, you can start the OTA transfer with a single click.
146
+ - **Battery Device Support**: Firmware updates are supported for battery-powered devices. You will be prompted to manually wake the device after starting the process to initiate the transfer.
147
+ - **Real-time Progress**: Monitor the progress of the firmware transfer via a live progress bar.
148
+
139
149
  ## ❓ Troubleshooting
140
150
 
141
151
  - **Device showing "No Response"?** Check the Homebridge logs. If the device is battery-powered, it may need to be "woken up" (usually by pressing a physical button on the device) to complete the interview.
@@ -125,6 +125,34 @@ class ZWaveUsbPlatform {
125
125
  }));
126
126
  return sendJson(nodes);
127
127
  }
128
+ if (normalizedUrl.startsWith('/nodes/') && normalizedUrl.endsWith('/name') && method === 'POST') {
129
+ let body = '';
130
+ req.on('data', (chunk) => (body += chunk));
131
+ req.on('end', () => {
132
+ try {
133
+ const parts = normalizedUrl.split('/');
134
+ const nodeId = parseInt(parts[2], 10);
135
+ const { name } = JSON.parse(body);
136
+ if (!this.zwaveController) {
137
+ throw new Error('Controller not initialized');
138
+ }
139
+ this.zwaveController.setNodeName(nodeId, name);
140
+ // Update Homebridge Accessory Name
141
+ const accessory = this.zwaveAccessories.get(nodeId);
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]);
146
+ }
147
+ return sendJson({ success: true });
148
+ }
149
+ catch (err) {
150
+ this.log.error(`Failed to rename node: ${err}`);
151
+ return sendJson({ error: err instanceof Error ? err.message : String(err) }, 500);
152
+ }
153
+ });
154
+ return;
155
+ }
128
156
  if (normalizedUrl.startsWith('/firmware/updates/') && method === 'GET') {
129
157
  const nodeId = parseInt(normalizedUrl.split('/').pop() || '0', 10);
130
158
  this.zwaveController?.getAvailableFirmwareUpdates(nodeId)
@@ -54,6 +54,11 @@ export declare class ZWaveController extends EventEmitter implements IZWaveContr
54
54
  * that can no longer be physically excluded.
55
55
  */
56
56
  removeFailedNode(nodeId: number): Promise<void>;
57
+ /**
58
+ * Updates the user-defined name for a node in the Z-Wave network.
59
+ * This name is persisted by Z-Wave JS in its cache files.
60
+ */
61
+ setNodeName(nodeId: number, name: string): void;
57
62
  getAvailableFirmwareUpdates(nodeId: number): Promise<unknown[]>;
58
63
  beginFirmwareUpdate(nodeId: number, update: unknown): Promise<void>;
59
64
  abortFirmwareUpdate(nodeId: number): Promise<void>;
@@ -670,6 +670,18 @@ class ZWaveController extends events_1.EventEmitter {
670
670
  throw err;
671
671
  }
672
672
  }
673
+ /**
674
+ * Updates the user-defined name for a node in the Z-Wave network.
675
+ * This name is persisted by Z-Wave JS in its cache files.
676
+ */
677
+ setNodeName(nodeId, name) {
678
+ const node = this.nodes.get(nodeId);
679
+ if (!node) {
680
+ throw new Error(`Node ${nodeId} not found`);
681
+ }
682
+ this.log.info(`Updating name for Node ${nodeId} to: "${name}"`);
683
+ node.name = name;
684
+ }
673
685
  async getAvailableFirmwareUpdates(nodeId) {
674
686
  if (nodeId === 1) {
675
687
  this.log.debug('Node 1 is the controller; skipping firmware update check.');
@@ -20,6 +20,7 @@ export interface IZWaveController extends EventEmitter {
20
20
  startHealing(): Promise<boolean>;
21
21
  stopHealing(): Promise<boolean>;
22
22
  removeFailedNode(nodeId: number): Promise<void>;
23
+ setNodeName(nodeId: number, name: string): void;
23
24
  setS2Pin(pin: string): void;
24
25
  getAvailableFirmwareUpdates(nodeId: number): Promise<unknown[]>;
25
26
  beginFirmwareUpdate(nodeId: number, update: unknown): Promise<void>;
@@ -172,6 +172,7 @@
172
172
  const tr = document.createElement('tr');
173
173
  const status = statusMap[node.status] || 'Unknown';
174
174
  const progress = node.firmwareProgress;
175
+ const nodeName = node.name || node.label || 'Node ' + node.nodeId;
175
176
 
176
177
  let actionHtml = '';
177
178
  if (progress) {
@@ -184,12 +185,17 @@
184
185
  } else if (node.nodeId === 1) {
185
186
  actionHtml = `<span class="badge badge-light">Controller</span>`;
186
187
  } else {
187
- actionHtml = `<button class="btn btn-sm btn-outline-primary check-update" data-nodeid="${node.nodeId}">Check Update</button>`;
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
+ `;
188
194
  }
189
195
 
190
196
  tr.innerHTML = `
191
197
  <td>${node.nodeId}</td>
192
- <td>${node.name || node.label || 'Node ' + node.nodeId}</td>
198
+ <td id="node-name-${node.nodeId}">${nodeName}</td>
193
199
  <td><span class="badge badge-${status === 'Alive' || status === 'Awake' ? 'success' : 'secondary'}">${status}</span></td>
194
200
  <td>${node.firmwareVersion || 'Unknown'}</td>
195
201
  <td id="node-action-${node.nodeId}">${actionHtml}</td>
@@ -197,6 +203,25 @@
197
203
  tbody.appendChild(tr);
198
204
  });
199
205
 
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
+ }
221
+ }
222
+ });
223
+ });
224
+
200
225
  // Add event listeners for check buttons
201
226
  document.querySelectorAll('.check-update').forEach((btn) => {
202
227
  btn.addEventListener('click', async (e) => {
@@ -11,6 +11,10 @@ class UiServer extends HomebridgePluginUiServer {
11
11
  return await this.ipcRequest('/nodes', 'GET');
12
12
  });
13
13
 
14
+ this.onRequest('rename-node', async ({ nodeId, name }) => {
15
+ return await this.ipcRequest(`/nodes/${nodeId}/name`, 'POST', { name });
16
+ });
17
+
14
18
  this.onRequest('check-firmware', async (nodeId) => {
15
19
  return await this.ipcRequest(`/firmware/updates/${nodeId}`, 'GET');
16
20
  });
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.1.0",
4
+ "version": "2.2.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": {