@matter-server/ws-client 0.2.7-alpha.0-20260118-993a1c7 → 0.2.7-alpha.0-20260119-49e7237

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/src/client.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Connection, WebSocketFactory } from "./connection.js";
8
- import { InvalidServerVersion } from "./exceptions.js";
8
+ import { CommandTimeoutError, ConnectionClosedError, InvalidServerVersion } from "./exceptions.js";
9
9
  import {
10
10
  AccessControlEntry,
11
11
  APICommands,
@@ -14,6 +14,8 @@ import {
14
14
  CommissioningParameters,
15
15
  ErrorResultMessage,
16
16
  EventMessage,
17
+ LogLevelResponse,
18
+ LogLevelString,
17
19
  MatterFabricData,
18
20
  MatterSoftwareVersion,
19
21
  NodePingResult,
@@ -29,16 +31,28 @@ function toNodeKey(nodeId: number | bigint): string {
29
31
  return String(nodeId);
30
32
  }
31
33
 
34
+ /** Default timeout for WebSocket commands in milliseconds (5 minutes) */
35
+ export const DEFAULT_COMMAND_TIMEOUT = 5 * 60 * 1000;
36
+
32
37
  export class MatterClient {
33
38
  public connection: Connection;
34
39
  public nodes: Record<string, MatterNode> = {};
35
40
  public serverBaseAddress: string;
36
41
  /** Whether this client is connected to a production server (optional, for UI purposes) */
37
42
  public isProduction: boolean = false;
43
+ /** Default timeout for commands in milliseconds. Set to 0 to disable timeouts. */
44
+ public commandTimeout: number = DEFAULT_COMMAND_TIMEOUT;
38
45
  // Using 'unknown' for resolve since the actual types vary by command
39
- private result_futures: Record<string, { resolve: (value: unknown) => void; reject: (reason?: unknown) => void }> =
40
- {};
41
- private msgId = 0;
46
+ private result_futures: Record<
47
+ string,
48
+ {
49
+ resolve: (value: unknown) => void;
50
+ reject: (reason?: unknown) => void;
51
+ timeoutId?: ReturnType<typeof setTimeout>;
52
+ }
53
+ > = {};
54
+ // Start with random offset for defense-in-depth and easier debugging across sessions
55
+ private msgId = Math.floor(Math.random() * 0x7fffffff);
42
56
  private eventListeners: Record<string, Array<() => void>> = {};
43
57
 
44
58
  /**
@@ -71,134 +85,182 @@ export class MatterClient {
71
85
  };
72
86
  }
73
87
 
74
- async commissionWithCode(code: string, networkOnly = true): Promise<MatterNode> {
88
+ async commissionWithCode(code: string, networkOnly = true, timeout?: number): Promise<MatterNode> {
75
89
  // Commission a device using a QR Code or Manual Pairing Code.
76
90
  // code: The QR Code or Manual Pairing Code for device commissioning.
77
91
  // network_only: If True, restricts device discovery to network only.
92
+ // timeout: Optional command timeout in milliseconds.
78
93
  // Returns: The NodeInfo of the commissioned device.
79
- return await this.sendCommand("commission_with_code", 0, {
80
- code: code,
81
- network_only: networkOnly,
82
- });
94
+ return await this.sendCommand(
95
+ "commission_with_code",
96
+ 0,
97
+ {
98
+ code: code,
99
+ network_only: networkOnly,
100
+ },
101
+ timeout,
102
+ );
83
103
  }
84
104
 
85
- async setWifiCredentials(ssid: string, credentials: string) {
105
+ async setWifiCredentials(ssid: string, credentials: string, timeout?: number): Promise<void> {
86
106
  // Set WiFi credentials for commissioning to a (new) device.
87
- await this.sendCommand("set_wifi_credentials", 0, { ssid, credentials });
107
+ await this.sendCommand("set_wifi_credentials", 0, { ssid, credentials }, timeout);
88
108
  }
89
109
 
90
- async setThreadOperationalDataset(dataset: string) {
110
+ async setThreadOperationalDataset(dataset: string, timeout?: number): Promise<void> {
91
111
  // Set Thread Operational dataset in the stack.
92
- await this.sendCommand("set_thread_dataset", 0, { dataset });
112
+ await this.sendCommand("set_thread_dataset", 0, { dataset }, timeout);
93
113
  }
94
114
 
95
115
  async openCommissioningWindow(
96
116
  nodeId: number | bigint,
97
- timeout?: number,
117
+ windowTimeout?: number,
98
118
  iteration?: number,
99
119
  option?: number,
100
120
  discriminator?: number,
121
+ timeout?: number,
101
122
  ): Promise<CommissioningParameters> {
102
123
  // Open a commissioning window to commission a device present on this controller to another.
124
+ // windowTimeout: How long to keep the commissioning window open (in seconds).
125
+ // timeout: Optional command timeout in milliseconds.
103
126
  // Returns code to use as discriminator.
104
- return await this.sendCommand("open_commissioning_window", 0, {
105
- node_id: nodeId,
127
+ return await this.sendCommand(
128
+ "open_commissioning_window",
129
+ 0,
130
+ {
131
+ node_id: nodeId,
132
+ timeout: windowTimeout,
133
+ iteration,
134
+ option,
135
+ discriminator,
136
+ },
106
137
  timeout,
107
- iteration,
108
- option,
109
- discriminator,
110
- });
138
+ );
111
139
  }
112
140
 
113
- async discoverCommissionableNodes(): Promise<CommissionableNodeData[]> {
141
+ async discoverCommissionableNodes(timeout?: number): Promise<CommissionableNodeData[]> {
114
142
  // Discover Commissionable Nodes (discovered on BLE or mDNS).
115
- return await this.sendCommand("discover_commissionable_nodes", 0, {});
143
+ return await this.sendCommand("discover_commissionable_nodes", 0, {}, timeout);
116
144
  }
117
145
 
118
- async getMatterFabrics(nodeId: number | bigint): Promise<MatterFabricData[]> {
146
+ async getMatterFabrics(nodeId: number | bigint, timeout?: number): Promise<MatterFabricData[]> {
119
147
  // Get Matter fabrics from a device.
120
148
  // Returns a list of MatterFabricData objects.
121
- return await this.sendCommand("get_matter_fabrics", 3, { node_id: nodeId });
149
+ return await this.sendCommand("get_matter_fabrics", 3, { node_id: nodeId }, timeout);
122
150
  }
123
151
 
124
- async removeMatterFabric(nodeId: number | bigint, fabricIndex: number) {
152
+ async removeMatterFabric(nodeId: number | bigint, fabricIndex: number, timeout?: number): Promise<void> {
125
153
  // Remove a Matter fabric from a device.
126
- await this.sendCommand("remove_matter_fabric", 3, { node_id: nodeId, fabric_index: fabricIndex });
154
+ await this.sendCommand("remove_matter_fabric", 3, { node_id: nodeId, fabric_index: fabricIndex }, timeout);
127
155
  }
128
156
 
129
- async pingNode(nodeId: number | bigint, attempts = 1): Promise<NodePingResult> {
157
+ async pingNode(nodeId: number | bigint, attempts = 1, timeout?: number): Promise<NodePingResult> {
130
158
  // Ping node on the currently known IP-address(es).
131
- return await this.sendCommand("ping_node", 0, { node_id: nodeId, attempts });
159
+ return await this.sendCommand("ping_node", 0, { node_id: nodeId, attempts }, timeout);
132
160
  }
133
161
 
134
- async getNodeIPAddresses(nodeId: number | bigint, preferCache?: boolean, scoped?: boolean): Promise<string[]> {
162
+ async getNodeIPAddresses(
163
+ nodeId: number | bigint,
164
+ preferCache?: boolean,
165
+ scoped?: boolean,
166
+ timeout?: number,
167
+ ): Promise<string[]> {
135
168
  // Return the currently known (scoped) IP-address(es).
136
- return await this.sendCommand("get_node_ip_addresses", 8, {
137
- node_id: nodeId,
138
- prefer_cache: preferCache,
139
- scoped: scoped,
140
- });
169
+ return await this.sendCommand(
170
+ "get_node_ip_addresses",
171
+ 8,
172
+ {
173
+ node_id: nodeId,
174
+ prefer_cache: preferCache,
175
+ scoped: scoped,
176
+ },
177
+ timeout,
178
+ );
141
179
  }
142
180
 
143
- async removeNode(nodeId: number | bigint) {
181
+ async removeNode(nodeId: number | bigint, timeout?: number): Promise<void> {
144
182
  // Remove a Matter node/device from the fabric.
145
- await this.sendCommand("remove_node", 0, { node_id: nodeId });
183
+ await this.sendCommand("remove_node", 0, { node_id: nodeId }, timeout);
146
184
  }
147
185
 
148
- async interviewNode(nodeId: number | bigint) {
186
+ async interviewNode(nodeId: number | bigint, timeout?: number): Promise<void> {
149
187
  // Interview a node.
150
- await this.sendCommand("interview_node", 0, { node_id: nodeId });
188
+ await this.sendCommand("interview_node", 0, { node_id: nodeId }, timeout);
151
189
  }
152
190
 
153
- async importTestNode(dump: string) {
191
+ async importTestNode(dump: string, timeout?: number): Promise<void> {
154
192
  // Import test node(s) from a HA or Matter server diagnostics dump.
155
- await this.sendCommand("import_test_node", 0, { dump });
193
+ await this.sendCommand("import_test_node", 0, { dump }, timeout);
156
194
  }
157
195
 
158
- async readAttribute(nodeId: number | bigint, attributePath: string | string[]): Promise<Record<string, unknown>> {
196
+ async readAttribute(
197
+ nodeId: number | bigint,
198
+ attributePath: string | string[],
199
+ timeout?: number,
200
+ ): Promise<Record<string, unknown>> {
159
201
  // Read one or more attribute(s) on a node by specifying an attributepath.
160
- return await this.sendCommand("read_attribute", 0, { node_id: nodeId, attribute_path: attributePath });
202
+ return await this.sendCommand("read_attribute", 0, { node_id: nodeId, attribute_path: attributePath }, timeout);
161
203
  }
162
204
 
163
- async writeAttribute(nodeId: number | bigint, attributePath: string, value: unknown): Promise<unknown> {
205
+ async writeAttribute(
206
+ nodeId: number | bigint,
207
+ attributePath: string,
208
+ value: unknown,
209
+ timeout?: number,
210
+ ): Promise<unknown> {
164
211
  // Write an attribute(value) on a target node.
165
- return await this.sendCommand("write_attribute", 0, {
166
- node_id: nodeId,
167
- attribute_path: attributePath,
168
- value: value,
169
- });
212
+ return await this.sendCommand(
213
+ "write_attribute",
214
+ 0,
215
+ {
216
+ node_id: nodeId,
217
+ attribute_path: attributePath,
218
+ value: value,
219
+ },
220
+ timeout,
221
+ );
170
222
  }
171
223
 
172
- async checkNodeUpdate(nodeId: number | bigint): Promise<MatterSoftwareVersion | null> {
224
+ async checkNodeUpdate(nodeId: number | bigint, timeout?: number): Promise<MatterSoftwareVersion | null> {
173
225
  // Check if there is an update for a particular node.
174
226
  // Reads the current software version and checks the DCL if there is an update
175
227
  // available. If there is an update available, the command returns the version
176
228
  // information of the latest update available.
177
- return await this.sendCommand("check_node_update", 10, { node_id: nodeId });
229
+ return await this.sendCommand("check_node_update", 10, { node_id: nodeId }, timeout);
178
230
  }
179
231
 
180
- async updateNode(nodeId: number | bigint, softwareVersion: number | string) {
232
+ async updateNode(nodeId: number | bigint, softwareVersion: number | string, timeout?: number): Promise<void> {
181
233
  // Update a node to a new software version.
182
234
  // This command checks if the requested software version is indeed still available
183
235
  // and if so, it will start the update process. The update process will be handled
184
236
  // by the built-in OTA provider. The OTA provider will download the update and
185
237
  // notify the node about the new update.
186
- await this.sendCommand("update_node", 10, { node_id: nodeId, software_version: softwareVersion });
238
+ await this.sendCommand("update_node", 10, { node_id: nodeId, software_version: softwareVersion }, timeout);
187
239
  }
188
240
 
189
- async setACLEntry(nodeId: number | bigint, entry: AccessControlEntry[]) {
190
- return await this.sendCommand("set_acl_entry", 0, {
191
- node_id: nodeId,
192
- entry: entry,
193
- });
241
+ async setACLEntry(nodeId: number | bigint, entry: AccessControlEntry[], timeout?: number) {
242
+ return await this.sendCommand(
243
+ "set_acl_entry",
244
+ 0,
245
+ {
246
+ node_id: nodeId,
247
+ entry: entry,
248
+ },
249
+ timeout,
250
+ );
194
251
  }
195
252
 
196
- async setNodeBinding(nodeId: number | bigint, endpoint: number, bindings: BindingTarget[]) {
197
- return await this.sendCommand("set_node_binding", 0, {
198
- node_id: nodeId,
199
- endpoint: endpoint,
200
- bindings: bindings,
201
- });
253
+ async setNodeBinding(nodeId: number | bigint, endpoint: number, bindings: BindingTarget[], timeout?: number) {
254
+ return await this.sendCommand(
255
+ "set_node_binding",
256
+ 0,
257
+ {
258
+ node_id: nodeId,
259
+ endpoint: endpoint,
260
+ bindings: bindings,
261
+ },
262
+ timeout,
263
+ );
202
264
  }
203
265
 
204
266
  async deviceCommand(
@@ -207,41 +269,90 @@ export class MatterClient {
207
269
  clusterId: number,
208
270
  commandName: string,
209
271
  payload: Record<string, unknown> = {},
272
+ timeout?: number,
210
273
  ): Promise<unknown> {
211
- return await this.sendCommand("device_command", 0, {
212
- node_id: nodeId,
213
- endpoint_id: endpointId,
214
- cluster_id: clusterId,
215
- command_name: commandName,
216
- payload,
217
- response_type: null,
218
- });
274
+ return await this.sendCommand(
275
+ "device_command",
276
+ 0,
277
+ {
278
+ node_id: nodeId,
279
+ endpoint_id: endpointId,
280
+ cluster_id: clusterId,
281
+ command_name: commandName,
282
+ payload,
283
+ response_type: null,
284
+ },
285
+ timeout,
286
+ );
287
+ }
288
+
289
+ async getNodes(onlyAvailable = false, timeout?: number): Promise<MatterNode[]> {
290
+ return await this.sendCommand("get_nodes", 0, { only_available: onlyAvailable }, timeout);
219
291
  }
220
292
 
221
- async getNodes(onlyAvailable = false): Promise<MatterNode[]> {
222
- return await this.sendCommand("get_nodes", 0, { only_available: onlyAvailable });
293
+ async getNode(nodeId: number | bigint, timeout?: number): Promise<MatterNode> {
294
+ return await this.sendCommand("get_node", 0, { node_id: nodeId }, timeout);
223
295
  }
224
296
 
225
- async getNode(nodeId: number | bigint): Promise<MatterNode> {
226
- return await this.sendCommand("get_node", 0, { node_id: nodeId });
297
+ async getVendorNames(filterVendors?: number[], timeout?: number): Promise<Record<string, string>> {
298
+ return await this.sendCommand("get_vendor_names", 0, { filter_vendors: filterVendors }, timeout);
227
299
  }
228
300
 
229
- async getVendorNames(filterVendors?: number[]): Promise<Record<string, string>> {
230
- return await this.sendCommand("get_vendor_names", 0, { filter_vendors: filterVendors });
301
+ async fetchServerInfo(timeout?: number) {
302
+ return await this.sendCommand("server_info", 0, {}, timeout);
231
303
  }
232
304
 
233
- async fetchServerInfo() {
234
- return await this.sendCommand("server_info", 0, {});
305
+ async setDefaultFabricLabel(label: string | null, timeout?: number): Promise<void> {
306
+ await this.sendCommand("set_default_fabric_label", 0, { label }, timeout);
235
307
  }
236
308
 
237
- async setDefaultFabricLabel(label: string | null) {
238
- await this.sendCommand("set_default_fabric_label", 0, { label });
309
+ /**
310
+ * Get the current log levels for console and file logging.
311
+ * @param timeout Optional command timeout in milliseconds
312
+ * @returns The current log level configuration
313
+ */
314
+ async getLogLevel(timeout?: number): Promise<LogLevelResponse> {
315
+ return await this.sendCommand("get_loglevel", 0, {}, timeout);
316
+ }
317
+
318
+ /**
319
+ * Set the log level for console and/or file logging.
320
+ * Changes are temporary and will be reset when the server restarts.
321
+ * @param consoleLoglevel Console log level to set (optional)
322
+ * @param fileLoglevel File log level to set, only applied if file logging is enabled (optional)
323
+ * @param timeout Optional command timeout in milliseconds
324
+ * @returns The log level configuration after the change
325
+ */
326
+ async setLogLevel(
327
+ consoleLoglevel?: LogLevelString,
328
+ fileLoglevel?: LogLevelString,
329
+ timeout?: number,
330
+ ): Promise<LogLevelResponse> {
331
+ return await this.sendCommand(
332
+ "set_loglevel",
333
+ 0,
334
+ {
335
+ console_loglevel: consoleLoglevel,
336
+ file_loglevel: fileLoglevel,
337
+ },
338
+ timeout,
339
+ );
239
340
  }
240
341
 
342
+ /**
343
+ * Send a command to the Matter server.
344
+ * @param command The command name
345
+ * @param require_schema Minimum schema version required (0 for any version)
346
+ * @param args Command arguments
347
+ * @param timeout Optional timeout in milliseconds. Defaults to `commandTimeout`. Set to 0 to disable.
348
+ * @returns Promise that resolves with the command result
349
+ * @throws Error if the command times out or fails
350
+ */
241
351
  sendCommand<T extends keyof APICommands>(
242
352
  command: T,
243
353
  require_schema: number | undefined = undefined,
244
354
  args: APICommands[T]["requestArgs"],
355
+ timeout = this.commandTimeout,
245
356
  ): Promise<APICommands[T]["response"]> {
246
357
  if (require_schema && this.serverInfo.schema_version < require_schema) {
247
358
  throw new InvalidServerVersion(
@@ -250,26 +361,88 @@ export class MatterClient {
250
361
  );
251
362
  }
252
363
 
253
- const messageId = ++this.msgId;
364
+ // Reset counter before overflow to maintain precision
365
+ if (this.msgId >= Number.MAX_SAFE_INTEGER) {
366
+ this.msgId = 0;
367
+ }
368
+ const messageId = String(++this.msgId);
254
369
 
255
370
  const message = {
256
- message_id: messageId.toString(),
371
+ message_id: messageId,
257
372
  command,
258
373
  args,
259
374
  };
260
375
 
261
- const messagePromise = new Promise<APICommands[T]["response"]>((resolve, reject) => {
376
+ return new Promise<APICommands[T]["response"]>((resolve, reject) => {
377
+ // Set up timeout if enabled
378
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
379
+ if (timeout > 0) {
380
+ timeoutId = setTimeout(() => {
381
+ // Check if still pending (not already resolved/rejected)
382
+ const pending = this.result_futures[messageId];
383
+ if (pending) {
384
+ // Clear timeout and delete entry BEFORE rejecting to prevent double resolution
385
+ if (pending.timeoutId) {
386
+ clearTimeout(pending.timeoutId);
387
+ }
388
+ delete this.result_futures[messageId];
389
+ reject(new CommandTimeoutError(command, timeout));
390
+ }
391
+ }, timeout);
392
+ }
393
+
262
394
  // Type-erased storage: resolve/reject are stored as unknown handlers
263
395
  this.result_futures[messageId] = {
264
396
  resolve: resolve as (value: unknown) => void,
265
397
  reject,
398
+ timeoutId,
266
399
  };
267
400
  this.connection.sendMessage(message);
268
401
  });
402
+ }
403
+
404
+ /**
405
+ * Safely resolve a pending command, ensuring it's only resolved once.
406
+ * Clears timeout and removes from pending futures before resolving.
407
+ */
408
+ private _resolvePendingCommand(messageId: string, result: unknown): void {
409
+ const pending = this.result_futures[messageId];
410
+ if (pending) {
411
+ // Clear timeout and delete entry BEFORE resolving to prevent double resolution
412
+ if (pending.timeoutId) {
413
+ clearTimeout(pending.timeoutId);
414
+ }
415
+ delete this.result_futures[messageId];
416
+ pending.resolve(result);
417
+ }
418
+ }
269
419
 
270
- return messagePromise.finally(() => {
420
+ /**
421
+ * Safely reject a pending command, ensuring it's only rejected once.
422
+ * Clears timeout and removes from pending futures before rejecting.
423
+ */
424
+ private _rejectPendingCommand(messageId: string, error: Error): void {
425
+ const pending = this.result_futures[messageId];
426
+ if (pending) {
427
+ // Clear timeout and delete entry BEFORE rejecting to prevent double resolution
428
+ if (pending.timeoutId) {
429
+ clearTimeout(pending.timeoutId);
430
+ }
271
431
  delete this.result_futures[messageId];
272
- });
432
+ pending.reject(error);
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Reject all pending commands with a ConnectionClosedError.
438
+ * Called when the connection is closed or lost.
439
+ */
440
+ private _rejectAllPendingCommands(): void {
441
+ const error = new ConnectionClosedError();
442
+ const pendingIds = Object.keys(this.result_futures);
443
+ for (const messageId of pendingIds) {
444
+ this._rejectPendingCommand(messageId, error);
445
+ }
273
446
  }
274
447
 
275
448
  async connect() {
@@ -278,11 +451,16 @@ export class MatterClient {
278
451
  }
279
452
  await this.connection.connect(
280
453
  msg => this._handleIncomingMessage(msg as IncomingMessage),
281
- () => this.fireEvent("connection_lost"),
454
+ () => {
455
+ this._rejectAllPendingCommands();
456
+ this.fireEvent("connection_lost");
457
+ },
282
458
  );
283
459
  }
284
460
 
285
461
  disconnect(clearStorage = false) {
462
+ // Reject all pending commands before disconnecting
463
+ this._rejectAllPendingCommands();
286
464
  // disconnect from the server
287
465
  if (this.connection && this.connection.connected) {
288
466
  this.connection.disconnect();
@@ -312,20 +490,12 @@ export class MatterClient {
312
490
  }
313
491
 
314
492
  if ("error_code" in msg) {
315
- const promise = this.result_futures[msg.message_id];
316
- if (promise) {
317
- promise.reject(new Error(msg.details));
318
- delete this.result_futures[msg.message_id];
319
- }
493
+ this._rejectPendingCommand(msg.message_id, new Error(msg.details));
320
494
  return;
321
495
  }
322
496
 
323
497
  if ("result" in msg) {
324
- const promise = this.result_futures[msg.message_id];
325
- if (promise) {
326
- promise.resolve(msg.result);
327
- delete this.result_futures[msg.message_id];
328
- }
498
+ this._resolvePendingCommand(msg.message_id, msg.result);
329
499
  return;
330
500
  }
331
501
 
package/src/connection.ts CHANGED
@@ -62,6 +62,9 @@ export class Connection {
62
62
 
63
63
  this.socket.onclose = () => {
64
64
  console.log("WebSocket Closed");
65
+ // Clean up so reconnect can work
66
+ this.socket = undefined;
67
+ this.serverInfo = undefined;
65
68
  onConnectionLost();
66
69
  };
67
70
 
@@ -89,6 +92,8 @@ export class Connection {
89
92
  this.socket.close();
90
93
  this.socket = undefined;
91
94
  }
95
+ // Reset serverInfo so reconnect will properly handle the initial server info message
96
+ this.serverInfo = undefined;
92
97
  }
93
98
 
94
99
  sendMessage(message: CommandMessage): void {
package/src/exceptions.ts CHANGED
@@ -7,3 +7,26 @@
7
7
  export class MatterError extends Error {}
8
8
 
9
9
  export class InvalidServerVersion extends MatterError {}
10
+
11
+ /**
12
+ * Error thrown when a WebSocket command times out waiting for a response.
13
+ */
14
+ export class CommandTimeoutError extends MatterError {
15
+ constructor(
16
+ public readonly command: string,
17
+ public readonly timeoutMs: number,
18
+ ) {
19
+ super(`Command '${command}' timed out after ${timeoutMs}ms`);
20
+ this.name = "CommandTimeoutError";
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Error thrown when the WebSocket connection is closed while commands are pending.
26
+ */
27
+ export class ConnectionClosedError extends MatterError {
28
+ constructor(message = "Connection closed while command was pending") {
29
+ super(message);
30
+ this.name = "ConnectionClosedError";
31
+ }
32
+ }
@@ -161,6 +161,23 @@ export interface APICommands {
161
161
  requestArgs: { label: string | null };
162
162
  response: Record<string, never>;
163
163
  };
164
+ get_loglevel: {
165
+ requestArgs: Record<string, never>;
166
+ response: LogLevelResponse;
167
+ };
168
+ set_loglevel: {
169
+ requestArgs: { console_loglevel?: LogLevelString; file_loglevel?: LogLevelString };
170
+ response: LogLevelResponse;
171
+ };
172
+ }
173
+
174
+ /** Log level string values matching CLI options */
175
+ export type LogLevelString = "critical" | "error" | "warning" | "info" | "debug";
176
+
177
+ /** Response for get_loglevel and set_loglevel commands */
178
+ export interface LogLevelResponse {
179
+ console_loglevel: LogLevelString;
180
+ file_loglevel: LogLevelString | null;
164
181
  }
165
182
 
166
183
  /** Access Control Entry for set_acl_entry command */
@@ -213,6 +230,8 @@ export interface CommandMessage {
213
230
  export interface ServerInfoMessage {
214
231
  fabric_id: number | bigint;
215
232
  compressed_fabric_id: number | bigint;
233
+ /** The fabric index. Note: Only available with OHF Matter Server, not Python Matter Server. */
234
+ fabric_index?: number;
216
235
  schema_version: number;
217
236
  min_supported_schema_version: number;
218
237
  sdk_version: string;
@@ -342,3 +361,18 @@ export interface MatterFabricData {
342
361
 
343
362
  export type NotificationType = "success" | "info" | "warning" | "error";
344
363
  export type NodePingResult = Record<string, boolean>;
364
+
365
+ /**
366
+ * Minimum test node ID. Node IDs >= this value are reserved for test nodes.
367
+ * Uses high 64-bit range (0xFFFF_FFFE_0000_0000) to avoid collision with real node IDs.
368
+ */
369
+ export const TEST_NODE_START = 0xffff_fffe_0000_0000n;
370
+
371
+ /**
372
+ * Check if a node ID is in the test node range (>= TEST_NODE_START).
373
+ * Test nodes are imported diagnostic dumps, not real commissioned devices.
374
+ */
375
+ export function isTestNodeId(nodeId: number | bigint): boolean {
376
+ const bigId = typeof nodeId === "bigint" ? nodeId : BigInt(nodeId);
377
+ return bigId >= TEST_NODE_START;
378
+ }