@matter-server/ws-controller 0.3.6 → 0.3.7
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/dist/esm/controller/AttributeDataCache.d.ts +5 -5
- package/dist/esm/controller/AttributeDataCache.d.ts.map +1 -1
- package/dist/esm/controller/AttributeDataCache.js +24 -56
- package/dist/esm/controller/AttributeDataCache.js.map +1 -1
- package/dist/esm/controller/ControllerCommandHandler.d.ts +9 -2
- package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -1
- package/dist/esm/controller/ControllerCommandHandler.js +21 -8
- package/dist/esm/controller/ControllerCommandHandler.js.map +1 -1
- package/dist/esm/controller/MatterController.d.ts.map +1 -1
- package/dist/esm/controller/MatterController.js.map +1 -1
- package/dist/esm/server/Converters.d.ts +3 -0
- package/dist/esm/server/Converters.d.ts.map +1 -1
- package/dist/esm/server/Converters.js +65 -27
- package/dist/esm/server/Converters.js.map +2 -2
- package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -1
- package/dist/esm/server/WebSocketControllerHandler.js +30 -28
- package/dist/esm/server/WebSocketControllerHandler.js.map +1 -1
- package/package.json +1 -1
- package/src/controller/AttributeDataCache.ts +29 -74
- package/src/controller/ControllerCommandHandler.ts +23 -10
- package/src/controller/MatterController.ts +0 -1
- package/src/server/Converters.ts +99 -36
- package/src/server/WebSocketControllerHandler.ts +53 -49
package/src/server/Converters.ts
CHANGED
|
@@ -167,9 +167,73 @@ export function convertCommandDataToMatter(
|
|
|
167
167
|
return convertWebSocketGenericToMatter(value, model, clusterModel);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Model conversion kinds, classified once per ValueModel and cached for fast dispatch.
|
|
172
|
+
*/
|
|
173
|
+
const enum ConvKind {
|
|
174
|
+
Passthrough,
|
|
175
|
+
EpochS,
|
|
176
|
+
EpochUS,
|
|
177
|
+
Bytes,
|
|
178
|
+
Bitmap,
|
|
179
|
+
Struct,
|
|
180
|
+
List,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Cached model-to-kind classification. Avoids repeated metabase property traversal. */
|
|
184
|
+
const modelKindCache = new WeakMap<ValueModel, ConvKind>();
|
|
185
|
+
|
|
186
|
+
/** Precomputed struct member info: avoids camelize() on every conversion. */
|
|
187
|
+
type StructMemberEntry = { readonly name: string; readonly id: number; readonly model: ValueModel };
|
|
188
|
+
const structMemberCache = new WeakMap<ValueModel, StructMemberEntry[]>();
|
|
189
|
+
|
|
190
|
+
function classifyModel(model: ValueModel): ConvKind {
|
|
191
|
+
let kind = modelKindCache.get(model);
|
|
192
|
+
if (kind !== undefined) return kind;
|
|
193
|
+
|
|
194
|
+
if (model.type === "list") {
|
|
195
|
+
kind = ConvKind.List;
|
|
196
|
+
} else if (model.metabase?.name === "struct") {
|
|
197
|
+
kind = ConvKind.Struct;
|
|
198
|
+
} else if (model.metabase?.metatype === "bitmap") {
|
|
199
|
+
kind = ConvKind.Bitmap;
|
|
200
|
+
} else if (model.metabase?.metatype === "bytes") {
|
|
201
|
+
kind = ConvKind.Bytes;
|
|
202
|
+
} else if (model.metabase?.metatype === "integer") {
|
|
203
|
+
kind =
|
|
204
|
+
model.type === "epoch-s"
|
|
205
|
+
? ConvKind.EpochS
|
|
206
|
+
: model.type === "epoch-us"
|
|
207
|
+
? ConvKind.EpochUS
|
|
208
|
+
: ConvKind.Passthrough;
|
|
209
|
+
} else {
|
|
210
|
+
kind = ConvKind.Passthrough;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
modelKindCache.set(model, kind);
|
|
214
|
+
return kind;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getStructMembers(model: ValueModel): StructMemberEntry[] {
|
|
218
|
+
let members = structMemberCache.get(model);
|
|
219
|
+
if (members !== undefined) return members;
|
|
220
|
+
|
|
221
|
+
members = [];
|
|
222
|
+
for (const member of model.members) {
|
|
223
|
+
if (member.name !== undefined && member.id !== undefined) {
|
|
224
|
+
members.push({ name: camelize(member.name), id: member.id, model: member });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
structMemberCache.set(model, members);
|
|
228
|
+
return members;
|
|
229
|
+
}
|
|
230
|
+
|
|
170
231
|
/**
|
|
171
232
|
* Uses the matter.js Model to convert the response data for read, subscribe and invoke into a tag-based response
|
|
172
233
|
* including conversion of data types.
|
|
234
|
+
*
|
|
235
|
+
* Model classification and struct member info are cached in WeakMaps so that repeated calls
|
|
236
|
+
* for the same model (e.g. across 160 nodes with identical clusters) skip the metabase traversal.
|
|
173
237
|
*/
|
|
174
238
|
export function convertMatterToWebSocketTagBased(
|
|
175
239
|
value: unknown,
|
|
@@ -180,34 +244,50 @@ export function convertMatterToWebSocketTagBased(
|
|
|
180
244
|
return null;
|
|
181
245
|
}
|
|
182
246
|
if (model === undefined) {
|
|
183
|
-
//
|
|
247
|
+
// Unknown attributes: simple type conversions only
|
|
184
248
|
if (Bytes.isBytes(value)) {
|
|
185
|
-
return
|
|
249
|
+
return Bytes.toBase64(value);
|
|
186
250
|
}
|
|
187
251
|
if (isObject(value) || !["string", "number", "bigint", "boolean", "undefined"].includes(typeof value)) {
|
|
188
|
-
return null;
|
|
252
|
+
return null;
|
|
189
253
|
}
|
|
190
|
-
|
|
191
254
|
return value;
|
|
192
255
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
256
|
+
|
|
257
|
+
switch (classifyModel(model)) {
|
|
258
|
+
case ConvKind.Passthrough:
|
|
259
|
+
return value;
|
|
260
|
+
|
|
261
|
+
case ConvKind.EpochS:
|
|
262
|
+
return typeof value === "number" ? value - MATTER_EPOCH_OFFSET_S : value;
|
|
263
|
+
|
|
264
|
+
case ConvKind.EpochUS:
|
|
265
|
+
return typeof value === "number" || typeof value === "bigint"
|
|
266
|
+
? BigInt(value) - MATTER_EPOCH_OFFSET_US
|
|
267
|
+
: value;
|
|
268
|
+
|
|
269
|
+
case ConvKind.Bytes:
|
|
270
|
+
return value instanceof Uint8Array ? Bytes.toBase64(value) : value;
|
|
271
|
+
|
|
272
|
+
case ConvKind.List:
|
|
273
|
+
return Array.isArray(value)
|
|
274
|
+
? value.map(v => convertMatterToWebSocketTagBased(v, model.members[0], clusterModel))
|
|
275
|
+
: value;
|
|
276
|
+
|
|
277
|
+
case ConvKind.Struct: {
|
|
278
|
+
if (!isObject(value)) return value;
|
|
279
|
+
const result: { [key: string]: any } = {};
|
|
280
|
+
for (const { name, id, model: memberModel } of getStructMembers(model)) {
|
|
281
|
+
if (Object.hasOwn(value, name)) {
|
|
282
|
+
result[id] = convertMatterToWebSocketTagBased(value[name], memberModel, clusterModel);
|
|
283
|
+
}
|
|
203
284
|
}
|
|
285
|
+
return result;
|
|
204
286
|
}
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
if (isObject(value) && model.metabase?.metatype === "bitmap") {
|
|
208
|
-
if (clusterModel !== undefined) {
|
|
209
|
-
let numberValue = 0;
|
|
210
287
|
|
|
288
|
+
case ConvKind.Bitmap: {
|
|
289
|
+
if (!isObject(value) || clusterModel === undefined) return value;
|
|
290
|
+
let numberValue = 0;
|
|
211
291
|
for (const member of clusterModel.scope.membersOf(model)) {
|
|
212
292
|
const memberValue =
|
|
213
293
|
member.name !== undefined && value[camelize(member.name)]
|
|
@@ -231,26 +311,9 @@ export function convertMatterToWebSocketTagBased(
|
|
|
231
311
|
numberValue |= typeof memberValue === "boolean" ? 1 : memberValue << minBit;
|
|
232
312
|
}
|
|
233
313
|
}
|
|
234
|
-
|
|
235
314
|
return numberValue;
|
|
236
315
|
}
|
|
237
316
|
}
|
|
238
|
-
|
|
239
|
-
if (value instanceof Uint8Array && model.metabase?.metatype === "bytes") {
|
|
240
|
-
value = `${Bytes.toBase64(value)}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (model.metabase?.metatype === "integer") {
|
|
244
|
-
// Convert Epoch timestamps to Unix timestamps we use internally
|
|
245
|
-
if (model.type === "epoch-s" && typeof value === "number") {
|
|
246
|
-
value -= MATTER_EPOCH_OFFSET_S;
|
|
247
|
-
} else if (model.type === "epoch-us" && (typeof value === "number" || typeof value === "bigint")) {
|
|
248
|
-
value = BigInt(value) - MATTER_EPOCH_OFFSET_US;
|
|
249
|
-
}
|
|
250
|
-
return value;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return value;
|
|
254
317
|
}
|
|
255
318
|
|
|
256
319
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { camelize, ClusterId, FabricIndex, Logger, LogLevel, Millis, NodeId, ObserverGroup } from "@matter/main";
|
|
8
8
|
import { ControllerCommissioningFlowOptions } from "@matter/main/protocol";
|
|
9
|
-
import { EndpointNumber,
|
|
9
|
+
import { EndpointNumber, QrPairingCodeCodec } from "@matter/main/types";
|
|
10
10
|
import { NodeStates } from "@project-chip/matter.js/device";
|
|
11
11
|
import { WebSocketServer } from "ws";
|
|
12
12
|
import { ControllerCommandHandler } from "../controller/ControllerCommandHandler.js";
|
|
@@ -138,19 +138,19 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
138
138
|
|
|
139
139
|
switch (eventName) {
|
|
140
140
|
case "node_added":
|
|
141
|
-
case "node_updated":
|
|
142
|
-
|
|
143
|
-
nodeDetails
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
141
|
+
case "node_updated": {
|
|
142
|
+
try {
|
|
143
|
+
const nodeDetails = this.#collectNodeDetails(nodeId);
|
|
144
|
+
logger.debug(`[${connId}] Sending ${eventName} event for Node ${formatNodeId(nodeId)}`);
|
|
145
|
+
ws.send(toBigIntAwareJson({ event: eventName, data: nodeDetails }));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
logger.error(
|
|
148
|
+
`[${connId}] Failed to collect node details for Node ${formatNodeId(nodeId)}`,
|
|
149
|
+
err,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
153
152
|
break;
|
|
153
|
+
}
|
|
154
154
|
case "node_removed":
|
|
155
155
|
logger.debug(`[${connId}] Sending node_removed event for Node ${formatNodeId(nodeId)}`);
|
|
156
156
|
ws.send(toBigIntAwareJson({ event: eventName, data: nodeId }));
|
|
@@ -163,8 +163,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
163
163
|
if (this.#closed) return;
|
|
164
164
|
const { endpointId, clusterId, attributeId } = data.path;
|
|
165
165
|
const pathStr = `${endpointId}/${clusterId}/${attributeId}`;
|
|
166
|
-
const
|
|
167
|
-
const clusterData = ClusterMap[cluster.name.toLowerCase()];
|
|
166
|
+
const clusterData = ClusterMap[clusterId];
|
|
168
167
|
const value = convertMatterToWebSocketTagBased(
|
|
169
168
|
data.value,
|
|
170
169
|
clusterData?.attributes[attributeId],
|
|
@@ -321,7 +320,9 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
321
320
|
);
|
|
322
321
|
});
|
|
323
322
|
|
|
324
|
-
|
|
323
|
+
// Initialize all nodes (populates attribute caches) and start connecting them.
|
|
324
|
+
// Guarded internally so it runs exactly once even with multiple listen addresses.
|
|
325
|
+
await this.#commandHandler.initializeNodes();
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
unregister(): Promise<void> {
|
|
@@ -372,7 +373,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
372
373
|
let enableListeners: boolean | undefined = undefined;
|
|
373
374
|
switch (command) {
|
|
374
375
|
case "start_listening":
|
|
375
|
-
result =
|
|
376
|
+
result = this.#handleStartListening(args);
|
|
376
377
|
enableListeners = true;
|
|
377
378
|
break;
|
|
378
379
|
case "set_default_fabric_label":
|
|
@@ -388,7 +389,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
388
389
|
result = await this.#handleGetNode(args);
|
|
389
390
|
break;
|
|
390
391
|
case "get_nodes":
|
|
391
|
-
result =
|
|
392
|
+
result = this.#handleGetNodes(args);
|
|
392
393
|
break;
|
|
393
394
|
case "get_node_ip_addresses":
|
|
394
395
|
result = await this.#handleGetNodeIpAddresses(args);
|
|
@@ -414,7 +415,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
414
415
|
case "diagnostics":
|
|
415
416
|
result = {
|
|
416
417
|
info: await this.#getServerInfo(),
|
|
417
|
-
nodes:
|
|
418
|
+
nodes: this.#handleGetNodes(args),
|
|
418
419
|
events: this.getEventHistory(),
|
|
419
420
|
};
|
|
420
421
|
break;
|
|
@@ -543,8 +544,11 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
543
544
|
this.#broadcastEvent("server_info_updated", serverInfo);
|
|
544
545
|
}
|
|
545
546
|
|
|
546
|
-
|
|
547
|
-
|
|
547
|
+
#handleStartListening(_args: ArgsOf<"start_listening">): ResponseOf<"start_listening"> {
|
|
548
|
+
logger.info("WebSocket server start_listening");
|
|
549
|
+
const data = this.#handleGetNodes({});
|
|
550
|
+
logger.info("WebSocket server start_listening. Returned", data.length, "nodes");
|
|
551
|
+
return data;
|
|
548
552
|
}
|
|
549
553
|
|
|
550
554
|
async #handleSetDefaultFabricLabel(
|
|
@@ -592,7 +596,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
592
596
|
threadCredentials,
|
|
593
597
|
});
|
|
594
598
|
|
|
595
|
-
return
|
|
599
|
+
return this.#collectNodeDetails(nodeId);
|
|
596
600
|
}
|
|
597
601
|
|
|
598
602
|
async #handleCommissionOnNetwork(
|
|
@@ -640,15 +644,15 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
640
644
|
|
|
641
645
|
const { nodeId } = await this.#commandHandler.commissionNode(commissionRequest);
|
|
642
646
|
|
|
643
|
-
return
|
|
647
|
+
return this.#collectNodeDetails(nodeId);
|
|
644
648
|
}
|
|
645
649
|
|
|
646
|
-
|
|
650
|
+
#handleGetNodes(args: ArgsOf<"get_nodes">): ResponseOf<"get_nodes"> {
|
|
647
651
|
const { only_available = false } = args ?? {};
|
|
648
652
|
const nodeDetails = new Array<MatterNode>();
|
|
649
653
|
// Include real nodes
|
|
650
654
|
for (const node of this.#commandHandler.getNodeIds()) {
|
|
651
|
-
const details =
|
|
655
|
+
const details = this.#collectNodeDetails(node);
|
|
652
656
|
if (!only_available || details.available) {
|
|
653
657
|
nodeDetails.push(details);
|
|
654
658
|
}
|
|
@@ -673,9 +677,9 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
673
677
|
|
|
674
678
|
// Pass the last interview date for real nodes
|
|
675
679
|
if (handler === this.#commandHandler) {
|
|
676
|
-
return
|
|
680
|
+
return this.#commandHandler.getNodeDetails(nodeId, this.#lastInterviewDates.get(nodeId));
|
|
677
681
|
}
|
|
678
|
-
return
|
|
682
|
+
return handler.getNodeDetails(nodeId);
|
|
679
683
|
}
|
|
680
684
|
|
|
681
685
|
async #handleGetNodeIpAddresses(
|
|
@@ -899,24 +903,25 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
899
903
|
addresses,
|
|
900
904
|
mrpSessionActiveInterval,
|
|
901
905
|
mrpSessionIdleInterval,
|
|
902
|
-
}) =>
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
906
|
+
}) =>
|
|
907
|
+
({
|
|
908
|
+
instance_name: instanceName,
|
|
909
|
+
host_name: hostName, // TODO
|
|
910
|
+
port,
|
|
911
|
+
long_discriminator: longDiscriminator,
|
|
912
|
+
vendor_id: vendorId,
|
|
913
|
+
product_id: productId,
|
|
914
|
+
commissioning_mode: commissioningMode,
|
|
915
|
+
device_type: deviceType,
|
|
916
|
+
device_name: deviceName,
|
|
917
|
+
pairing_instruction: pairingInstruction,
|
|
918
|
+
pairing_hint: pairingHint,
|
|
919
|
+
mrp_retry_interval_idle: mrpSessionIdleInterval,
|
|
920
|
+
mrp_retry_interval_active: mrpSessionActiveInterval,
|
|
921
|
+
supports_tcp: supportsTcpServer,
|
|
922
|
+
addresses,
|
|
923
|
+
rotating_id: rotatingId,
|
|
924
|
+
}),
|
|
920
925
|
);
|
|
921
926
|
}
|
|
922
927
|
|
|
@@ -968,9 +973,9 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
968
973
|
return await this.#commandHandler.updateNode(NodeId(node_id), targetVersion);
|
|
969
974
|
}
|
|
970
975
|
|
|
971
|
-
|
|
976
|
+
#collectNodeDetails(nodeId: NodeId): MatterNode {
|
|
972
977
|
const lastInterviewDate = this.#lastInterviewDates.get(nodeId);
|
|
973
|
-
return
|
|
978
|
+
return this.#commandHandler.getNodeDetails(nodeId, lastInterviewDate);
|
|
974
979
|
}
|
|
975
980
|
|
|
976
981
|
#convertCommandDataToWebSocketTagBased(
|
|
@@ -980,8 +985,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
980
985
|
clusterData?: ClusterMapEntry,
|
|
981
986
|
) {
|
|
982
987
|
if (!clusterData) {
|
|
983
|
-
|
|
984
|
-
clusterData = clusterData ?? ClusterMap[cluster.name.toLowerCase()];
|
|
988
|
+
clusterData = ClusterMap[clusterId];
|
|
985
989
|
}
|
|
986
990
|
|
|
987
991
|
if (clusterData === undefined || clusterData.commands[commandName.toLowerCase()] === undefined) {
|