@matter/nodejs-shell 0.11.0-alpha.0-20241007-547af42a8

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.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/dist/cjs/MatterNode.js +115 -0
  4. package/dist/cjs/MatterNode.js.map +6 -0
  5. package/dist/cjs/app.js +162 -0
  6. package/dist/cjs/app.js.map +6 -0
  7. package/dist/cjs/package.json +7 -0
  8. package/dist/cjs/shell/Shell.js +195 -0
  9. package/dist/cjs/shell/Shell.js.map +6 -0
  10. package/dist/cjs/shell/cmd_cluster-attributes.js +310 -0
  11. package/dist/cjs/shell/cmd_cluster-attributes.js.map +6 -0
  12. package/dist/cjs/shell/cmd_cluster-commands.js +158 -0
  13. package/dist/cjs/shell/cmd_cluster-commands.js.map +6 -0
  14. package/dist/cjs/shell/cmd_cluster-events.js +104 -0
  15. package/dist/cjs/shell/cmd_cluster-events.js.map +6 -0
  16. package/dist/cjs/shell/cmd_commission.js +237 -0
  17. package/dist/cjs/shell/cmd_commission.js.map +6 -0
  18. package/dist/cjs/shell/cmd_config.js +315 -0
  19. package/dist/cjs/shell/cmd_config.js.map +6 -0
  20. package/dist/cjs/shell/cmd_discover.js +123 -0
  21. package/dist/cjs/shell/cmd_discover.js.map +6 -0
  22. package/dist/cjs/shell/cmd_identify.js +66 -0
  23. package/dist/cjs/shell/cmd_identify.js.map +6 -0
  24. package/dist/cjs/shell/cmd_nodes.js +244 -0
  25. package/dist/cjs/shell/cmd_nodes.js.map +6 -0
  26. package/dist/cjs/shell/cmd_session.js +43 -0
  27. package/dist/cjs/shell/cmd_session.js.map +6 -0
  28. package/dist/cjs/shell/cmd_subscribe.js +59 -0
  29. package/dist/cjs/shell/cmd_subscribe.js.map +6 -0
  30. package/dist/cjs/shell/cmd_tlv.js +175 -0
  31. package/dist/cjs/shell/cmd_tlv.js.map +6 -0
  32. package/dist/cjs/util/CommandlineParser.js +106 -0
  33. package/dist/cjs/util/CommandlineParser.js.map +6 -0
  34. package/dist/cjs/util/Json.js +66 -0
  35. package/dist/cjs/util/Json.js.map +6 -0
  36. package/dist/cjs/util/String.js +32 -0
  37. package/dist/cjs/util/String.js.map +6 -0
  38. package/package.json +58 -0
  39. package/src/MatterNode.ts +141 -0
  40. package/src/app.ts +158 -0
  41. package/src/shell/Shell.ts +184 -0
  42. package/src/shell/cmd_cluster-attributes.ts +340 -0
  43. package/src/shell/cmd_cluster-commands.ts +174 -0
  44. package/src/shell/cmd_cluster-events.ts +99 -0
  45. package/src/shell/cmd_commission.ts +273 -0
  46. package/src/shell/cmd_config.ts +366 -0
  47. package/src/shell/cmd_discover.ts +127 -0
  48. package/src/shell/cmd_identify.ts +51 -0
  49. package/src/shell/cmd_nodes.ts +249 -0
  50. package/src/shell/cmd_session.ts +23 -0
  51. package/src/shell/cmd_subscribe.ts +43 -0
  52. package/src/shell/cmd_tlv.ts +169 -0
  53. package/src/tsconfig.json +22 -0
  54. package/src/util/CommandlineParser.ts +123 -0
  55. package/src/util/Json.ts +50 -0
  56. package/src/util/String.ts +9 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Logger } from "@matter/general";
8
+ import { ClusterModel, CommandModel, MatterModel } from "@matter/model";
9
+ import { ValidationError } from "@project-chip/matter.js/common";
10
+ import { ClusterId } from "@project-chip/matter.js/datatype";
11
+ import type { Argv } from "yargs";
12
+ import { MatterNode } from "../MatterNode";
13
+ import { convertJsonDataWithModel } from "../util/Json";
14
+ import { camelize } from "../util/String";
15
+
16
+ function generateAllCommandHandlersForCluster(yargs: Argv, theNode: MatterNode) {
17
+ MatterModel.standard.clusters.forEach(cluster => {
18
+ yargs = generateClusterCommandHandlers(yargs, cluster, theNode);
19
+ });
20
+ return yargs;
21
+ }
22
+
23
+ function generateClusterCommandHandlers(yargs: Argv, cluster: ClusterModel, theNode: MatterNode) {
24
+ if (cluster.id === undefined) {
25
+ return yargs;
26
+ }
27
+ yargs = yargs.command(
28
+ [cluster.name.toLowerCase(), `0x${cluster.id.toString(16)}`],
29
+ `Invoke ${cluster.name} commands`,
30
+ yargs => {
31
+ cluster.commands.forEach(command => {
32
+ yargs = generateCommandHandler(yargs, cluster.id, cluster.name, command, theNode);
33
+ });
34
+ return yargs;
35
+ },
36
+ async (argv: any) => {
37
+ argv.unhandled = true;
38
+ },
39
+ );
40
+
41
+ return yargs;
42
+ }
43
+
44
+ function generateCommandHandler(
45
+ yargs: Argv,
46
+ clusterId: number,
47
+ clusterName: string,
48
+ command: CommandModel,
49
+ theNode: MatterNode,
50
+ ) {
51
+ //console.log("Generating command handler for ", command.name, JSON.stringify(command));
52
+ //console.log(command.definingModel);
53
+
54
+ if (command.definingModel !== undefined) {
55
+ // If command has parameters for the call
56
+ return yargs.command(
57
+ [`${command.name.toLowerCase()} <value> <nodeId> <endpointId>`, `0x${command.id.toString(16)}`],
58
+ `Invoke ${clusterName}.${command.name} command`,
59
+ yargs =>
60
+ yargs
61
+ .positional("value", {
62
+ describe: "value to write as JSON value",
63
+ type: "string",
64
+ demandOption: true,
65
+ })
66
+ .positional("endpoint-id", {
67
+ describe: "endpoint id to write to",
68
+ type: "number",
69
+ demandOption: true,
70
+ })
71
+ .positional("node-id", {
72
+ describe: "node id to write t.",
73
+ type: "string",
74
+ demandOption: true,
75
+ }),
76
+ async argv => {
77
+ const { nodeId, endpointId, value } = argv;
78
+
79
+ let parsedValue: any;
80
+ try {
81
+ parsedValue = JSON.parse(value);
82
+ } catch (error) {
83
+ try {
84
+ parsedValue = JSON.parse(`"${value}"`);
85
+ } catch (innerError) {
86
+ console.log(`ERROR: Could not parse value ${value} as JSON.`);
87
+ return;
88
+ }
89
+ }
90
+
91
+ await executeCommand(theNode, nodeId, endpointId, clusterId, command, parsedValue);
92
+ },
93
+ );
94
+ }
95
+
96
+ // Command has no parameters for the call
97
+ return yargs.command(
98
+ [`${command.name.toLowerCase()} <nodeId> <endpointId>`, `0x${command.id.toString(16)}`],
99
+ `Invoke ${clusterName}.${command.name} command`,
100
+ yargs =>
101
+ yargs
102
+ .positional("endpoint-id", {
103
+ describe: "endpoint id to write to",
104
+ type: "number",
105
+ demandOption: true,
106
+ })
107
+ .positional("node-id", {
108
+ describe: "node id to write t.",
109
+ type: "string",
110
+ demandOption: true,
111
+ }),
112
+ async argv => {
113
+ const { nodeId, endpointId } = argv;
114
+ await executeCommand(theNode, nodeId, endpointId, clusterId, command);
115
+ },
116
+ );
117
+ }
118
+
119
+ async function executeCommand(
120
+ theNode: MatterNode,
121
+ nodeId: string,
122
+ endpointId: number,
123
+ clusterId: number,
124
+ command: CommandModel,
125
+ requestData?: any,
126
+ ) {
127
+ const commandName = camelize(command.name);
128
+
129
+ const node = (await theNode.connectAndGetNodes(nodeId))[0];
130
+
131
+ const clusterClient = node.getDeviceById(endpointId)?.getClusterClientById(ClusterId(clusterId));
132
+ if (clusterClient === undefined) {
133
+ console.log(`ERROR: Cluster ${node.nodeId.toString()}/${endpointId}/${clusterId} not found.`);
134
+ return;
135
+ }
136
+ if (clusterClient.commands[commandName] == undefined) {
137
+ console.log(`ERROR: Command ${node.nodeId.toString()}/${endpointId}/${clusterId}/${command.id} not supported.`);
138
+ return;
139
+ }
140
+ try {
141
+ if (requestData !== undefined) {
142
+ requestData = convertJsonDataWithModel(command, requestData);
143
+ }
144
+
145
+ const result = await clusterClient.commands[commandName](requestData);
146
+ console.log(
147
+ `Command ${command.name} ${node.nodeId.toString()}/${endpointId}/${clusterId}/${command.id} invoked ${requestData ? `with ${Logger.toJSON(requestData)}` : ""}`,
148
+ );
149
+ if (result !== undefined) {
150
+ console.log(`Result: ${Logger.toJSON(result)}`);
151
+ }
152
+ } catch (error) {
153
+ if (error instanceof ValidationError) {
154
+ console.log(
155
+ `ERROR: Could not validate data for command ${command.name} with ${Logger.toJSON(requestData)}: ${error}${error.fieldName !== undefined ? ` in field ${error.fieldName}` : ""}`,
156
+ );
157
+ } else {
158
+ console.log(
159
+ `ERROR: Could not invoke command ${command.name} ${requestData ? `with ${Logger.toJSON(requestData)}` : ""}: ${error}`,
160
+ );
161
+ }
162
+ }
163
+ }
164
+
165
+ export default function cmdCommands(theNode: MatterNode) {
166
+ return {
167
+ command: ["commands", "c"],
168
+ describe: "Invoke commands",
169
+ builder: (yargs: Argv) => generateAllCommandHandlersForCluster(yargs, theNode),
170
+ handler: async (argv: any) => {
171
+ argv.unhandled = true;
172
+ },
173
+ };
174
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Logger } from "@matter/general";
8
+ import { ClusterModel, EventModel, MatterModel } from "@matter/model";
9
+ import { SupportedEventClient } from "@project-chip/matter.js/cluster";
10
+ import { ClusterId } from "@project-chip/matter.js/datatype";
11
+ import type { Argv } from "yargs";
12
+ import { MatterNode } from "../MatterNode";
13
+ import { camelize } from "../util/String";
14
+
15
+ function generateAllEventHandlersForCluster(yargs: Argv, theNode: MatterNode) {
16
+ MatterModel.standard.clusters.forEach(cluster => {
17
+ yargs = generateClusterEventHandlers(yargs, cluster, theNode);
18
+ });
19
+ return yargs;
20
+ }
21
+
22
+ function generateClusterEventHandlers(yargs: Argv, cluster: ClusterModel, theNode: MatterNode) {
23
+ if (cluster.id === undefined) {
24
+ return yargs;
25
+ }
26
+ yargs = yargs.command(
27
+ [cluster.name.toLowerCase(), `0x${cluster.id.toString(16)}`],
28
+ `Read ${cluster.name} events`,
29
+ yargs => {
30
+ cluster.events.forEach(event => {
31
+ yargs = generateEventHandler(yargs, cluster.id, cluster.name, event, theNode);
32
+ });
33
+ return yargs;
34
+ },
35
+ async (argv: any) => {
36
+ argv.unhandled = true;
37
+ },
38
+ );
39
+
40
+ return yargs;
41
+ }
42
+
43
+ function generateEventHandler(
44
+ yargs: Argv,
45
+ clusterId: number,
46
+ clusterName: string,
47
+ event: EventModel,
48
+ theNode: MatterNode,
49
+ ) {
50
+ //console.log("Generating event handler for ", event.name, JSON.stringify(event));
51
+ const eventName = camelize(event.name);
52
+ return yargs.command(
53
+ [`${event.name.toLowerCase()} <node-id> <endpoint-id>`, `0x${event.id.toString(16)}`],
54
+ `Read ${clusterName}.${event.name} event`,
55
+ yargs =>
56
+ yargs
57
+ .positional("endpoint-id", {
58
+ describe: "endpoint id to read",
59
+ type: "number",
60
+ demandOption: true,
61
+ })
62
+ .positional("node-id", {
63
+ describe: "node id to read",
64
+ type: "string",
65
+ demandOption: true,
66
+ }),
67
+ async argv => {
68
+ const { nodeId, endpointId } = argv;
69
+ const node = (await theNode.connectAndGetNodes(nodeId))[0];
70
+
71
+ const clusterClient = node.getDeviceById(endpointId)?.getClusterClientById(ClusterId(clusterId));
72
+ if (clusterClient === undefined) {
73
+ console.log(`ERROR: Cluster ${node.nodeId.toString()}/${endpointId}/${clusterId} not found.`);
74
+ return;
75
+ }
76
+ const eventClient = clusterClient.events[eventName];
77
+ if (!(eventClient instanceof SupportedEventClient)) {
78
+ console.log(
79
+ `ERROR: Event ${node.nodeId.toString()}/${endpointId}/${clusterId}/${event.id} not supported.`,
80
+ );
81
+ return;
82
+ }
83
+ console.log(
84
+ `Event value for ${eventName} ${node.nodeId.toString()}/${endpointId}/${clusterId}/${event.id}: ${Logger.toJSON(await eventClient.get())}`,
85
+ );
86
+ },
87
+ );
88
+ }
89
+
90
+ export default function cmdEvents(theNode: MatterNode) {
91
+ return {
92
+ command: ["events", "e"],
93
+ describe: "Read events",
94
+ builder: (yargs: Argv) => generateAllEventHandlersForCluster(yargs, theNode),
95
+ handler: async (argv: any) => {
96
+ argv.unhandled = true;
97
+ },
98
+ };
99
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Logger, MatterError } from "@matter/general";
8
+ import { NodeCommissioningOptions } from "@project-chip/matter.js";
9
+ import { BasicInformationCluster, DescriptorCluster, GeneralCommissioning } from "@project-chip/matter.js/cluster";
10
+ import { NodeId } from "@project-chip/matter.js/datatype";
11
+ import { ManualPairingCodeCodec, QrCode } from "@project-chip/matter.js/schema";
12
+ import type { Argv } from "yargs";
13
+ import { MatterNode } from "../MatterNode";
14
+ import { createDiagnosticCallbacks } from "./cmd_nodes";
15
+
16
+ export default function commands(theNode: MatterNode) {
17
+ return {
18
+ command: "commission",
19
+ describe: "Handle device commissioning",
20
+ builder: (yargs: Argv) =>
21
+ yargs
22
+ // Pair
23
+ .command("pair", "Pair with a matter device", yargs => {
24
+ return (
25
+ yargs
26
+ // Pair
27
+ .command(
28
+ "* [node-id] [ip:[port]]",
29
+ "Commission a matter device",
30
+ yargs => {
31
+ return yargs
32
+ .positional("node-id", {
33
+ describe: "node id",
34
+ default: undefined,
35
+ type: "string",
36
+ })
37
+ .positional("ip", {
38
+ describe: "ip address",
39
+ default: undefined,
40
+ type: "string",
41
+ })
42
+ .positional("port", {
43
+ describe: "ip port",
44
+ default: undefined,
45
+ type: "number",
46
+ });
47
+ },
48
+ async argv => {
49
+ const {
50
+ pairingCode,
51
+ nodeId: nodeIdStr,
52
+ ipPort,
53
+ ip,
54
+ ble = false,
55
+ instanceId,
56
+ } = argv;
57
+ let { setupPinCode, discriminator, shortDiscriminator } = argv;
58
+
59
+ if (typeof pairingCode === "string") {
60
+ const { shortDiscriminator: pairingCodeShortDiscriminator, passcode } =
61
+ ManualPairingCodeCodec.decode(pairingCode);
62
+ shortDiscriminator = pairingCodeShortDiscriminator;
63
+ setupPinCode = passcode;
64
+ discriminator = undefined;
65
+ } else if (discriminator === undefined && shortDiscriminator === undefined) {
66
+ discriminator = 3840;
67
+ }
68
+
69
+ const nodeId = nodeIdStr !== undefined ? NodeId(BigInt(nodeIdStr)) : undefined;
70
+ await theNode.start();
71
+ if (theNode.commissioningController === undefined) {
72
+ throw new Error("CommissioningController not initialized");
73
+ }
74
+
75
+ const options = {
76
+ discovery: {
77
+ knownAddress:
78
+ ip !== undefined && ipPort !== undefined
79
+ ? { ip, port: ipPort, type: "udp" }
80
+ : undefined,
81
+ identifierData:
82
+ instanceId !== undefined
83
+ ? { instanceId }
84
+ : discriminator !== undefined
85
+ ? { longDiscriminator: discriminator }
86
+ : shortDiscriminator !== undefined
87
+ ? { shortDiscriminator }
88
+ : {},
89
+ discoveryCapabilities: {
90
+ ble,
91
+ onIpNetwork: true,
92
+ },
93
+ },
94
+ passcode: setupPinCode,
95
+ ...createDiagnosticCallbacks(),
96
+ } as NodeCommissioningOptions;
97
+
98
+ options.commissioning = {
99
+ nodeId: nodeId !== undefined ? NodeId(nodeId) : undefined,
100
+ regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.Outdoor, // Set to the most restrictive if relevant
101
+ regulatoryCountryCode: "XX",
102
+ };
103
+
104
+ console.log(Logger.toJSON(options));
105
+
106
+ if (theNode.Store.has("WiFiSsid") && theNode.Store.has("WiFiPassword")) {
107
+ options.commissioning.wifiNetwork = {
108
+ wifiSsid: await theNode.Store.get<string>("WiFiSsid", ""),
109
+ wifiCredentials: await theNode.Store.get<string>("WiFiPassword", ""),
110
+ };
111
+ }
112
+ if (
113
+ theNode.Store.has("ThreadName") &&
114
+ theNode.Store.has("ThreadOperationalDataset")
115
+ ) {
116
+ options.commissioning.threadNetwork = {
117
+ networkName: await theNode.Store.get<string>("ThreadName", ""),
118
+ operationalDataset: await theNode.Store.get<string>(
119
+ "ThreadOperationalDataset",
120
+ "",
121
+ ),
122
+ };
123
+ }
124
+
125
+ const commissionedNodeId =
126
+ await theNode.commissioningController.commissionNode(options);
127
+
128
+ console.log("Commissioned Node:", commissionedNodeId);
129
+
130
+ const node = theNode.commissioningController.getPairedNode(commissionedNodeId);
131
+ if (node === undefined) {
132
+ // Should not happen
133
+ throw new MatterError("Node not found after commissioning.");
134
+ }
135
+
136
+ // Important: This is a temporary API to proof the methods working and this will change soon and is NOT stable!
137
+ // It is provided to proof the concept
138
+
139
+ // Example to initialize a ClusterClient and access concrete fields as API methods
140
+ const descriptor = node.getRootClusterClient(DescriptorCluster);
141
+ if (descriptor !== undefined) {
142
+ console.log(await descriptor.attributes.deviceTypeList.get()); // you can call that way
143
+ console.log(await descriptor.getServerListAttribute()); // or more convenient that way
144
+ } else {
145
+ console.log("No Descriptor Cluster found. This should never happen!");
146
+ }
147
+
148
+ // Example to subscribe to a field and get the value
149
+ const info = node.getRootClusterClient(BasicInformationCluster);
150
+ if (info !== undefined) {
151
+ console.log(await info.getProductNameAttribute()); // This call is executed remotely
152
+ //console.log(await info.subscribeProductNameAttribute(value => console.log("productName", value), 5, 30));
153
+ //console.log(await info.getProductNameAttribute()); // This call is resolved locally because we have subscribed to the value!
154
+ } else {
155
+ console.log("No BasicInformation Cluster found. This should never happen!");
156
+ }
157
+ },
158
+ )
159
+ .options({
160
+ pairingCode: {
161
+ describe: "pairing code",
162
+ default: undefined,
163
+ type: "string",
164
+ },
165
+ setupPinCode: {
166
+ describe: "setup pin code",
167
+ default: 20202021,
168
+ type: "number",
169
+ },
170
+ instanceId: {
171
+ alias: "i",
172
+ describe: "instance id",
173
+ type: "string",
174
+ },
175
+ discriminator: {
176
+ alias: "d",
177
+ description: "Long discriminator",
178
+ type: "number",
179
+ },
180
+ shortDiscriminator: {
181
+ alias: "s",
182
+ description: "Short discriminator",
183
+ type: "number",
184
+ },
185
+ ble: {
186
+ alias: "b",
187
+ description: "Also discover over BLE",
188
+ type: "boolean",
189
+ default: false,
190
+ },
191
+ })
192
+ );
193
+ })
194
+ .command(
195
+ "open-basic-window <node-id> [timeout]",
196
+ "Open a basic commissioning window",
197
+ yargs => {
198
+ return yargs
199
+ .positional("node-id", {
200
+ describe: "node id",
201
+ type: "string",
202
+ demandOption: true,
203
+ })
204
+ .positional("timeout", {
205
+ describe: "timeout in seconds",
206
+ type: "number",
207
+ default: 900,
208
+ });
209
+ },
210
+ async argv => {
211
+ const { nodeId, timeout = 900 } = argv;
212
+ await theNode.start();
213
+ const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
214
+
215
+ await node.openBasicCommissioningWindow(timeout);
216
+
217
+ console.log(`Basic Commissioning Window for node ${nodeId} opened`);
218
+ },
219
+ )
220
+ .command(
221
+ "open-enhanced-window <node-id> [timeout]",
222
+ "Open a enhanced commissioning window",
223
+ yargs => {
224
+ return yargs
225
+ .positional("node-id", {
226
+ describe: "node id",
227
+ type: "string",
228
+ demandOption: true,
229
+ })
230
+ .positional("timeout", {
231
+ describe: "timeout in seconds",
232
+ type: "number",
233
+ default: 900,
234
+ });
235
+ },
236
+ async argv => {
237
+ await theNode.start();
238
+ const { nodeId, timeout = 900 } = argv;
239
+ const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
240
+ const data = await node.openEnhancedCommissioningWindow(timeout);
241
+
242
+ console.log(`Enhanced Commissioning Window for node ${nodeId} opened`);
243
+ const { qrPairingCode, manualPairingCode } = data;
244
+
245
+ console.log(QrCode.get(qrPairingCode));
246
+ console.log(
247
+ `QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`,
248
+ );
249
+ console.log(`Manual pairing code: ${manualPairingCode}`);
250
+ },
251
+ )
252
+ .command(
253
+ "unpair <node-id>",
254
+ "Unpair/Decommission a node",
255
+ yargs => {
256
+ return yargs.positional("node-id", {
257
+ describe: "node id",
258
+ type: "string",
259
+ demandOption: true,
260
+ });
261
+ },
262
+ async argv => {
263
+ await theNode.start();
264
+ const { nodeId } = argv;
265
+ const node = (await theNode.connectAndGetNodes(nodeId, { autoSubscribe: false }))[0];
266
+ await node.decommission();
267
+ },
268
+ ),
269
+ handler: async (argv: any) => {
270
+ argv.unhandled = true;
271
+ },
272
+ };
273
+ }