@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
package/src/app.ts ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @license
4
+ * Copyright 2022-2024 Matter.js Authors
5
+ * SPDX-License-Identifier: Apache-2.0
6
+ */
7
+
8
+ import { LogFormat, LogLevel, Logger, singleton } from "@matter/general";
9
+ import { createFileLogger } from "@matter/nodejs";
10
+ import { NodeJsBle } from "@matter/nodejs-ble";
11
+ import { Ble } from "@matter/protocol";
12
+ import yargs from "yargs/yargs";
13
+ import { MatterNode } from "./MatterNode.js";
14
+ import { Shell } from "./shell/Shell";
15
+
16
+ const PROMPT = "matter> ";
17
+ const logger = Logger.get("Shell");
18
+ if (process.stdin?.isTTY) Logger.format = LogFormat.ANSI;
19
+
20
+ let theNode: MatterNode;
21
+
22
+ export function setLogLevel(identifier: string, level: string): void {
23
+ let logLevel = LogLevel.INFO;
24
+ switch (level) {
25
+ case "fatal":
26
+ logLevel = LogLevel.FATAL;
27
+ break;
28
+ case "error":
29
+ logLevel = LogLevel.ERROR;
30
+ break;
31
+ case "warn":
32
+ logLevel = LogLevel.WARN;
33
+ break;
34
+ case "debug":
35
+ logLevel = LogLevel.DEBUG;
36
+ break;
37
+ }
38
+ Logger.setDefaultLoglevelForLogger(identifier, logLevel);
39
+ }
40
+
41
+ /**
42
+ * @file Top level application for Matter Node.
43
+ */
44
+ async function main() {
45
+ const yargsInstance = yargs(process.argv.slice(2))
46
+ .command(
47
+ "* [node-num] [node-type]",
48
+ "Matter Node Shell",
49
+ yargs => {
50
+ return yargs
51
+ .positional("node-num", {
52
+ describe: "Node number for storage",
53
+ default: 0,
54
+ type: "number",
55
+ })
56
+ .positional("node-type", {
57
+ describe: "Type of the node",
58
+ choices: ["controller"],
59
+ default: "controller",
60
+ type: "string",
61
+ })
62
+ .options({
63
+ ble: {
64
+ description: "Enable BLE support.",
65
+ type: "boolean",
66
+ },
67
+ bleHciId: {
68
+ description:
69
+ "HCI ID of the BLE adapter to use. The provided value will be persisted for future runs.",
70
+ type: "number",
71
+ default: 0,
72
+ },
73
+ factoryReset: {
74
+ description: "Factory-Reset storage of this node.",
75
+ default: false,
76
+ type: "boolean",
77
+ },
78
+ netInterface: {
79
+ description: "Network interface to use for MDNS announcements and scanning.",
80
+ type: "string",
81
+ default: undefined,
82
+ },
83
+ logfile: {
84
+ description:
85
+ "Logfile to use to log to. By Default debug loglevel is logged to the file. The provided value will be persisted for future runs.",
86
+ type: "string",
87
+ default: undefined,
88
+ },
89
+ });
90
+ },
91
+ async argv => {
92
+ if (argv.help) return;
93
+
94
+ const { nodeNum, ble, bleHciId, nodeType, factoryReset, netInterface, logfile } = argv;
95
+
96
+ theNode = new MatterNode(nodeNum, netInterface);
97
+ await theNode.initialize(factoryReset);
98
+
99
+ if (logfile !== undefined) {
100
+ await theNode.Store.set("LogFile", logfile);
101
+ }
102
+ if (theNode.Store.has("LogFile")) {
103
+ const storedLogFileName = await theNode.Store.get<string>("LogFile");
104
+ if (storedLogFileName !== undefined) {
105
+ Logger.addLogger("file", await createFileLogger(storedLogFileName), {
106
+ defaultLogLevel: await theNode.Store.get<LogLevel>("LoglevelFile", LogLevel.DEBUG),
107
+ logFormat: LogFormat.PLAIN,
108
+ });
109
+ }
110
+ }
111
+ setLogLevel("default", await theNode.Store.get<string>("LogLevel", "info"));
112
+
113
+ const theShell = new Shell(theNode, nodeNum, PROMPT);
114
+
115
+ if (bleHciId !== undefined) {
116
+ await theNode.Store.set("BleHciId", bleHciId);
117
+ }
118
+
119
+ if (ble) {
120
+ const hciId = await theNode.Store.get<number>("BleHciId", 0);
121
+ // Initialize Ble
122
+ Ble.get = singleton(
123
+ () =>
124
+ new NodeJsBle({
125
+ hciId,
126
+ }),
127
+ );
128
+ }
129
+
130
+ console.log(`Started Node #${nodeNum} (Type: ${nodeType}) ${ble ? "with" : "without"} BLE`);
131
+ theShell.start();
132
+ },
133
+ )
134
+ .version(false)
135
+ .scriptName("shell");
136
+ await yargsInstance.wrap(yargsInstance.terminalWidth()).parseAsync();
137
+ }
138
+
139
+ process.on("message", function (message) {
140
+ console.log(`Message to shell.ts: ${message}`);
141
+
142
+ switch (message) {
143
+ case "exit":
144
+ exit().catch(error => logger.error(error));
145
+ }
146
+ });
147
+
148
+ export async function exit(code = 0) {
149
+ await theNode?.close();
150
+ process.exit(code);
151
+ }
152
+
153
+ process.on("SIGINT", () => {
154
+ // Pragmatic way to make sure the storage is correctly closed before the process ends.
155
+ exit().catch(error => logger.error(error));
156
+ });
157
+
158
+ main().catch(error => logger.error(error));
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { MatterError } from "@matter/general";
8
+ import { createWriteStream, readFileSync } from "fs";
9
+ import readline from "node:readline";
10
+ import yargs from "yargs/yargs";
11
+ import { MatterNode } from "../MatterNode.js";
12
+ import { exit } from "../app";
13
+ import { commandlineParser } from "../util/CommandlineParser.js";
14
+ import cmdAttributes from "./cmd_cluster-attributes";
15
+ import cmdCommands from "./cmd_cluster-commands";
16
+ import cmdEvents from "./cmd_cluster-events";
17
+ import cmdCommission from "./cmd_commission.js";
18
+ import cmdConfig from "./cmd_config.js";
19
+ import cmdDiscover from "./cmd_discover.js";
20
+ import cmdIdentify from "./cmd_identify.js";
21
+ import cmdNodes from "./cmd_nodes.js";
22
+ import cmdSession from "./cmd_session.js";
23
+ import cmdSubscribe from "./cmd_subscribe.js";
24
+ import cmdTlv from "./cmd_tlv";
25
+
26
+ const MAX_HISTORY_SIZE = 1000;
27
+
28
+ function exitCommand() {
29
+ return {
30
+ command: "exit",
31
+ describe: "Exit",
32
+ builder: {},
33
+ handler: async () => {
34
+ console.log("Goodbye.");
35
+ await exit();
36
+ },
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Class to process and dispatch shell commands.
42
+ */
43
+ export class Shell {
44
+ readline?: readline.Interface;
45
+ writeStream?: NodeJS.WritableStream;
46
+
47
+ /**
48
+ * Construct a new Shell object.
49
+ */
50
+ constructor(
51
+ public theNode: MatterNode,
52
+ public nodeNum: number,
53
+ public prompt: string,
54
+ ) {}
55
+
56
+ start() {
57
+ const history = new Array<string>();
58
+ const fileName = `.matter-shell-${this.nodeNum}.history`;
59
+ try {
60
+ const historyData = readFileSync(fileName, "utf8");
61
+ history.push(
62
+ ...historyData
63
+ .split("\n")
64
+ .map(line => line.trim())
65
+ .filter(line => line.length),
66
+ );
67
+ history.splice(0, -MAX_HISTORY_SIZE);
68
+ console.log(`Loaded ${history.length} history entries from ${fileName}`);
69
+ } catch (e) {
70
+ if (e instanceof Error && "code" in e && e.code !== "ENOENT") {
71
+ process.stderr.write(`Error happened during history file read: ${e}\n`);
72
+ }
73
+ }
74
+ try {
75
+ this.writeStream = createWriteStream(fileName, { flags: "w" });
76
+ this.writeStream.write(`${history.join("\n")}\n`);
77
+ } catch (e) {
78
+ process.stderr.write(`Error happened during history file write: ${e}\n`);
79
+ }
80
+
81
+ this.readline = readline.createInterface({
82
+ input: process.stdin,
83
+ output: process.stdout,
84
+ terminal: true,
85
+ prompt: this.prompt,
86
+ history: history.reverse(),
87
+ historySize: MAX_HISTORY_SIZE,
88
+ });
89
+ this.readline
90
+ .on("line", cmd => {
91
+ cmd = cmd.trim();
92
+ this.onReadLine(cmd)
93
+ .then(result => result && cmd.length && this.writeStream?.write(`${cmd}\n`))
94
+ .catch(e => {
95
+ process.stderr.write(`Read error: ${e}\n`);
96
+ process.exit(1);
97
+ });
98
+ })
99
+ .on("close", () => {
100
+ try {
101
+ this.writeStream?.end();
102
+ } catch (e) {
103
+ process.stderr.write(`Error happened during history file write: ${e}\n`);
104
+ }
105
+ exit()
106
+ .then(() => process.exit(0))
107
+ .catch(e => {
108
+ process.stderr.write(`Close error: ${e}\n`);
109
+ process.exit(1);
110
+ });
111
+ });
112
+
113
+ this.readline.prompt();
114
+ }
115
+
116
+ /**
117
+ * Method to process a line of raw cli text input.
118
+ *
119
+ * @param {string} line
120
+ */
121
+ async onReadLine(line: string) {
122
+ let result = true;
123
+ if (line) {
124
+ let args;
125
+ try {
126
+ args = commandlineParser(line);
127
+ } catch (error) {
128
+ process.stderr.write(`Error happened during command parsing: ${error}\n`);
129
+ return false;
130
+ }
131
+ const yargsInstance = yargs(args)
132
+ .command([
133
+ cmdCommission(this.theNode),
134
+ cmdConfig(this.theNode),
135
+ cmdSession(this.theNode),
136
+ cmdNodes(this.theNode),
137
+ cmdSubscribe(this.theNode),
138
+ cmdIdentify(this.theNode),
139
+ cmdDiscover(this.theNode),
140
+ cmdAttributes(this.theNode),
141
+ cmdEvents(this.theNode),
142
+ cmdCommands(this.theNode),
143
+ cmdTlv(),
144
+ exitCommand(),
145
+ ])
146
+ .command({
147
+ command: "*",
148
+ handler: argv => {
149
+ argv.unhandled = true;
150
+ },
151
+ })
152
+ .exitProcess(false)
153
+ .version(false)
154
+ .help("help")
155
+ .scriptName("")
156
+ .strictCommands(false)
157
+ .strictOptions(false)
158
+ .fail(false)
159
+ .strict(false);
160
+ try {
161
+ const argv = await yargsInstance.wrap(yargsInstance.terminalWidth()).parseAsync();
162
+
163
+ if (argv.unhandled) {
164
+ process.stderr.write(`Unknown command: ${line}\n`);
165
+ yargsInstance.showHelp();
166
+ } else {
167
+ console.log("Done.");
168
+ }
169
+ } catch (error) {
170
+ process.stderr.write(`Error happened during command: ${error}\n`);
171
+ if (error instanceof Error && error.stack) {
172
+ process.stderr.write(error.stack.toString());
173
+ process.stderr.write("\n");
174
+ }
175
+ if (!(error instanceof MatterError)) {
176
+ yargsInstance.showHelp();
177
+ result = false;
178
+ }
179
+ }
180
+ }
181
+ this.readline?.prompt();
182
+ return result;
183
+ }
184
+ }
@@ -0,0 +1,340 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2024 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Diagnostic, Logger } from "@matter/general";
8
+ import { AttributeModel, ClusterModel, MatterModel } from "@matter/model";
9
+ import { SupportedAttributeClient } from "@project-chip/matter.js/cluster";
10
+ import { ValidationError } from "@project-chip/matter.js/common";
11
+ import { AttributeId, ClusterId, EndpointNumber } from "@project-chip/matter.js/datatype";
12
+ import type { Argv } from "yargs";
13
+ import { MatterNode } from "../MatterNode";
14
+ import { convertJsonDataWithModel } from "../util/Json";
15
+ import { camelize } from "../util/String";
16
+
17
+ function generateAllAttributeHandlersForCluster(yargs: Argv, theNode: MatterNode) {
18
+ MatterModel.standard.clusters.forEach(cluster => {
19
+ yargs = generateClusterAttributeHandlers(yargs, cluster, theNode);
20
+ });
21
+
22
+ yargs = yargs.command(
23
+ "by-id <cluster-id> read <attribute-id> <node-id> <endpoint-id>",
24
+ `Read attributes by id`,
25
+ yargs =>
26
+ yargs
27
+ .positional("cluster-id", {
28
+ describe: "cluster id to read from",
29
+ type: "number",
30
+ demandOption: true,
31
+ })
32
+ .positional("attribute-id", {
33
+ describe: "attribute id to read, use * to read all attributes of the given cluster",
34
+ type: "string",
35
+ demandOption: true,
36
+ })
37
+ .positional("endpoint-id", {
38
+ describe: "endpoint id to read",
39
+ type: "number",
40
+ demandOption: true,
41
+ })
42
+ .positional("node-id", {
43
+ describe: "node id to read",
44
+ type: "string",
45
+ demandOption: true,
46
+ }),
47
+ async argv => {
48
+ const { nodeId, endpointId, clusterId, attributeId: rawAttributeId } = argv;
49
+ const attributeId = rawAttributeId === "*" ? undefined : parseInt(rawAttributeId);
50
+ const node = (await theNode.connectAndGetNodes(nodeId))[0];
51
+
52
+ try {
53
+ const interactionClient = await node.getInteractionClient();
54
+ const result = await interactionClient.getMultipleAttributes({
55
+ attributes: [
56
+ {
57
+ endpointId: EndpointNumber(endpointId),
58
+ clusterId: ClusterId(clusterId),
59
+ attributeId: attributeId !== undefined ? AttributeId(attributeId) : undefined,
60
+ },
61
+ ],
62
+ });
63
+ console.log(
64
+ `Attribute values for cluster ${node.nodeId.toString()}/${endpointId}/${clusterId}/${attributeId}:`,
65
+ );
66
+ for (const {
67
+ path: { attributeId, attributeName },
68
+ value,
69
+ } of result) {
70
+ console.log(
71
+ ` ${Diagnostic.hex(attributeId)}${attributeName !== undefined ? ` (${attributeName})` : ""}: ${Logger.toJSON(value)}`,
72
+ );
73
+ }
74
+ } catch (error) {
75
+ console.log(
76
+ `ERROR: Could not get attribute ${node.nodeId.toString()}/${endpointId}/${clusterId}/${attributeId}: ${error}`,
77
+ );
78
+ }
79
+ },
80
+ );
81
+ return yargs;
82
+ }
83
+
84
+ function generateClusterAttributeHandlers(yargs: Argv, cluster: ClusterModel, theNode: MatterNode) {
85
+ if (cluster.id === undefined) {
86
+ return yargs;
87
+ }
88
+ yargs = yargs.command(
89
+ [cluster.name.toLowerCase(), `0x${cluster.id.toString(16)}`],
90
+ `Read/Write ${cluster.name} attributes`,
91
+ yargs => {
92
+ yargs = yargs.command(
93
+ "read",
94
+ `Reads attributes of ${cluster.name}`,
95
+ yargs => {
96
+ yargs = yargs.command(
97
+ ["* <node-id> <endpoint-id>", "all"],
98
+ `Read all attributes of ${cluster.name}`,
99
+ yargs => {
100
+ return yargs
101
+ .positional("endpoint-id", {
102
+ describe: "endpoint id to read",
103
+ type: "number",
104
+ demandOption: true,
105
+ })
106
+ .positional("node-id", {
107
+ describe: "node id to read",
108
+ type: "string",
109
+ demandOption: true,
110
+ })
111
+ .options({
112
+ remote: {
113
+ describe: "request value always remote",
114
+ default: false,
115
+ type: "boolean",
116
+ },
117
+ });
118
+ },
119
+ async argv => {
120
+ const clusterId = cluster.id;
121
+ const { nodeId, endpointId, remote } = argv;
122
+ const requestRemote = remote ? true : undefined;
123
+ const node = (await theNode.connectAndGetNodes(nodeId))[0];
124
+
125
+ const clusterClient = node
126
+ .getDeviceById(endpointId)
127
+ ?.getClusterClientById(ClusterId(clusterId));
128
+ if (clusterClient === undefined) {
129
+ console.log(
130
+ `ERROR: Cluster ${node.nodeId.toString()}/${endpointId}/${clusterId} not found.`,
131
+ );
132
+ return;
133
+ }
134
+ console.log(
135
+ `Attribute values for cluster ${cluster.name} (${node.nodeId.toString()}/${endpointId}/${clusterId}):`,
136
+ );
137
+ for (const attribute of cluster.attributes) {
138
+ const attributeName = camelize(attribute.name);
139
+ const attributeClient = clusterClient.attributes[attributeName];
140
+ if (!(attributeClient instanceof SupportedAttributeClient)) {
141
+ continue;
142
+ }
143
+ console.log(
144
+ ` ${attributeName} (${attribute.id}): ${Logger.toJSON(await attributeClient.get(requestRemote))}`,
145
+ );
146
+ }
147
+ },
148
+ );
149
+
150
+ cluster.attributes.forEach(attribute => {
151
+ yargs = generateAttributeReadHandler(yargs, cluster.id, cluster.name, attribute, theNode);
152
+ });
153
+ return yargs;
154
+ },
155
+ async (argv: any) => {
156
+ argv.unhandled = true;
157
+ },
158
+ );
159
+
160
+ if (cluster.attributes.some(attribute => attribute.writable)) {
161
+ yargs = yargs.command(
162
+ "write",
163
+ `Writes attributes of ${cluster.name}`,
164
+ yargs => {
165
+ cluster.attributes.forEach(attribute => {
166
+ if (!attribute.writable) {
167
+ return;
168
+ }
169
+ yargs = generateAttributeWriteHandler(yargs, cluster.id, cluster.name, attribute, theNode);
170
+ });
171
+ return yargs;
172
+ },
173
+ async (argv: any) => {
174
+ argv.unhandled = true;
175
+ },
176
+ );
177
+ }
178
+
179
+ return yargs;
180
+ },
181
+ async (argv: any) => {
182
+ argv.unhandled = true;
183
+ },
184
+ );
185
+
186
+ return yargs;
187
+ }
188
+
189
+ function generateAttributeReadHandler(
190
+ yargs: Argv,
191
+ clusterId: number,
192
+ clusterName: string,
193
+ attribute: AttributeModel,
194
+ theNode: MatterNode,
195
+ ) {
196
+ const attributeName = camelize(attribute.name);
197
+ return yargs.command(
198
+ [`${attribute.name.toLowerCase()} <node-id> <endpoint-id>`, `0x${attribute.id.toString(16)}`],
199
+ `Read ${clusterName}.${attribute.name} attribute`,
200
+ yargs =>
201
+ yargs
202
+ .positional("endpoint-id", {
203
+ describe: "endpoint id to read",
204
+ type: "number",
205
+ demandOption: true,
206
+ })
207
+ .positional("node-id", {
208
+ describe: "node id to read",
209
+ type: "string",
210
+ demandOption: true,
211
+ })
212
+ .options({
213
+ remote: {
214
+ describe: "request value always remote",
215
+ default: false,
216
+ type: "boolean",
217
+ },
218
+ }),
219
+ async argv => {
220
+ const { nodeId, endpointId, remote } = argv;
221
+ const requestRemote = remote ? true : undefined;
222
+ const node = (await theNode.connectAndGetNodes(nodeId))[0];
223
+
224
+ const clusterClient = node.getDeviceById(endpointId)?.getClusterClientById(ClusterId(clusterId));
225
+ if (clusterClient === undefined) {
226
+ console.log(`ERROR: Cluster ${node.nodeId.toString()}/${endpointId}/${clusterId} not found.`);
227
+ return;
228
+ }
229
+ const attributeClient = clusterClient.attributes[attributeName];
230
+ if (!(attributeClient instanceof SupportedAttributeClient)) {
231
+ console.log(
232
+ `ERROR: Attribute ${node.nodeId.toString()}/${endpointId}/${clusterId}/${attribute.id} not supported.`,
233
+ );
234
+ return;
235
+ }
236
+ try {
237
+ console.log(
238
+ `Attribute value for ${attributeName} ${node.nodeId.toString()}/${endpointId}/${clusterId}/${attribute.id}: ${Logger.toJSON(await attributeClient.get(requestRemote))}`,
239
+ );
240
+ } catch (error) {
241
+ console.log(`ERROR: Could not get attribute ${attribute.name}: ${error}`);
242
+ }
243
+ },
244
+ );
245
+ }
246
+
247
+ function generateAttributeWriteHandler(
248
+ yargs: Argv,
249
+ clusterId: number,
250
+ clusterName: string,
251
+ attribute: AttributeModel,
252
+ theNode: MatterNode,
253
+ ) {
254
+ //console.log("Generating attribute handler for ", attribute.name, attribute);
255
+ //console.log(attribute.definingModel);
256
+ const attributeName = camelize(attribute.name);
257
+ const typeHint = `${attribute.type}${attribute.definingModel === undefined ? "" : " as JSON string"}`;
258
+ return yargs.command(
259
+ [`${attribute.name.toLowerCase()} <value> <nodeId> <endpointId>`, `0x${attribute.id.toString(16)}`],
260
+ `Write ${clusterName}.${attribute.name} attribute`,
261
+ yargs =>
262
+ yargs
263
+ .positional("endpoint-id", {
264
+ describe: "endpoint id to write to",
265
+ type: "number",
266
+ demandOption: true,
267
+ })
268
+ .positional("node-id", {
269
+ describe: "node id to write t.",
270
+ type: "string",
271
+ demandOption: true,
272
+ })
273
+ .positional("value", {
274
+ describe: `value to write (${typeHint})`,
275
+ type: "string",
276
+ demandOption: true,
277
+ }),
278
+
279
+ async argv => {
280
+ const { nodeId, endpointId, value } = argv;
281
+
282
+ let parsedValue: any;
283
+ try {
284
+ parsedValue = JSON.parse(value);
285
+ } catch (error) {
286
+ try {
287
+ parsedValue = JSON.parse(`"${value}"`);
288
+ } catch (innerError) {
289
+ console.log(`ERROR: Could not parse value ${value} as JSON.`);
290
+ return;
291
+ }
292
+ }
293
+
294
+ const node = (await theNode.connectAndGetNodes(nodeId))[0];
295
+
296
+ const clusterClient = node.getDeviceById(endpointId)?.getClusterClientById(ClusterId(clusterId));
297
+ if (clusterClient === undefined) {
298
+ console.log(`ERROR: Cluster ${node.nodeId.toString()}/${endpointId}/${clusterId} not found.`);
299
+ return;
300
+ }
301
+ const attributeClient = clusterClient.attributes[attributeName];
302
+ if (!(attributeClient instanceof SupportedAttributeClient)) {
303
+ console.log(
304
+ `ERROR: Attribute ${node.nodeId.toString()}/${endpointId}/${clusterId}/${attribute.id} not supported.`,
305
+ );
306
+ return;
307
+ }
308
+
309
+ try {
310
+ parsedValue = convertJsonDataWithModel(attribute, parsedValue);
311
+
312
+ await attributeClient.set(parsedValue);
313
+ console.log(
314
+ `Attribute ${attributeName} ${node.nodeId.toString()}/${endpointId}/${clusterId}/${attribute.id} set to ${Logger.toJSON(value)}`,
315
+ );
316
+ } catch (error) {
317
+ if (error instanceof ValidationError) {
318
+ console.log(
319
+ `ERROR: Could not validate data for attribute ${attribute.name} to ${Logger.toJSON(parsedValue)}: ${error}${error.fieldName !== undefined ? ` in field ${error.fieldName}` : ""}`,
320
+ );
321
+ } else {
322
+ console.log(
323
+ `ERROR: Could not set attribute ${attribute.name} to ${Logger.toJSON(parsedValue)}: ${error}`,
324
+ );
325
+ }
326
+ }
327
+ },
328
+ );
329
+ }
330
+
331
+ export default function cmdAttributes(theNode: MatterNode) {
332
+ return {
333
+ command: ["attributes", "a"],
334
+ describe: "Read and Write attributes",
335
+ builder: (yargs: Argv) => generateAllAttributeHandlersForCluster(yargs, theNode),
336
+ handler: async (argv: any) => {
337
+ argv.unhandled = true;
338
+ },
339
+ };
340
+ }