Skip to content

Commit e8790e5

Browse files
raman325claude
andcommitted
feat: add schema 47 zniffer, utility, and broadcast commands (4/4)
Add zniffer Long Range and file I/O commands, utility firmware commands, and broadcast node Long Range support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9f657b0 commit e8790e5

12 files changed

Lines changed: 281 additions & 12 deletions

API_SCHEMA.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,7 @@ Base schema.
131131
- Added node event: `check link reliability progress`
132132
- Added controller events: `joining network show dsk`, `joining network done`
133133
- Added endpoint commands: `get_ccs`, `may_support_basic_cc`, `was_cc_removed_via_config`
134+
- Added broadcast node Long Range support via `longRange` flag on `broadcast_node` commands
135+
- Added zniffer Long Range commands: `get_lr_regions`, `get_current_lr_channel_config`, `get_supported_lr_channel_configs`, `set_lr_channel_config`
136+
- Added zniffer file I/O commands: `save_capture_to_file`, `load_capture_from_file`, `load_capture_from_buffer`
137+
- Added utility commands: `guess_firmware_file_format`, `try_unzip_firmware_file`, `extract_firmware`

README.md

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1489,7 +1489,9 @@ interface {
14891489

14901490
### Multicasting
14911491

1492-
There are several commands available that can be multicast to multiple nodes simultaneously. If you would like to broadcast to all nodes, use the `broadcast_node` prefix for the following commands. If you would like to multicast to a subset of nodes, use the `multicast_group` prefix for the following commands, adding a `nodeIDs` list as an input parameter:
1492+
There are several commands available that can be multicast to multiple nodes simultaneously. If you would like to broadcast to all nodes, use the `broadcast_node` prefix for the following commands. If you would like to multicast to a subset of nodes, use the `multicast_group` prefix for the following commands, adding a `nodeIDs` list as an input parameter.
1493+
1494+
> NOTE: For `broadcast_node` commands, you can pass a `longRange` boolean flag to broadcast to Long Range nodes instead of classic Z-Wave nodes.
14931495
14941496
```ts
14951497
interface IncomingCommandMulticastGroupBase extends IncomingCommandBase {
@@ -1757,6 +1759,101 @@ interface {
17571759
}
17581760
```
17591761

1762+
#### Get Long Range regions
1763+
1764+
Gets list of Long Range capable regions.
1765+
1766+
[compatible with schema version: 47+]
1767+
1768+
```ts
1769+
interface {
1770+
messageId: string;
1771+
command: "zniffer.get_lr_regions";
1772+
}
1773+
```
1774+
1775+
#### Get current Long Range channel config
1776+
1777+
Gets currently configured Long Range channel configuration.
1778+
1779+
[compatible with schema version: 47+]
1780+
1781+
```ts
1782+
interface {
1783+
messageId: string;
1784+
command: "zniffer.get_current_lr_channel_config";
1785+
}
1786+
```
1787+
1788+
#### Get supported Long Range channel configs
1789+
1790+
Gets map of supported Long Range channel configurations.
1791+
1792+
[compatible with schema version: 47+]
1793+
1794+
```ts
1795+
interface {
1796+
messageId: string;
1797+
command: "zniffer.get_supported_lr_channel_configs";
1798+
}
1799+
```
1800+
1801+
#### Set Long Range channel config
1802+
1803+
Set Long Range channel configuration (800 series only).
1804+
1805+
[compatible with schema version: 47+]
1806+
1807+
```ts
1808+
interface {
1809+
messageId: string;
1810+
command: "zniffer.set_lr_channel_config";
1811+
channelConfig: number;
1812+
}
1813+
```
1814+
1815+
#### [Save capture to file](https://zwave-js.github.io/node-zwave-js/#/api/zniffer?id=capturing-frames)
1816+
1817+
Saves the captured frames to a `.zlf` file that can be read by the official Zniffer application.
1818+
1819+
[compatible with schema version: 47+]
1820+
1821+
```ts
1822+
interface {
1823+
messageId: string;
1824+
command: "zniffer.save_capture_to_file";
1825+
filePath: string;
1826+
}
1827+
```
1828+
1829+
#### [Load capture from file](https://zwave-js.github.io/node-zwave-js/#/api/zniffer?id=capturing-frames)
1830+
1831+
Loads previously saved captured frames from a `.zlf` file.
1832+
1833+
[compatible with schema version: 47+]
1834+
1835+
```ts
1836+
interface {
1837+
messageId: string;
1838+
command: "zniffer.load_capture_from_file";
1839+
filePath: string;
1840+
}
1841+
```
1842+
1843+
#### [Load capture from buffer](https://zwave-js.github.io/node-zwave-js/#/api/zniffer?id=capturing-frames)
1844+
1845+
Loads captured frames from a base64 encoded buffer.
1846+
1847+
[compatible with schema version: 47+]
1848+
1849+
```ts
1850+
interface {
1851+
messageId: string;
1852+
command: "zniffer.load_capture_from_buffer";
1853+
data: string; // base64 encoded .zlf data
1854+
}
1855+
```
1856+
17601857
## Events
17611858

17621859
### `zwave-js` Events

src/lib/broadcast_node/incoming_message.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { SetValueAPIOptions } from "zwave-js";
33
import { IncomingCommandBase } from "../incoming_message_base.js";
44
import { BroadcastNodeCommand } from "./command.js";
55

6-
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
7-
export interface IncomingCommandBroadcastNodeBase extends IncomingCommandBase {}
6+
export interface IncomingCommandBroadcastNodeBase extends IncomingCommandBase {
7+
longRange?: boolean;
8+
}
89

910
export interface IncomingCommandBroadcastNodeSetValue extends IncomingCommandBroadcastNodeBase {
1011
command: BroadcastNodeCommand.setValue;

src/lib/broadcast_node/message_handler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export class BroadcastNodeMessageHandler implements MessageHandler {
1818
): Promise<BroadcastNodeResultTypes[BroadcastNodeCommand]> {
1919
const { command } = message;
2020

21-
const virtualNode = this.driver.controller.getBroadcastNode();
21+
const virtualNode = message.longRange
22+
? this.driver.controller.getBroadcastNodeLR()
23+
: this.driver.controller.getBroadcastNode();
2224

2325
switch (message.command) {
2426
case BroadcastNodeCommand.setValue: {

src/lib/utils/command.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ export enum UtilsCommand {
66
buffer2hex = "utils.buffer2hex", // While made available in the server due to its availability within the driver, this functionality works best when implemented locally
77
getEnumMemberName = "utils.get_enum_member_name", // While made available in the server due to its availability within the driver, this functionality works best when implemented locally
88
rssiToString = "utils.rssi_to_string",
9+
guessFirmwareFileFormat = "utils.guess_firmware_file_format",
10+
tryUnzipFirmwareFile = "utils.try_unzip_firmware_file",
11+
extractFirmware = "utils.extract_firmware",
912
}

src/lib/utils/incoming_message.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { RSSI } from "zwave-js";
2+
import { FirmwareFileFormat } from "@zwave-js/core";
23
import { IncomingCommandBase } from "../incoming_message_base.js";
34
import { UtilsCommand } from "./command.js";
45

@@ -43,11 +44,31 @@ export interface IncomingCommandUtilsRssiToString extends IncomingCommandUtilsBa
4344
rssi: RSSI;
4445
}
4546

47+
export interface IncomingCommandUtilsGuessFirmwareFileFormat extends IncomingCommandUtilsBase {
48+
command: UtilsCommand.guessFirmwareFileFormat;
49+
filename: string;
50+
file: string; // base64 encoded
51+
}
52+
53+
export interface IncomingCommandUtilsTryUnzipFirmwareFile extends IncomingCommandUtilsBase {
54+
command: UtilsCommand.tryUnzipFirmwareFile;
55+
file: string; // base64 encoded ZIP file
56+
}
57+
58+
export interface IncomingCommandUtilsExtractFirmware extends IncomingCommandUtilsBase {
59+
command: UtilsCommand.extractFirmware;
60+
file: string; // base64 encoded firmware file
61+
format: FirmwareFileFormat;
62+
}
63+
4664
export type IncomingMessageUtils =
4765
| IncomingCommandUtilsParseQRCodeString
4866
| IncomingCommandUtilsTryParseDSKFromQRCodeString
4967
| IncomingCommandUtilsNum2hex
5068
| IncomingCommandUtilsFormatId
5169
| IncomingCommandUtilsBuffer2hex
5270
| IncomingCommandUtilsGetEnumMemberName
53-
| IncomingCommandUtilsRssiToString;
71+
| IncomingCommandUtilsRssiToString
72+
| IncomingCommandUtilsGuessFirmwareFileFormat
73+
| IncomingCommandUtilsTryUnzipFirmwareFile
74+
| IncomingCommandUtilsExtractFirmware;

src/lib/utils/message_handler.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import {
55
num2hex,
66
rssiToString,
77
} from "zwave-js";
8-
import { parseQRCodeString, tryParseDSKFromQRCodeString } from "@zwave-js/core";
8+
import {
9+
extractFirmware,
10+
guessFirmwareFileFormat,
11+
parseQRCodeString,
12+
tryParseDSKFromQRCodeString,
13+
tryUnzipFirmwareFile,
14+
} from "@zwave-js/core";
915
import { UnknownCommandError } from "../error.js";
1016
import { UtilsCommand } from "./command.js";
1117
import { IncomingMessageUtils } from "./incoming_message.js";
@@ -36,10 +42,7 @@ export class UtilsMessageHandler implements MessageHandler {
3642
return { id };
3743
}
3844
case UtilsCommand.buffer2hex: {
39-
const hex = buffer2hex(
40-
Uint8Array.from(message.buffer),
41-
message.uppercase,
42-
);
45+
const hex = buffer2hex(message.buffer, message.uppercase);
4346
return { hex };
4447
}
4548
case UtilsCommand.getEnumMemberName: {
@@ -50,6 +53,35 @@ export class UtilsMessageHandler implements MessageHandler {
5053
const rssi = rssiToString(message.rssi);
5154
return { rssi };
5255
}
56+
case UtilsCommand.guessFirmwareFileFormat: {
57+
const fileBuffer = Buffer.from(message.file, "base64");
58+
const format = guessFirmwareFileFormat(message.filename, fileBuffer);
59+
return { format };
60+
}
61+
case UtilsCommand.tryUnzipFirmwareFile: {
62+
const zipBuffer = Buffer.from(message.file, "base64");
63+
const result = tryUnzipFirmwareFile(zipBuffer);
64+
if (!result) {
65+
return { file: undefined };
66+
}
67+
return {
68+
file: {
69+
filename: result.filename,
70+
format: result.format,
71+
data: Buffer.from(result.rawData).toString("base64"),
72+
},
73+
};
74+
}
75+
case UtilsCommand.extractFirmware: {
76+
const fileBuffer = Buffer.from(message.file, "base64");
77+
const firmware = await extractFirmware(fileBuffer, message.format);
78+
return {
79+
firmware: {
80+
data: Buffer.from(firmware.data).toString("base64"),
81+
firmwareTarget: firmware.firmwareTarget,
82+
},
83+
};
84+
}
5385
default: {
5486
throw new UnknownCommandError(command);
5587
}

src/lib/utils/outgoing_message.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QRProvisioningInformation } from "@zwave-js/core";
1+
import { FirmwareFileFormat, QRProvisioningInformation } from "@zwave-js/core";
22
import { UtilsCommand } from "./command.js";
33

44
export interface UtilsResultTypes {
@@ -13,4 +13,21 @@ export interface UtilsResultTypes {
1313
[UtilsCommand.buffer2hex]: { hex: string };
1414
[UtilsCommand.getEnumMemberName]: { name: string };
1515
[UtilsCommand.rssiToString]: { rssi: string };
16+
[UtilsCommand.guessFirmwareFileFormat]: { format: FirmwareFileFormat };
17+
[UtilsCommand.tryUnzipFirmwareFile]: {
18+
// Returns undefined if not a valid ZIP or no compatible firmware found
19+
file:
20+
| {
21+
filename: string;
22+
format: FirmwareFileFormat;
23+
data: string; // base64 encoded
24+
}
25+
| undefined;
26+
};
27+
[UtilsCommand.extractFirmware]: {
28+
firmware: {
29+
data: string; // base64 encoded
30+
firmwareTarget?: number;
31+
};
32+
};
1633
}

src/lib/zniffer/command.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,16 @@ export enum ZnifferCommand {
99
supportedFrequencies = "zniffer.supported_frequencies",
1010
currentFrequency = "zniffer.current_frequency",
1111
setFrequency = "zniffer.set_frequency",
12+
// Undocumented
13+
getLRRegions = "zniffer.get_lr_regions",
14+
// Undocumented
15+
getCurrentLRChannelConfig = "zniffer.get_current_lr_channel_config",
16+
// Undocumented
17+
getSupportedLRChannelConfigs = "zniffer.get_supported_lr_channel_configs",
18+
// Undocumented
19+
setLRChannelConfig = "zniffer.set_lr_channel_config",
20+
// File I/O
21+
saveCaptureToFile = "zniffer.save_capture_to_file",
22+
loadCaptureFromFile = "zniffer.load_capture_from_file",
23+
loadCaptureFromBuffer = "zniffer.load_capture_from_buffer",
1224
}

src/lib/zniffer/incoming_message.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,40 @@ export interface IncomingCommandZnifferSetFrequency extends IncomingCommandZniff
4848
frequency: number;
4949
}
5050

51+
// Long Range
52+
export interface IncomingCommandZnifferGetLRRegions extends IncomingCommandZnifferBase {
53+
command: ZnifferCommand.getLRRegions;
54+
}
55+
56+
export interface IncomingCommandZnifferGetCurrentLRChannelConfig extends IncomingCommandZnifferBase {
57+
command: ZnifferCommand.getCurrentLRChannelConfig;
58+
}
59+
60+
export interface IncomingCommandZnifferGetSupportedLRChannelConfigs extends IncomingCommandZnifferBase {
61+
command: ZnifferCommand.getSupportedLRChannelConfigs;
62+
}
63+
64+
export interface IncomingCommandZnifferSetLRChannelConfig extends IncomingCommandZnifferBase {
65+
command: ZnifferCommand.setLRChannelConfig;
66+
channelConfig: number;
67+
}
68+
69+
// File I/O
70+
export interface IncomingCommandZnifferSaveCaptureToFile extends IncomingCommandZnifferBase {
71+
command: ZnifferCommand.saveCaptureToFile;
72+
filePath: string;
73+
}
74+
75+
export interface IncomingCommandZnifferLoadCaptureFromFile extends IncomingCommandZnifferBase {
76+
command: ZnifferCommand.loadCaptureFromFile;
77+
filePath: string;
78+
}
79+
80+
export interface IncomingCommandZnifferLoadCaptureFromBuffer extends IncomingCommandZnifferBase {
81+
command: ZnifferCommand.loadCaptureFromBuffer;
82+
data: string; // base64 encoded
83+
}
84+
5185
export type IncomingMessageZniffer =
5286
| IncomingCommandZnifferClearCapturedFrames
5387
| IncomingCommandZnifferGetCaptureAsZLFBuffer
@@ -58,4 +92,11 @@ export type IncomingMessageZniffer =
5892
| IncomingCommandZnifferSupportedFrequencies
5993
| IncomingCommandZnifferCurrentFrequency
6094
| IncomingCommandZnifferSetFrequency
61-
| IncomingCommandZnifferDestroy;
95+
| IncomingCommandZnifferDestroy
96+
| IncomingCommandZnifferGetLRRegions
97+
| IncomingCommandZnifferGetCurrentLRChannelConfig
98+
| IncomingCommandZnifferGetSupportedLRChannelConfigs
99+
| IncomingCommandZnifferSetLRChannelConfig
100+
| IncomingCommandZnifferSaveCaptureToFile
101+
| IncomingCommandZnifferLoadCaptureFromFile
102+
| IncomingCommandZnifferLoadCaptureFromBuffer;

0 commit comments

Comments
 (0)