@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/README.md CHANGED
@@ -85,6 +85,7 @@ new MatterClient(url: string, wsFactory?: WebSocketFactory)
85
85
  - `serverInfo`: Server information (fabric ID, SDK version, etc.)
86
86
  - `serverBaseAddress`: The base address extracted from the URL
87
87
  - `isProduction`: Whether connected to a production server (for UI purposes)
88
+ - `commandTimeout`: Default timeout for commands in milliseconds (default: 5 minutes). Set to `0` to disable timeouts.
88
89
 
89
90
  #### Methods
90
91
 
@@ -114,6 +115,90 @@ new MatterClient(url: string, wsFactory?: WebSocketFactory)
114
115
  - `server_info_updated`: Fired when server info changes
115
116
  - `connection_lost`: Fired when connection is lost
116
117
 
118
+ ### Server Info
119
+
120
+ The `serverInfo` property contains information about the connected Matter server:
121
+
122
+ ```typescript
123
+ interface ServerInfoMessage {
124
+ fabric_id: bigint; // The fabric ID
125
+ compressed_fabric_id: bigint; // Compressed fabric ID (global ID)
126
+ fabric_index?: number; // The fabric index (OHF Matter Server only)
127
+ schema_version: number; // API schema version
128
+ min_supported_schema_version: number;
129
+ sdk_version: string; // Server SDK version string
130
+ wifi_credentials_set: boolean; // Whether WiFi credentials are configured
131
+ thread_credentials_set: boolean; // Whether Thread dataset is configured
132
+ bluetooth_enabled: boolean; // Whether BLE commissioning is available
133
+ }
134
+ ```
135
+
136
+ **Note:** The `fabric_index` field is specific to OHF Matter Server and is not available in Python Matter Server. When connecting to Python Matter Server, this field will be undefined.
137
+
138
+ ### Command Timeouts
139
+
140
+ All commands have a default timeout of 5 minutes (300,000ms) to prevent promises from hanging indefinitely if the server doesn't respond. You can configure this behavior globally or per-call:
141
+
142
+ ```typescript
143
+ import { MatterClient, CommandTimeoutError, DEFAULT_COMMAND_TIMEOUT } from "@matter-server/ws-client";
144
+
145
+ const client = new MatterClient("ws://localhost:5580/ws");
146
+
147
+ // Check the default timeout (5 minutes)
148
+ console.log(DEFAULT_COMMAND_TIMEOUT); // 300000
149
+
150
+ // Change the default timeout for all commands (e.g., 1 minute)
151
+ client.commandTimeout = 60000;
152
+
153
+ // Disable timeouts entirely (not recommended)
154
+ client.commandTimeout = 0;
155
+
156
+ // Override timeout for a specific call (e.g., 30 seconds for a quick command)
157
+ await client.deviceCommand(nodeId, 1, 6, "toggle", {}, 30000);
158
+
159
+ // Use a longer timeout for operations that take time (e.g., 10 minutes for commissioning)
160
+ await client.commissionWithCode("MT:Y3.5UNQO100KA0648G00", false, 600000);
161
+
162
+ // Handle timeout errors
163
+ try {
164
+ await client.deviceCommand(nodeId, 1, 6, "toggle");
165
+ } catch (err) {
166
+ if (err instanceof CommandTimeoutError) {
167
+ console.log(`Command '${err.command}' timed out after ${err.timeoutMs}ms`);
168
+ }
169
+ }
170
+ ```
171
+
172
+ All client methods accept an optional `timeout` parameter as their last argument to override the default timeout for that specific call.
173
+
174
+ ### Connection Handling
175
+
176
+ When the WebSocket connection is closed (either by calling `disconnect()` or due to connection loss), all pending commands are automatically rejected with a `ConnectionClosedError`:
177
+
178
+ ```typescript
179
+ import { MatterClient, ConnectionClosedError } from "@matter-server/ws-client";
180
+
181
+ const client = new MatterClient("ws://localhost:5580/ws");
182
+ await client.connect();
183
+
184
+ // Start a long-running command
185
+ const commandPromise = client.commissionWithCode("MT:Y3.5UNQO100KA0648G00", false);
186
+
187
+ // If the connection is lost or disconnected while the command is pending:
188
+ try {
189
+ await commandPromise;
190
+ } catch (err) {
191
+ if (err instanceof ConnectionClosedError) {
192
+ console.log("Connection was closed while command was pending");
193
+ }
194
+ }
195
+
196
+ // Listen for connection loss events
197
+ client.addEventListener("connection_lost", () => {
198
+ console.log("Connection to server was lost");
199
+ });
200
+ ```
201
+
117
202
  ### Other Exports
118
203
 
119
204
  ```typescript
@@ -126,6 +211,11 @@ import {
126
211
  // Exceptions
127
212
  MatterError,
128
213
  InvalidServerVersion,
214
+ CommandTimeoutError,
215
+ ConnectionClosedError,
216
+
217
+ // Constants
218
+ DEFAULT_COMMAND_TIMEOUT,
129
219
 
130
220
  // Types
131
221
  ServerInfoMessage,
@@ -4,8 +4,10 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { Connection, WebSocketFactory } from "./connection.js";
7
- import { AccessControlEntry, APICommands, BindingTarget, CommissionableNodeData, CommissioningParameters, EventMessage, MatterFabricData, MatterSoftwareVersion, NodePingResult } from "./models/model.js";
7
+ import { AccessControlEntry, APICommands, BindingTarget, CommissionableNodeData, CommissioningParameters, EventMessage, LogLevelResponse, LogLevelString, MatterFabricData, MatterSoftwareVersion, NodePingResult } from "./models/model.js";
8
8
  import { MatterNode } from "./models/node.js";
9
+ /** Default timeout for WebSocket commands in milliseconds (5 minutes) */
10
+ export declare const DEFAULT_COMMAND_TIMEOUT: number;
9
11
  export declare class MatterClient {
10
12
  url: string;
11
13
  connection: Connection;
@@ -13,6 +15,8 @@ export declare class MatterClient {
13
15
  serverBaseAddress: string;
14
16
  /** Whether this client is connected to a production server (optional, for UI purposes) */
15
17
  isProduction: boolean;
18
+ /** Default timeout for commands in milliseconds. Set to 0 to disable timeouts. */
19
+ commandTimeout: number;
16
20
  private result_futures;
17
21
  private msgId;
18
22
  private eventListeners;
@@ -26,31 +30,70 @@ export declare class MatterClient {
26
30
  constructor(url: string, wsFactory?: WebSocketFactory);
27
31
  get serverInfo(): import("./models/model.js").ServerInfoMessage;
28
32
  addEventListener(event: string, listener: () => void): () => void;
29
- commissionWithCode(code: string, networkOnly?: boolean): Promise<MatterNode>;
30
- setWifiCredentials(ssid: string, credentials: string): Promise<void>;
31
- setThreadOperationalDataset(dataset: string): Promise<void>;
32
- openCommissioningWindow(nodeId: number | bigint, timeout?: number, iteration?: number, option?: number, discriminator?: number): Promise<CommissioningParameters>;
33
- discoverCommissionableNodes(): Promise<CommissionableNodeData[]>;
34
- getMatterFabrics(nodeId: number | bigint): Promise<MatterFabricData[]>;
35
- removeMatterFabric(nodeId: number | bigint, fabricIndex: number): Promise<void>;
36
- pingNode(nodeId: number | bigint, attempts?: number): Promise<NodePingResult>;
37
- getNodeIPAddresses(nodeId: number | bigint, preferCache?: boolean, scoped?: boolean): Promise<string[]>;
38
- removeNode(nodeId: number | bigint): Promise<void>;
39
- interviewNode(nodeId: number | bigint): Promise<void>;
40
- importTestNode(dump: string): Promise<void>;
41
- readAttribute(nodeId: number | bigint, attributePath: string | string[]): Promise<Record<string, unknown>>;
42
- writeAttribute(nodeId: number | bigint, attributePath: string, value: unknown): Promise<unknown>;
43
- checkNodeUpdate(nodeId: number | bigint): Promise<MatterSoftwareVersion | null>;
44
- updateNode(nodeId: number | bigint, softwareVersion: number | string): Promise<void>;
45
- setACLEntry(nodeId: number | bigint, entry: AccessControlEntry[]): Promise<import("./models/model.js").AttributeWriteResult[] | null>;
46
- setNodeBinding(nodeId: number | bigint, endpoint: number, bindings: BindingTarget[]): Promise<import("./models/model.js").AttributeWriteResult[] | null>;
47
- deviceCommand(nodeId: number | bigint, endpointId: number, clusterId: number, commandName: string, payload?: Record<string, unknown>): Promise<unknown>;
48
- getNodes(onlyAvailable?: boolean): Promise<MatterNode[]>;
49
- getNode(nodeId: number | bigint): Promise<MatterNode>;
50
- getVendorNames(filterVendors?: number[]): Promise<Record<string, string>>;
51
- fetchServerInfo(): Promise<import("./models/model.js").ServerInfoMessage>;
52
- setDefaultFabricLabel(label: string | null): Promise<void>;
53
- sendCommand<T extends keyof APICommands>(command: T, require_schema: number | undefined, args: APICommands[T]["requestArgs"]): Promise<APICommands[T]["response"]>;
33
+ commissionWithCode(code: string, networkOnly?: boolean, timeout?: number): Promise<MatterNode>;
34
+ setWifiCredentials(ssid: string, credentials: string, timeout?: number): Promise<void>;
35
+ setThreadOperationalDataset(dataset: string, timeout?: number): Promise<void>;
36
+ openCommissioningWindow(nodeId: number | bigint, windowTimeout?: number, iteration?: number, option?: number, discriminator?: number, timeout?: number): Promise<CommissioningParameters>;
37
+ discoverCommissionableNodes(timeout?: number): Promise<CommissionableNodeData[]>;
38
+ getMatterFabrics(nodeId: number | bigint, timeout?: number): Promise<MatterFabricData[]>;
39
+ removeMatterFabric(nodeId: number | bigint, fabricIndex: number, timeout?: number): Promise<void>;
40
+ pingNode(nodeId: number | bigint, attempts?: number, timeout?: number): Promise<NodePingResult>;
41
+ getNodeIPAddresses(nodeId: number | bigint, preferCache?: boolean, scoped?: boolean, timeout?: number): Promise<string[]>;
42
+ removeNode(nodeId: number | bigint, timeout?: number): Promise<void>;
43
+ interviewNode(nodeId: number | bigint, timeout?: number): Promise<void>;
44
+ importTestNode(dump: string, timeout?: number): Promise<void>;
45
+ readAttribute(nodeId: number | bigint, attributePath: string | string[], timeout?: number): Promise<Record<string, unknown>>;
46
+ writeAttribute(nodeId: number | bigint, attributePath: string, value: unknown, timeout?: number): Promise<unknown>;
47
+ checkNodeUpdate(nodeId: number | bigint, timeout?: number): Promise<MatterSoftwareVersion | null>;
48
+ updateNode(nodeId: number | bigint, softwareVersion: number | string, timeout?: number): Promise<void>;
49
+ setACLEntry(nodeId: number | bigint, entry: AccessControlEntry[], timeout?: number): Promise<import("./models/model.js").AttributeWriteResult[] | null>;
50
+ setNodeBinding(nodeId: number | bigint, endpoint: number, bindings: BindingTarget[], timeout?: number): Promise<import("./models/model.js").AttributeWriteResult[] | null>;
51
+ deviceCommand(nodeId: number | bigint, endpointId: number, clusterId: number, commandName: string, payload?: Record<string, unknown>, timeout?: number): Promise<unknown>;
52
+ getNodes(onlyAvailable?: boolean, timeout?: number): Promise<MatterNode[]>;
53
+ getNode(nodeId: number | bigint, timeout?: number): Promise<MatterNode>;
54
+ getVendorNames(filterVendors?: number[], timeout?: number): Promise<Record<string, string>>;
55
+ fetchServerInfo(timeout?: number): Promise<import("./models/model.js").ServerInfoMessage>;
56
+ setDefaultFabricLabel(label: string | null, timeout?: number): Promise<void>;
57
+ /**
58
+ * Get the current log levels for console and file logging.
59
+ * @param timeout Optional command timeout in milliseconds
60
+ * @returns The current log level configuration
61
+ */
62
+ getLogLevel(timeout?: number): Promise<LogLevelResponse>;
63
+ /**
64
+ * Set the log level for console and/or file logging.
65
+ * Changes are temporary and will be reset when the server restarts.
66
+ * @param consoleLoglevel Console log level to set (optional)
67
+ * @param fileLoglevel File log level to set, only applied if file logging is enabled (optional)
68
+ * @param timeout Optional command timeout in milliseconds
69
+ * @returns The log level configuration after the change
70
+ */
71
+ setLogLevel(consoleLoglevel?: LogLevelString, fileLoglevel?: LogLevelString, timeout?: number): Promise<LogLevelResponse>;
72
+ /**
73
+ * Send a command to the Matter server.
74
+ * @param command The command name
75
+ * @param require_schema Minimum schema version required (0 for any version)
76
+ * @param args Command arguments
77
+ * @param timeout Optional timeout in milliseconds. Defaults to `commandTimeout`. Set to 0 to disable.
78
+ * @returns Promise that resolves with the command result
79
+ * @throws Error if the command times out or fails
80
+ */
81
+ sendCommand<T extends keyof APICommands>(command: T, require_schema: number | undefined, args: APICommands[T]["requestArgs"], timeout?: number): Promise<APICommands[T]["response"]>;
82
+ /**
83
+ * Safely resolve a pending command, ensuring it's only resolved once.
84
+ * Clears timeout and removes from pending futures before resolving.
85
+ */
86
+ private _resolvePendingCommand;
87
+ /**
88
+ * Safely reject a pending command, ensuring it's only rejected once.
89
+ * Clears timeout and removes from pending futures before rejecting.
90
+ */
91
+ private _rejectPendingCommand;
92
+ /**
93
+ * Reject all pending commands with a ConnectionClosedError.
94
+ * Called when the connection is closed or lost.
95
+ */
96
+ private _rejectAllPendingCommands;
54
97
  connect(): Promise<void>;
55
98
  disconnect(clearStorage?: boolean): void;
56
99
  startListening(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
@@ -4,11 +4,12 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { Connection } from "./connection.js";
7
- import { InvalidServerVersion } from "./exceptions.js";
7
+ import { CommandTimeoutError, ConnectionClosedError, InvalidServerVersion } from "./exceptions.js";
8
8
  import { MatterNode } from "./models/node.js";
9
9
  function toNodeKey(nodeId) {
10
10
  return String(nodeId);
11
11
  }
12
+ const DEFAULT_COMMAND_TIMEOUT = 5 * 60 * 1e3;
12
13
  class MatterClient {
13
14
  /**
14
15
  * Create a new MatterClient.
@@ -28,9 +29,12 @@ class MatterClient {
28
29
  serverBaseAddress;
29
30
  /** Whether this client is connected to a production server (optional, for UI purposes) */
30
31
  isProduction = false;
32
+ /** Default timeout for commands in milliseconds. Set to 0 to disable timeouts. */
33
+ commandTimeout = DEFAULT_COMMAND_TIMEOUT;
31
34
  // Using 'unknown' for resolve since the actual types vary by command
32
35
  result_futures = {};
33
- msgId = 0;
36
+ // Start with random offset for defense-in-depth and easier debugging across sessions
37
+ msgId = Math.floor(Math.random() * 2147483647);
34
38
  eventListeners = {};
35
39
  get serverInfo() {
36
40
  return this.connection.serverInfo;
@@ -44,131 +48,255 @@ class MatterClient {
44
48
  this.eventListeners[event] = this.eventListeners[event].filter((l) => l !== listener);
45
49
  };
46
50
  }
47
- async commissionWithCode(code, networkOnly = true) {
48
- return await this.sendCommand("commission_with_code", 0, {
49
- code,
50
- network_only: networkOnly
51
- });
52
- }
53
- async setWifiCredentials(ssid, credentials) {
54
- await this.sendCommand("set_wifi_credentials", 0, { ssid, credentials });
55
- }
56
- async setThreadOperationalDataset(dataset) {
57
- await this.sendCommand("set_thread_dataset", 0, { dataset });
51
+ async commissionWithCode(code, networkOnly = true, timeout) {
52
+ return await this.sendCommand(
53
+ "commission_with_code",
54
+ 0,
55
+ {
56
+ code,
57
+ network_only: networkOnly
58
+ },
59
+ timeout
60
+ );
58
61
  }
59
- async openCommissioningWindow(nodeId, timeout, iteration, option, discriminator) {
60
- return await this.sendCommand("open_commissioning_window", 0, {
61
- node_id: nodeId,
62
- timeout,
63
- iteration,
64
- option,
65
- discriminator
66
- });
62
+ async setWifiCredentials(ssid, credentials, timeout) {
63
+ await this.sendCommand("set_wifi_credentials", 0, { ssid, credentials }, timeout);
64
+ }
65
+ async setThreadOperationalDataset(dataset, timeout) {
66
+ await this.sendCommand("set_thread_dataset", 0, { dataset }, timeout);
67
+ }
68
+ async openCommissioningWindow(nodeId, windowTimeout, iteration, option, discriminator, timeout) {
69
+ return await this.sendCommand(
70
+ "open_commissioning_window",
71
+ 0,
72
+ {
73
+ node_id: nodeId,
74
+ timeout: windowTimeout,
75
+ iteration,
76
+ option,
77
+ discriminator
78
+ },
79
+ timeout
80
+ );
67
81
  }
68
- async discoverCommissionableNodes() {
69
- return await this.sendCommand("discover_commissionable_nodes", 0, {});
82
+ async discoverCommissionableNodes(timeout) {
83
+ return await this.sendCommand("discover_commissionable_nodes", 0, {}, timeout);
70
84
  }
71
- async getMatterFabrics(nodeId) {
72
- return await this.sendCommand("get_matter_fabrics", 3, { node_id: nodeId });
85
+ async getMatterFabrics(nodeId, timeout) {
86
+ return await this.sendCommand("get_matter_fabrics", 3, { node_id: nodeId }, timeout);
73
87
  }
74
- async removeMatterFabric(nodeId, fabricIndex) {
75
- await this.sendCommand("remove_matter_fabric", 3, { node_id: nodeId, fabric_index: fabricIndex });
88
+ async removeMatterFabric(nodeId, fabricIndex, timeout) {
89
+ await this.sendCommand("remove_matter_fabric", 3, { node_id: nodeId, fabric_index: fabricIndex }, timeout);
76
90
  }
77
- async pingNode(nodeId, attempts = 1) {
78
- return await this.sendCommand("ping_node", 0, { node_id: nodeId, attempts });
91
+ async pingNode(nodeId, attempts = 1, timeout) {
92
+ return await this.sendCommand("ping_node", 0, { node_id: nodeId, attempts }, timeout);
79
93
  }
80
- async getNodeIPAddresses(nodeId, preferCache, scoped) {
81
- return await this.sendCommand("get_node_ip_addresses", 8, {
82
- node_id: nodeId,
83
- prefer_cache: preferCache,
84
- scoped
85
- });
94
+ async getNodeIPAddresses(nodeId, preferCache, scoped, timeout) {
95
+ return await this.sendCommand(
96
+ "get_node_ip_addresses",
97
+ 8,
98
+ {
99
+ node_id: nodeId,
100
+ prefer_cache: preferCache,
101
+ scoped
102
+ },
103
+ timeout
104
+ );
86
105
  }
87
- async removeNode(nodeId) {
88
- await this.sendCommand("remove_node", 0, { node_id: nodeId });
106
+ async removeNode(nodeId, timeout) {
107
+ await this.sendCommand("remove_node", 0, { node_id: nodeId }, timeout);
89
108
  }
90
- async interviewNode(nodeId) {
91
- await this.sendCommand("interview_node", 0, { node_id: nodeId });
109
+ async interviewNode(nodeId, timeout) {
110
+ await this.sendCommand("interview_node", 0, { node_id: nodeId }, timeout);
92
111
  }
93
- async importTestNode(dump) {
94
- await this.sendCommand("import_test_node", 0, { dump });
112
+ async importTestNode(dump, timeout) {
113
+ await this.sendCommand("import_test_node", 0, { dump }, timeout);
95
114
  }
96
- async readAttribute(nodeId, attributePath) {
97
- return await this.sendCommand("read_attribute", 0, { node_id: nodeId, attribute_path: attributePath });
115
+ async readAttribute(nodeId, attributePath, timeout) {
116
+ return await this.sendCommand("read_attribute", 0, { node_id: nodeId, attribute_path: attributePath }, timeout);
98
117
  }
99
- async writeAttribute(nodeId, attributePath, value) {
100
- return await this.sendCommand("write_attribute", 0, {
101
- node_id: nodeId,
102
- attribute_path: attributePath,
103
- value
104
- });
118
+ async writeAttribute(nodeId, attributePath, value, timeout) {
119
+ return await this.sendCommand(
120
+ "write_attribute",
121
+ 0,
122
+ {
123
+ node_id: nodeId,
124
+ attribute_path: attributePath,
125
+ value
126
+ },
127
+ timeout
128
+ );
105
129
  }
106
- async checkNodeUpdate(nodeId) {
107
- return await this.sendCommand("check_node_update", 10, { node_id: nodeId });
130
+ async checkNodeUpdate(nodeId, timeout) {
131
+ return await this.sendCommand("check_node_update", 10, { node_id: nodeId }, timeout);
132
+ }
133
+ async updateNode(nodeId, softwareVersion, timeout) {
134
+ await this.sendCommand("update_node", 10, { node_id: nodeId, software_version: softwareVersion }, timeout);
135
+ }
136
+ async setACLEntry(nodeId, entry, timeout) {
137
+ return await this.sendCommand(
138
+ "set_acl_entry",
139
+ 0,
140
+ {
141
+ node_id: nodeId,
142
+ entry
143
+ },
144
+ timeout
145
+ );
108
146
  }
109
- async updateNode(nodeId, softwareVersion) {
110
- await this.sendCommand("update_node", 10, { node_id: nodeId, software_version: softwareVersion });
147
+ async setNodeBinding(nodeId, endpoint, bindings, timeout) {
148
+ return await this.sendCommand(
149
+ "set_node_binding",
150
+ 0,
151
+ {
152
+ node_id: nodeId,
153
+ endpoint,
154
+ bindings
155
+ },
156
+ timeout
157
+ );
111
158
  }
112
- async setACLEntry(nodeId, entry) {
113
- return await this.sendCommand("set_acl_entry", 0, {
114
- node_id: nodeId,
115
- entry
116
- });
159
+ async deviceCommand(nodeId, endpointId, clusterId, commandName, payload = {}, timeout) {
160
+ return await this.sendCommand(
161
+ "device_command",
162
+ 0,
163
+ {
164
+ node_id: nodeId,
165
+ endpoint_id: endpointId,
166
+ cluster_id: clusterId,
167
+ command_name: commandName,
168
+ payload,
169
+ response_type: null
170
+ },
171
+ timeout
172
+ );
117
173
  }
118
- async setNodeBinding(nodeId, endpoint, bindings) {
119
- return await this.sendCommand("set_node_binding", 0, {
120
- node_id: nodeId,
121
- endpoint,
122
- bindings
123
- });
174
+ async getNodes(onlyAvailable = false, timeout) {
175
+ return await this.sendCommand("get_nodes", 0, { only_available: onlyAvailable }, timeout);
124
176
  }
125
- async deviceCommand(nodeId, endpointId, clusterId, commandName, payload = {}) {
126
- return await this.sendCommand("device_command", 0, {
127
- node_id: nodeId,
128
- endpoint_id: endpointId,
129
- cluster_id: clusterId,
130
- command_name: commandName,
131
- payload,
132
- response_type: null
133
- });
177
+ async getNode(nodeId, timeout) {
178
+ return await this.sendCommand("get_node", 0, { node_id: nodeId }, timeout);
134
179
  }
135
- async getNodes(onlyAvailable = false) {
136
- return await this.sendCommand("get_nodes", 0, { only_available: onlyAvailable });
180
+ async getVendorNames(filterVendors, timeout) {
181
+ return await this.sendCommand("get_vendor_names", 0, { filter_vendors: filterVendors }, timeout);
137
182
  }
138
- async getNode(nodeId) {
139
- return await this.sendCommand("get_node", 0, { node_id: nodeId });
183
+ async fetchServerInfo(timeout) {
184
+ return await this.sendCommand("server_info", 0, {}, timeout);
140
185
  }
141
- async getVendorNames(filterVendors) {
142
- return await this.sendCommand("get_vendor_names", 0, { filter_vendors: filterVendors });
186
+ async setDefaultFabricLabel(label, timeout) {
187
+ await this.sendCommand("set_default_fabric_label", 0, { label }, timeout);
143
188
  }
144
- async fetchServerInfo() {
145
- return await this.sendCommand("server_info", 0, {});
189
+ /**
190
+ * Get the current log levels for console and file logging.
191
+ * @param timeout Optional command timeout in milliseconds
192
+ * @returns The current log level configuration
193
+ */
194
+ async getLogLevel(timeout) {
195
+ return await this.sendCommand("get_loglevel", 0, {}, timeout);
146
196
  }
147
- async setDefaultFabricLabel(label) {
148
- await this.sendCommand("set_default_fabric_label", 0, { label });
197
+ /**
198
+ * Set the log level for console and/or file logging.
199
+ * Changes are temporary and will be reset when the server restarts.
200
+ * @param consoleLoglevel Console log level to set (optional)
201
+ * @param fileLoglevel File log level to set, only applied if file logging is enabled (optional)
202
+ * @param timeout Optional command timeout in milliseconds
203
+ * @returns The log level configuration after the change
204
+ */
205
+ async setLogLevel(consoleLoglevel, fileLoglevel, timeout) {
206
+ return await this.sendCommand(
207
+ "set_loglevel",
208
+ 0,
209
+ {
210
+ console_loglevel: consoleLoglevel,
211
+ file_loglevel: fileLoglevel
212
+ },
213
+ timeout
214
+ );
149
215
  }
150
- sendCommand(command, require_schema = void 0, args) {
216
+ /**
217
+ * Send a command to the Matter server.
218
+ * @param command The command name
219
+ * @param require_schema Minimum schema version required (0 for any version)
220
+ * @param args Command arguments
221
+ * @param timeout Optional timeout in milliseconds. Defaults to `commandTimeout`. Set to 0 to disable.
222
+ * @returns Promise that resolves with the command result
223
+ * @throws Error if the command times out or fails
224
+ */
225
+ sendCommand(command, require_schema = void 0, args, timeout = this.commandTimeout) {
151
226
  if (require_schema && this.serverInfo.schema_version < require_schema) {
152
227
  throw new InvalidServerVersion(
153
228
  `Command not available due to incompatible server version. Update the Matter Server to a version that supports at least api schema ${require_schema}.`
154
229
  );
155
230
  }
156
- const messageId = ++this.msgId;
231
+ if (this.msgId >= Number.MAX_SAFE_INTEGER) {
232
+ this.msgId = 0;
233
+ }
234
+ const messageId = String(++this.msgId);
157
235
  const message = {
158
- message_id: messageId.toString(),
236
+ message_id: messageId,
159
237
  command,
160
238
  args
161
239
  };
162
- const messagePromise = new Promise((resolve, reject) => {
240
+ return new Promise((resolve, reject) => {
241
+ let timeoutId;
242
+ if (timeout > 0) {
243
+ timeoutId = setTimeout(() => {
244
+ const pending = this.result_futures[messageId];
245
+ if (pending) {
246
+ if (pending.timeoutId) {
247
+ clearTimeout(pending.timeoutId);
248
+ }
249
+ delete this.result_futures[messageId];
250
+ reject(new CommandTimeoutError(command, timeout));
251
+ }
252
+ }, timeout);
253
+ }
163
254
  this.result_futures[messageId] = {
164
255
  resolve,
165
- reject
256
+ reject,
257
+ timeoutId
166
258
  };
167
259
  this.connection.sendMessage(message);
168
260
  });
169
- return messagePromise.finally(() => {
261
+ }
262
+ /**
263
+ * Safely resolve a pending command, ensuring it's only resolved once.
264
+ * Clears timeout and removes from pending futures before resolving.
265
+ */
266
+ _resolvePendingCommand(messageId, result) {
267
+ const pending = this.result_futures[messageId];
268
+ if (pending) {
269
+ if (pending.timeoutId) {
270
+ clearTimeout(pending.timeoutId);
271
+ }
170
272
  delete this.result_futures[messageId];
171
- });
273
+ pending.resolve(result);
274
+ }
275
+ }
276
+ /**
277
+ * Safely reject a pending command, ensuring it's only rejected once.
278
+ * Clears timeout and removes from pending futures before rejecting.
279
+ */
280
+ _rejectPendingCommand(messageId, error) {
281
+ const pending = this.result_futures[messageId];
282
+ if (pending) {
283
+ if (pending.timeoutId) {
284
+ clearTimeout(pending.timeoutId);
285
+ }
286
+ delete this.result_futures[messageId];
287
+ pending.reject(error);
288
+ }
289
+ }
290
+ /**
291
+ * Reject all pending commands with a ConnectionClosedError.
292
+ * Called when the connection is closed or lost.
293
+ */
294
+ _rejectAllPendingCommands() {
295
+ const error = new ConnectionClosedError();
296
+ const pendingIds = Object.keys(this.result_futures);
297
+ for (const messageId of pendingIds) {
298
+ this._rejectPendingCommand(messageId, error);
299
+ }
172
300
  }
173
301
  async connect() {
174
302
  if (this.connection.connected) {
@@ -176,10 +304,14 @@ class MatterClient {
176
304
  }
177
305
  await this.connection.connect(
178
306
  (msg) => this._handleIncomingMessage(msg),
179
- () => this.fireEvent("connection_lost")
307
+ () => {
308
+ this._rejectAllPendingCommands();
309
+ this.fireEvent("connection_lost");
310
+ }
180
311
  );
181
312
  }
182
313
  disconnect(clearStorage = false) {
314
+ this._rejectAllPendingCommands();
183
315
  if (this.connection && this.connection.connected) {
184
316
  this.connection.disconnect();
185
317
  }
@@ -203,19 +335,11 @@ class MatterClient {
203
335
  return;
204
336
  }
205
337
  if ("error_code" in msg) {
206
- const promise = this.result_futures[msg.message_id];
207
- if (promise) {
208
- promise.reject(new Error(msg.details));
209
- delete this.result_futures[msg.message_id];
210
- }
338
+ this._rejectPendingCommand(msg.message_id, new Error(msg.details));
211
339
  return;
212
340
  }
213
341
  if ("result" in msg) {
214
- const promise = this.result_futures[msg.message_id];
215
- if (promise) {
216
- promise.resolve(msg.result);
217
- delete this.result_futures[msg.message_id];
218
- }
342
+ this._resolvePendingCommand(msg.message_id, msg.result);
219
343
  return;
220
344
  }
221
345
  console.warn("Received message with unknown format", msg);
@@ -281,6 +405,7 @@ class MatterClient {
281
405
  }
282
406
  }
283
407
  export {
408
+ DEFAULT_COMMAND_TIMEOUT,
284
409
  MatterClient
285
410
  };
286
411
  //# sourceMappingURL=client.js.map