@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 +90 -0
- package/dist/esm/client.d.ts +69 -26
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +226 -101
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/connection.d.ts.map +1 -1
- package/dist/esm/connection.js +3 -0
- package/dist/esm/connection.js.map +1 -1
- package/dist/esm/exceptions.d.ts +14 -0
- package/dist/esm/exceptions.d.ts.map +1 -1
- package/dist/esm/exceptions.js +16 -0
- package/dist/esm/exceptions.js.map +1 -1
- package/dist/esm/models/model.d.ts +30 -0
- package/dist/esm/models/model.d.ts.map +1 -1
- package/dist/esm/models/model.js +8 -1
- package/dist/esm/models/model.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +267 -97
- package/src/connection.ts +5 -0
- package/src/exceptions.ts +23 -0
- package/src/models/model.ts +34 -0
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<
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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(
|
|
105
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
222
|
-
return await this.sendCommand("
|
|
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
|
|
226
|
-
return await this.sendCommand("
|
|
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
|
|
230
|
-
return await this.sendCommand("
|
|
301
|
+
async fetchServerInfo(timeout?: number) {
|
|
302
|
+
return await this.sendCommand("server_info", 0, {}, timeout);
|
|
231
303
|
}
|
|
232
304
|
|
|
233
|
-
async
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
|
371
|
+
message_id: messageId,
|
|
257
372
|
command,
|
|
258
373
|
args,
|
|
259
374
|
};
|
|
260
375
|
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/models/model.ts
CHANGED
|
@@ -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
|
+
}
|