@matter-server/ws-controller 0.2.0-alpha.0-00000000-000000000
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/LICENSE +201 -0
- package/README.md +11 -0
- package/dist/esm/controller/AttributeDataCache.d.ts +49 -0
- package/dist/esm/controller/AttributeDataCache.d.ts.map +1 -0
- package/dist/esm/controller/AttributeDataCache.js +154 -0
- package/dist/esm/controller/AttributeDataCache.js.map +6 -0
- package/dist/esm/controller/ControllerCommandHandler.d.ts +118 -0
- package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -0
- package/dist/esm/controller/ControllerCommandHandler.js +1015 -0
- package/dist/esm/controller/ControllerCommandHandler.js.map +6 -0
- package/dist/esm/controller/LegacyDataInjector.d.ts +95 -0
- package/dist/esm/controller/LegacyDataInjector.d.ts.map +1 -0
- package/dist/esm/controller/LegacyDataInjector.js +196 -0
- package/dist/esm/controller/LegacyDataInjector.js.map +6 -0
- package/dist/esm/controller/MatterController.d.ts +59 -0
- package/dist/esm/controller/MatterController.d.ts.map +1 -0
- package/dist/esm/controller/MatterController.js +212 -0
- package/dist/esm/controller/MatterController.js.map +6 -0
- package/dist/esm/controller/Nodes.d.ts +62 -0
- package/dist/esm/controller/Nodes.d.ts.map +1 -0
- package/dist/esm/controller/Nodes.js +85 -0
- package/dist/esm/controller/Nodes.js.map +6 -0
- package/dist/esm/controller/TestNodeCommandHandler.d.ts +84 -0
- package/dist/esm/controller/TestNodeCommandHandler.d.ts.map +1 -0
- package/dist/esm/controller/TestNodeCommandHandler.js +225 -0
- package/dist/esm/controller/TestNodeCommandHandler.js.map +6 -0
- package/dist/esm/data/VendorIDs.d.ts +7 -0
- package/dist/esm/data/VendorIDs.d.ts.map +1 -0
- package/dist/esm/data/VendorIDs.js +1237 -0
- package/dist/esm/data/VendorIDs.js.map +6 -0
- package/dist/esm/example/send-command.d.ts +7 -0
- package/dist/esm/example/send-command.d.ts.map +1 -0
- package/dist/esm/example/send-command.js +60 -0
- package/dist/esm/example/send-command.js.map +6 -0
- package/dist/esm/index.d.ts +21 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +6 -0
- package/dist/esm/model/ModelMapper.d.ts +34 -0
- package/dist/esm/model/ModelMapper.d.ts.map +1 -0
- package/dist/esm/model/ModelMapper.js +62 -0
- package/dist/esm/model/ModelMapper.js.map +6 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/server/ConfigStorage.d.ts +29 -0
- package/dist/esm/server/ConfigStorage.d.ts.map +1 -0
- package/dist/esm/server/ConfigStorage.js +84 -0
- package/dist/esm/server/ConfigStorage.js.map +6 -0
- package/dist/esm/server/Converters.d.ts +53 -0
- package/dist/esm/server/Converters.d.ts.map +1 -0
- package/dist/esm/server/Converters.js +343 -0
- package/dist/esm/server/Converters.js.map +6 -0
- package/dist/esm/server/WebSocketControllerHandler.d.ts +21 -0
- package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -0
- package/dist/esm/server/WebSocketControllerHandler.js +767 -0
- package/dist/esm/server/WebSocketControllerHandler.js.map +6 -0
- package/dist/esm/types/CommandHandler.d.ts +258 -0
- package/dist/esm/types/CommandHandler.d.ts.map +1 -0
- package/dist/esm/types/CommandHandler.js +6 -0
- package/dist/esm/types/CommandHandler.js.map +6 -0
- package/dist/esm/types/WebServer.d.ts +12 -0
- package/dist/esm/types/WebServer.d.ts.map +1 -0
- package/dist/esm/types/WebServer.js +6 -0
- package/dist/esm/types/WebServer.js.map +6 -0
- package/dist/esm/types/WebSocketMessageTypes.d.ts +478 -0
- package/dist/esm/types/WebSocketMessageTypes.d.ts.map +1 -0
- package/dist/esm/types/WebSocketMessageTypes.js +77 -0
- package/dist/esm/types/WebSocketMessageTypes.js.map +6 -0
- package/dist/esm/util/matterVersion.d.ts +12 -0
- package/dist/esm/util/matterVersion.d.ts.map +1 -0
- package/dist/esm/util/matterVersion.js +32 -0
- package/dist/esm/util/matterVersion.js.map +6 -0
- package/dist/esm/util/network.d.ts +14 -0
- package/dist/esm/util/network.d.ts.map +1 -0
- package/dist/esm/util/network.js +63 -0
- package/dist/esm/util/network.js.map +6 -0
- package/package.json +45 -0
- package/src/controller/AttributeDataCache.ts +194 -0
- package/src/controller/ControllerCommandHandler.ts +1256 -0
- package/src/controller/LegacyDataInjector.ts +314 -0
- package/src/controller/MatterController.ts +265 -0
- package/src/controller/Nodes.ts +115 -0
- package/src/controller/TestNodeCommandHandler.ts +305 -0
- package/src/data/VendorIDs.ts +1234 -0
- package/src/example/send-command.ts +82 -0
- package/src/index.ts +33 -0
- package/src/model/ModelMapper.ts +87 -0
- package/src/server/ConfigStorage.ts +112 -0
- package/src/server/Converters.ts +483 -0
- package/src/server/WebSocketControllerHandler.ts +917 -0
- package/src/tsconfig.json +7 -0
- package/src/types/CommandHandler.ts +270 -0
- package/src/types/WebServer.ts +14 -0
- package/src/types/WebSocketMessageTypes.ts +525 -0
- package/src/util/matterVersion.ts +45 -0
- package/src/util/network.ts +85 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { ObserverGroup } from "@matter/general";
|
|
7
|
+
import { camelize, ClusterId, FabricIndex, Logger, Millis, NodeId } from "@matter/main";
|
|
8
|
+
import { EndpointNumber, getClusterById, QrPairingCodeCodec } from "@matter/main/types";
|
|
9
|
+
import { NodeStates } from "@project-chip/matter.js/device";
|
|
10
|
+
import { WebSocketServer } from "ws";
|
|
11
|
+
import { TestNodeCommandHandler } from "../controller/TestNodeCommandHandler.js";
|
|
12
|
+
import { VendorIds } from "../data/VendorIDs.js";
|
|
13
|
+
import { ClusterMap } from "../model/ModelMapper.js";
|
|
14
|
+
import {
|
|
15
|
+
ServerError,
|
|
16
|
+
ServerErrorCode
|
|
17
|
+
} from "../types/WebSocketMessageTypes.js";
|
|
18
|
+
import { MATTER_VERSION } from "../util/matterVersion.js";
|
|
19
|
+
import {
|
|
20
|
+
convertMatterToWebSocketTagBased,
|
|
21
|
+
parseBigIntAwareJson,
|
|
22
|
+
splitAttributePath,
|
|
23
|
+
toBigIntAwareJson
|
|
24
|
+
} from "./Converters.js";
|
|
25
|
+
const logger = Logger.get("WebSocketControllerHandler");
|
|
26
|
+
const EVENT_HISTORY_SIZE = 25;
|
|
27
|
+
const SCHEMA_VERSION = 11;
|
|
28
|
+
class WebSocketControllerHandler {
|
|
29
|
+
#controller;
|
|
30
|
+
#commandHandler;
|
|
31
|
+
#testNodeHandler;
|
|
32
|
+
#config;
|
|
33
|
+
#wss;
|
|
34
|
+
#closed = false;
|
|
35
|
+
/** Circular buffer for recent node events (max 25) */
|
|
36
|
+
#eventHistory = [];
|
|
37
|
+
/** Track when each node was last interviewed (connected) - keyed by nodeId */
|
|
38
|
+
#lastInterviewDates = /* @__PURE__ */ new Map();
|
|
39
|
+
constructor(controller, config) {
|
|
40
|
+
this.#controller = controller;
|
|
41
|
+
this.#commandHandler = controller.commandHandler;
|
|
42
|
+
this.#testNodeHandler = new TestNodeCommandHandler();
|
|
43
|
+
this.#config = config;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get the appropriate command handler for a node ID.
|
|
47
|
+
* Returns TestNodeCommandHandler for test nodes, ControllerCommandHandler for real nodes.
|
|
48
|
+
*/
|
|
49
|
+
#handlerFor(nodeId) {
|
|
50
|
+
return TestNodeCommandHandler.isTestNodeId(nodeId) ? this.#testNodeHandler : this.#commandHandler;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Add an event to the history buffer, maintaining max size.
|
|
54
|
+
*/
|
|
55
|
+
#addEventToHistory(event) {
|
|
56
|
+
this.#eventHistory.push(event);
|
|
57
|
+
if (this.#eventHistory.length > EVENT_HISTORY_SIZE) {
|
|
58
|
+
this.#eventHistory.shift();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the event history (last 25 events).
|
|
63
|
+
*/
|
|
64
|
+
getEventHistory() {
|
|
65
|
+
return [...this.#eventHistory];
|
|
66
|
+
}
|
|
67
|
+
async register(server) {
|
|
68
|
+
const wss = this.#wss = new WebSocketServer({ server, path: "/ws" });
|
|
69
|
+
wss.on("connection", (ws) => {
|
|
70
|
+
if (this.#closed) return;
|
|
71
|
+
let listening = false;
|
|
72
|
+
const observers = new ObserverGroup();
|
|
73
|
+
const sendNodeDetailsEvent = (eventName, nodeId) => {
|
|
74
|
+
if (this.#closed || !listening) return;
|
|
75
|
+
switch (eventName) {
|
|
76
|
+
case "node_added":
|
|
77
|
+
case "node_updated":
|
|
78
|
+
this.#collectNodeDetails(nodeId).then(
|
|
79
|
+
(nodeDetails) => {
|
|
80
|
+
logger.debug(`Sending ${eventName} event for Node ${nodeId}`, nodeDetails);
|
|
81
|
+
ws.send(toBigIntAwareJson({ event: eventName, data: nodeDetails }));
|
|
82
|
+
},
|
|
83
|
+
(err) => logger.error(`Failed to collect node details for Node ${nodeId}`, err)
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
case "node_removed":
|
|
87
|
+
logger.debug(`Sending node_removed event for Node ${nodeId}`);
|
|
88
|
+
ws.send(toBigIntAwareJson({ event: eventName, data: nodeId }));
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
observers.on(this.#commandHandler.events.attributeChanged, (nodeId, data) => {
|
|
93
|
+
if (this.#closed) return;
|
|
94
|
+
const { endpointId, clusterId, attributeId } = data.path;
|
|
95
|
+
const pathStr = `${endpointId}/${clusterId}/${attributeId}`;
|
|
96
|
+
const cluster = getClusterById(clusterId);
|
|
97
|
+
const clusterData = ClusterMap[cluster.name.toLowerCase()];
|
|
98
|
+
const value = convertMatterToWebSocketTagBased(
|
|
99
|
+
data.value,
|
|
100
|
+
clusterData?.attributes[attributeId],
|
|
101
|
+
clusterData?.model
|
|
102
|
+
);
|
|
103
|
+
logger.debug(`Sending attribute_updated event for Node ${nodeId}`, pathStr, value);
|
|
104
|
+
ws.send(toBigIntAwareJson({ event: "attribute_updated", data: [nodeId, pathStr, value] }));
|
|
105
|
+
});
|
|
106
|
+
observers.on(this.#commandHandler.events.eventChanged, (nodeId, data) => {
|
|
107
|
+
if (this.#closed || !listening) return;
|
|
108
|
+
const { path, events } = data;
|
|
109
|
+
const { endpointId, clusterId, eventId } = path;
|
|
110
|
+
for (const event of events) {
|
|
111
|
+
let timestamp;
|
|
112
|
+
let timestampType;
|
|
113
|
+
if (event.epochTimestamp !== void 0) {
|
|
114
|
+
timestamp = event.epochTimestamp;
|
|
115
|
+
timestampType = 1;
|
|
116
|
+
} else if (event.systemTimestamp !== void 0) {
|
|
117
|
+
timestamp = event.systemTimestamp;
|
|
118
|
+
timestampType = 0;
|
|
119
|
+
} else {
|
|
120
|
+
timestamp = Date.now();
|
|
121
|
+
timestampType = 2;
|
|
122
|
+
}
|
|
123
|
+
const nodeEvent = {
|
|
124
|
+
node_id: nodeId,
|
|
125
|
+
endpoint_id: endpointId,
|
|
126
|
+
cluster_id: clusterId,
|
|
127
|
+
event_id: eventId,
|
|
128
|
+
event_number: event.eventNumber,
|
|
129
|
+
priority: event.priority,
|
|
130
|
+
timestamp,
|
|
131
|
+
timestamp_type: timestampType,
|
|
132
|
+
data: event.data ?? null
|
|
133
|
+
};
|
|
134
|
+
this.#addEventToHistory(nodeEvent);
|
|
135
|
+
logger.debug(`Sending node_event for Node ${nodeId}`, nodeEvent);
|
|
136
|
+
ws.send(toBigIntAwareJson({ event: "node_event", data: nodeEvent }));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
observers.on(this.#commandHandler.events.nodeAdded, (nodeId) => {
|
|
140
|
+
sendNodeDetailsEvent("node_added", nodeId);
|
|
141
|
+
});
|
|
142
|
+
observers.on(this.#commandHandler.events.nodeStateChanged, (nodeId, state) => {
|
|
143
|
+
if (state === NodeStates.Disconnected) return;
|
|
144
|
+
if (state === NodeStates.Connected) {
|
|
145
|
+
this.#lastInterviewDates.set(nodeId, /* @__PURE__ */ new Date());
|
|
146
|
+
}
|
|
147
|
+
sendNodeDetailsEvent("node_updated", nodeId);
|
|
148
|
+
});
|
|
149
|
+
observers.on(this.#commandHandler.events.nodeStructureChanged, (nodeId) => {
|
|
150
|
+
sendNodeDetailsEvent("node_updated", nodeId);
|
|
151
|
+
});
|
|
152
|
+
observers.on(this.#commandHandler.events.nodeDecommissioned, (nodeId) => {
|
|
153
|
+
sendNodeDetailsEvent("node_removed", nodeId);
|
|
154
|
+
});
|
|
155
|
+
observers.on(this.#commandHandler.events.nodeEndpointAdded, (nodeId, endpointId) => {
|
|
156
|
+
if (this.#closed || !listening) return;
|
|
157
|
+
logger.info(`Sending endpoint_added event for Node ${nodeId} endpoint ${endpointId}`);
|
|
158
|
+
ws.send(
|
|
159
|
+
toBigIntAwareJson({ event: "endpoint_added", data: { node_id: nodeId, endpoint_id: endpointId } })
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
observers.on(this.#commandHandler.events.nodeEndpointRemoved, (nodeId, endpointId) => {
|
|
163
|
+
if (this.#closed || !listening) return;
|
|
164
|
+
logger.info(`Sending endpoint_removed event for Node ${nodeId} endpoint ${endpointId}`);
|
|
165
|
+
ws.send(
|
|
166
|
+
toBigIntAwareJson({
|
|
167
|
+
event: "endpoint_removed",
|
|
168
|
+
data: { node_id: nodeId, endpoint_id: endpointId }
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
observers.on(this.#testNodeHandler.nodeAdded, (_nodeId, testNode) => {
|
|
173
|
+
if (this.#closed || !listening) return;
|
|
174
|
+
logger.info(`Sending node_added event for test node ${testNode.node_id}`);
|
|
175
|
+
ws.send(toBigIntAwareJson({ event: "node_added", data: testNode }));
|
|
176
|
+
});
|
|
177
|
+
observers.on(this.#testNodeHandler.nodeRemoved, (nodeId) => {
|
|
178
|
+
if (this.#closed || !listening) return;
|
|
179
|
+
logger.info(`Sending node_removed event for test node ${nodeId}`);
|
|
180
|
+
ws.send(toBigIntAwareJson({ event: "node_removed", data: nodeId }));
|
|
181
|
+
});
|
|
182
|
+
const onClose = () => observers.close();
|
|
183
|
+
ws.on(
|
|
184
|
+
"message",
|
|
185
|
+
(data) => void this.#handleWebSocketRequest(data.toString()).then(
|
|
186
|
+
({ response, enableListeners }) => {
|
|
187
|
+
if (this.#closed) return;
|
|
188
|
+
if (enableListeners) {
|
|
189
|
+
listening = true;
|
|
190
|
+
}
|
|
191
|
+
const responseStr = toBigIntAwareJson(response);
|
|
192
|
+
logger.debug("Sending WebSocket response", responseStr);
|
|
193
|
+
ws.send(toBigIntAwareJson(response));
|
|
194
|
+
},
|
|
195
|
+
(err) => logger.error("Websocket request error", err)
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
ws.on("close", onClose);
|
|
199
|
+
ws.on("error", (err) => {
|
|
200
|
+
logger.error("Websocket error", err);
|
|
201
|
+
onClose();
|
|
202
|
+
});
|
|
203
|
+
this.#getServerInfo().then(
|
|
204
|
+
(response) => ws.send(toBigIntAwareJson(response)),
|
|
205
|
+
(err) => logger.error("Websocket handshake error", err)
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
await this.#commandHandler.connect();
|
|
209
|
+
}
|
|
210
|
+
unregister() {
|
|
211
|
+
if (!this.#wss || this.#closed) {
|
|
212
|
+
return Promise.resolve();
|
|
213
|
+
}
|
|
214
|
+
this.#closed = true;
|
|
215
|
+
const shutdownMessage = toBigIntAwareJson({ event: "server_shutdown", data: {} });
|
|
216
|
+
this.#wss.clients.forEach((client) => {
|
|
217
|
+
if (client.readyState === 1) {
|
|
218
|
+
try {
|
|
219
|
+
client.send(shutdownMessage, () => {
|
|
220
|
+
client.close();
|
|
221
|
+
});
|
|
222
|
+
} catch (err) {
|
|
223
|
+
logger.warn("Failed to send server_shutdown event to client", err);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
console.log("send close to clients");
|
|
228
|
+
const wss = this.#wss;
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
wss.close((err) => {
|
|
231
|
+
if (err) {
|
|
232
|
+
reject(err);
|
|
233
|
+
} else {
|
|
234
|
+
resolve();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async #handleWebSocketRequest(data) {
|
|
240
|
+
let messageId;
|
|
241
|
+
try {
|
|
242
|
+
logger.debug("Received WebSocket request", data);
|
|
243
|
+
const request = parseBigIntAwareJson(data);
|
|
244
|
+
const { command, args } = request;
|
|
245
|
+
messageId = request.message_id;
|
|
246
|
+
let result;
|
|
247
|
+
let enableListeners = void 0;
|
|
248
|
+
switch (command) {
|
|
249
|
+
case "start_listening":
|
|
250
|
+
result = await this.#handleStartListening(args);
|
|
251
|
+
enableListeners = true;
|
|
252
|
+
break;
|
|
253
|
+
case "set_default_fabric_label":
|
|
254
|
+
result = await this.#handleSetDefaultFabricLabel(args);
|
|
255
|
+
break;
|
|
256
|
+
case "commission_with_code":
|
|
257
|
+
result = await this.#handleCommissionWithCode(args);
|
|
258
|
+
break;
|
|
259
|
+
case "commission_on_network":
|
|
260
|
+
result = await this.#handleCommissionOnNetwork(args);
|
|
261
|
+
break;
|
|
262
|
+
case "get_node":
|
|
263
|
+
result = await this.#handleGetNode(args);
|
|
264
|
+
break;
|
|
265
|
+
case "get_nodes":
|
|
266
|
+
result = await this.#handleGetNodes(args);
|
|
267
|
+
break;
|
|
268
|
+
case "get_node_ip_addresses":
|
|
269
|
+
result = await this.#handleGetNodeIpAddresses(args);
|
|
270
|
+
break;
|
|
271
|
+
case "read_attribute":
|
|
272
|
+
result = await this.#handleReadAttribute(args);
|
|
273
|
+
break;
|
|
274
|
+
case "get_vendor_names":
|
|
275
|
+
result = await this.#handleGetVendorNames(args);
|
|
276
|
+
break;
|
|
277
|
+
case "device_command":
|
|
278
|
+
result = await this.#handleDeviceCommand(args);
|
|
279
|
+
break;
|
|
280
|
+
case "write_attribute":
|
|
281
|
+
result = await this.#handleWriteAttribute(args);
|
|
282
|
+
break;
|
|
283
|
+
case "interview_node":
|
|
284
|
+
result = await this.#handleInterviewNode(args);
|
|
285
|
+
break;
|
|
286
|
+
case "ping_node":
|
|
287
|
+
result = await this.#handlePingNode(args);
|
|
288
|
+
break;
|
|
289
|
+
case "diagnostics":
|
|
290
|
+
result = {
|
|
291
|
+
info: await this.#getServerInfo(),
|
|
292
|
+
nodes: await this.#handleGetNodes(args),
|
|
293
|
+
events: this.getEventHistory()
|
|
294
|
+
};
|
|
295
|
+
break;
|
|
296
|
+
case "remove_node":
|
|
297
|
+
result = await this.#handleRemoveNode(args);
|
|
298
|
+
break;
|
|
299
|
+
case "set_wifi_credentials":
|
|
300
|
+
result = await this.#handleSetWifiCredentials(args);
|
|
301
|
+
break;
|
|
302
|
+
case "set_thread_dataset":
|
|
303
|
+
result = await this.#handleSetThreadDataset(args);
|
|
304
|
+
break;
|
|
305
|
+
case "open_commissioning_window":
|
|
306
|
+
result = await this.#handleOpenCommissioningWindow(args);
|
|
307
|
+
break;
|
|
308
|
+
case "discover_commissionable_nodes":
|
|
309
|
+
result = await this.#handleDiscoverCommissionableNodes(args);
|
|
310
|
+
break;
|
|
311
|
+
case "get_matter_fabrics":
|
|
312
|
+
result = await this.#handleGetMatterFabrics(args);
|
|
313
|
+
break;
|
|
314
|
+
case "remove_matter_fabric":
|
|
315
|
+
result = await this.#handleRemoveMatterFabric(args);
|
|
316
|
+
break;
|
|
317
|
+
case "set_acl_entry":
|
|
318
|
+
result = await this.#handleSetAclEntry(args);
|
|
319
|
+
break;
|
|
320
|
+
case "set_node_binding":
|
|
321
|
+
result = await this.#handleSetNodeBinding(args);
|
|
322
|
+
break;
|
|
323
|
+
case "import_test_node":
|
|
324
|
+
result = await this.#handleImportTestNode(args);
|
|
325
|
+
break;
|
|
326
|
+
case "check_node_update":
|
|
327
|
+
result = await this.#handleCheckNodeUpdate(args);
|
|
328
|
+
break;
|
|
329
|
+
case "update_node":
|
|
330
|
+
result = await this.#handleUpdateNode(args);
|
|
331
|
+
break;
|
|
332
|
+
case "server_info":
|
|
333
|
+
result = await this.#getServerInfo();
|
|
334
|
+
break;
|
|
335
|
+
case "discover":
|
|
336
|
+
result = await this.#handleDiscoverCommissionableNodes({});
|
|
337
|
+
break;
|
|
338
|
+
default:
|
|
339
|
+
throw ServerError.invalidCommand(command);
|
|
340
|
+
}
|
|
341
|
+
if (result === void 0) {
|
|
342
|
+
throw new Error("No response");
|
|
343
|
+
}
|
|
344
|
+
logger.info("WebSocket request handled", messageId, result);
|
|
345
|
+
return {
|
|
346
|
+
response: {
|
|
347
|
+
message_id: messageId ?? "",
|
|
348
|
+
result
|
|
349
|
+
},
|
|
350
|
+
enableListeners
|
|
351
|
+
};
|
|
352
|
+
} catch (err) {
|
|
353
|
+
logger.error("Failed to handle websocket request", err);
|
|
354
|
+
const errorCode = err instanceof ServerError ? err.code : ServerErrorCode.UnknownError;
|
|
355
|
+
return {
|
|
356
|
+
response: {
|
|
357
|
+
message_id: messageId ?? "",
|
|
358
|
+
error_code: errorCode,
|
|
359
|
+
details: err.message
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async #getServerInfo() {
|
|
365
|
+
await this.#commandHandler.start();
|
|
366
|
+
const { fabricId: fabric_id, compressedFabricId: compressed_fabric_id } = await this.#commandHandler.getCommissionerFabricData();
|
|
367
|
+
return {
|
|
368
|
+
fabric_id,
|
|
369
|
+
compressed_fabric_id,
|
|
370
|
+
schema_version: SCHEMA_VERSION,
|
|
371
|
+
min_supported_schema_version: SCHEMA_VERSION,
|
|
372
|
+
sdk_version: `matter.js/${MATTER_VERSION}`,
|
|
373
|
+
wifi_credentials_set: !!(this.#config.wifiSsid && this.#config.wifiCredentials),
|
|
374
|
+
thread_credentials_set: !!this.#config.threadDataset,
|
|
375
|
+
bluetooth_enabled: this.#commandHandler.bleEnabled
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Broadcast an event to all connected WebSocket clients.
|
|
380
|
+
*/
|
|
381
|
+
#broadcastEvent(event, data) {
|
|
382
|
+
if (!this.#wss || this.#closed) return;
|
|
383
|
+
const message = toBigIntAwareJson({ event, data });
|
|
384
|
+
this.#wss.clients.forEach((client) => {
|
|
385
|
+
if (client.readyState === 1) {
|
|
386
|
+
try {
|
|
387
|
+
client.send(message);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
logger.warn(`Failed to broadcast ${event} event to client`, err);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Send server_info_updated event to all connected clients.
|
|
396
|
+
*/
|
|
397
|
+
async #broadcastServerInfoUpdated() {
|
|
398
|
+
const serverInfo = await this.#getServerInfo();
|
|
399
|
+
logger.info("Broadcasting server_info_updated event", serverInfo);
|
|
400
|
+
this.#broadcastEvent("server_info_updated", serverInfo);
|
|
401
|
+
}
|
|
402
|
+
async #handleStartListening(_args) {
|
|
403
|
+
return await this.#handleGetNodes({});
|
|
404
|
+
}
|
|
405
|
+
async #handleSetDefaultFabricLabel(args) {
|
|
406
|
+
const { label } = args;
|
|
407
|
+
const effectiveLabel = label && label.trim() !== "" ? label.trim() : "Home";
|
|
408
|
+
await this.#commandHandler.setFabricLabel(effectiveLabel);
|
|
409
|
+
await this.#config.set({ fabricLabel: effectiveLabel });
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
async #handleCommissionWithCode(args) {
|
|
413
|
+
const { code, network_only } = args;
|
|
414
|
+
const isQrCode = code.startsWith("MT:");
|
|
415
|
+
const nextNodeId = this.#config.nextNodeId;
|
|
416
|
+
let wifiCredentials = void 0;
|
|
417
|
+
let threadCredentials = void 0;
|
|
418
|
+
if (!network_only && this.#commandHandler.bleEnabled) {
|
|
419
|
+
if (this.#config.wifiSsid && this.#config.wifiCredentials) {
|
|
420
|
+
wifiCredentials = {
|
|
421
|
+
wifiSsid: this.#config.wifiSsid,
|
|
422
|
+
wifiCredentials: this.#config.wifiCredentials
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (this.#config.threadDataset) {
|
|
426
|
+
threadCredentials = {
|
|
427
|
+
networkName: "",
|
|
428
|
+
// Thread network name is not needed when providing operational dataset
|
|
429
|
+
operationalDataset: this.#config.threadDataset
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
await this.#config.set({ nextNodeId: nextNodeId + 1 });
|
|
434
|
+
const { nodeId } = await this.#commandHandler.commissionNode({
|
|
435
|
+
nodeId: NodeId(nextNodeId),
|
|
436
|
+
onNetworkOnly: network_only,
|
|
437
|
+
...isQrCode ? { qrCode: code } : { manualCode: code },
|
|
438
|
+
wifiCredentials,
|
|
439
|
+
threadCredentials
|
|
440
|
+
});
|
|
441
|
+
return await this.#collectNodeDetails(nodeId);
|
|
442
|
+
}
|
|
443
|
+
async #handleCommissionOnNetwork(args) {
|
|
444
|
+
const { setup_pin_code, filter_type, filter, ip_addr } = args;
|
|
445
|
+
const nextNodeId = this.#config.nextNodeId;
|
|
446
|
+
let commissionRequest;
|
|
447
|
+
const baseRequest = {
|
|
448
|
+
nodeId: NodeId(nextNodeId),
|
|
449
|
+
onNetworkOnly: true,
|
|
450
|
+
// commission_on_network is always network-only
|
|
451
|
+
knownAddress: ip_addr ? { ip: ip_addr, port: 5540 } : void 0
|
|
452
|
+
};
|
|
453
|
+
switch (filter_type) {
|
|
454
|
+
case 1:
|
|
455
|
+
if (filter === void 0)
|
|
456
|
+
throw ServerError.invalidArguments("filter required for filter_type 1 (short discriminator)");
|
|
457
|
+
commissionRequest = { ...baseRequest, passcode: setup_pin_code, shortDiscriminator: filter };
|
|
458
|
+
break;
|
|
459
|
+
case 2:
|
|
460
|
+
if (filter === void 0)
|
|
461
|
+
throw ServerError.invalidArguments("filter required for filter_type 2 (long discriminator)");
|
|
462
|
+
commissionRequest = { ...baseRequest, passcode: setup_pin_code, longDiscriminator: filter };
|
|
463
|
+
break;
|
|
464
|
+
case 3:
|
|
465
|
+
if (filter === void 0)
|
|
466
|
+
throw ServerError.invalidArguments("filter required for filter_type 3 (vendor ID)");
|
|
467
|
+
commissionRequest = { ...baseRequest, passcode: setup_pin_code, vendorId: filter, productId: 0 };
|
|
468
|
+
break;
|
|
469
|
+
case 4:
|
|
470
|
+
// Device type - not directly supported, fall back to no filter
|
|
471
|
+
case 0:
|
|
472
|
+
// No filter
|
|
473
|
+
default:
|
|
474
|
+
commissionRequest = { ...baseRequest, passcode: setup_pin_code };
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
await this.#config.set({ nextNodeId: nextNodeId + 1 });
|
|
478
|
+
const { nodeId } = await this.#commandHandler.commissionNode(commissionRequest);
|
|
479
|
+
return await this.#collectNodeDetails(nodeId);
|
|
480
|
+
}
|
|
481
|
+
async #handleGetNodes(args) {
|
|
482
|
+
const { only_available = false } = args ?? {};
|
|
483
|
+
const nodeDetails = new Array();
|
|
484
|
+
for (const node of this.#commandHandler.getNodeIds()) {
|
|
485
|
+
const details = await this.#collectNodeDetails(node);
|
|
486
|
+
if (!only_available || details.available) {
|
|
487
|
+
nodeDetails.push(details);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (const testNode of this.#testNodeHandler.getNodes()) {
|
|
491
|
+
if (!only_available || testNode.available) {
|
|
492
|
+
nodeDetails.push(testNode);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return nodeDetails;
|
|
496
|
+
}
|
|
497
|
+
async #handleGetNode(args) {
|
|
498
|
+
const { node_id } = args;
|
|
499
|
+
const nodeId = NodeId(node_id);
|
|
500
|
+
const handler = this.#handlerFor(node_id);
|
|
501
|
+
if (!handler.hasNode(nodeId)) {
|
|
502
|
+
throw ServerError.nodeNotExists(node_id);
|
|
503
|
+
}
|
|
504
|
+
if (handler === this.#commandHandler) {
|
|
505
|
+
return await this.#commandHandler.getNodeDetails(nodeId, this.#lastInterviewDates.get(nodeId));
|
|
506
|
+
}
|
|
507
|
+
return await handler.getNodeDetails(nodeId);
|
|
508
|
+
}
|
|
509
|
+
async #handleGetNodeIpAddresses(args) {
|
|
510
|
+
const { node_id, prefer_cache, scoped } = args;
|
|
511
|
+
const result = await this.#handlerFor(node_id).getNodeIpAddresses(NodeId(node_id), prefer_cache);
|
|
512
|
+
if (!scoped) {
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
return result.map((ip) => ip.includes("%") ? ip.split("%")[0] : ip);
|
|
516
|
+
}
|
|
517
|
+
async #handleReadAttribute(args) {
|
|
518
|
+
const { node_id: nodeId, attribute_path, fabric_filtered = false } = args;
|
|
519
|
+
const attributePaths = Array.isArray(attribute_path) ? attribute_path : [attribute_path];
|
|
520
|
+
const result = await this.#handlerFor(nodeId).handleReadAttributes(
|
|
521
|
+
NodeId(nodeId),
|
|
522
|
+
attributePaths,
|
|
523
|
+
fabric_filtered
|
|
524
|
+
);
|
|
525
|
+
if (Object.keys(result).length === 0) {
|
|
526
|
+
throw new Error("Failed to read attribute: no values returned");
|
|
527
|
+
}
|
|
528
|
+
return result;
|
|
529
|
+
}
|
|
530
|
+
async #handleWriteAttribute(args) {
|
|
531
|
+
const { node_id: nodeId, attribute_path, value } = args;
|
|
532
|
+
const { endpointId, clusterId, attributeId } = splitAttributePath(attribute_path);
|
|
533
|
+
if (endpointId === void 0 || clusterId === void 0 || attributeId === void 0) {
|
|
534
|
+
throw ServerError.invalidArguments("write_attribute does not support wildcards in attribute path");
|
|
535
|
+
}
|
|
536
|
+
const { status } = await this.#handlerFor(nodeId).handleWriteAttribute({
|
|
537
|
+
nodeId: NodeId(nodeId),
|
|
538
|
+
endpointId,
|
|
539
|
+
clusterId,
|
|
540
|
+
attributeId,
|
|
541
|
+
value
|
|
542
|
+
});
|
|
543
|
+
return [
|
|
544
|
+
{
|
|
545
|
+
Path: { EndpointId: endpointId, ClusterId: clusterId, AttributeId: attributeId },
|
|
546
|
+
Status: status ?? 0
|
|
547
|
+
}
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
async #handleGetVendorNames(args) {
|
|
551
|
+
const { filter_vendors } = args;
|
|
552
|
+
const dclVendors = await this.#controller.getAllVendors();
|
|
553
|
+
const mergedVendors = {};
|
|
554
|
+
for (const [vendorIdStr, vendorName] of Object.entries(VendorIds)) {
|
|
555
|
+
mergedVendors[vendorIdStr] = vendorName;
|
|
556
|
+
}
|
|
557
|
+
for (const [vendorId, vendorInfo] of dclVendors) {
|
|
558
|
+
mergedVendors[vendorId] = vendorInfo.vendorName;
|
|
559
|
+
}
|
|
560
|
+
if (!filter_vendors || !filter_vendors.length) {
|
|
561
|
+
return mergedVendors;
|
|
562
|
+
}
|
|
563
|
+
const result = {};
|
|
564
|
+
for (const vendorId of filter_vendors) {
|
|
565
|
+
const vendorName = mergedVendors[vendorId];
|
|
566
|
+
if (vendorName) {
|
|
567
|
+
result[vendorId] = vendorName;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
async #handleDeviceCommand(args) {
|
|
573
|
+
const {
|
|
574
|
+
node_id: nodeId,
|
|
575
|
+
endpoint_id: endpointId,
|
|
576
|
+
cluster_id: clusterId,
|
|
577
|
+
command_name: commandName,
|
|
578
|
+
payload,
|
|
579
|
+
response_type,
|
|
580
|
+
timed_request_timeout_ms: timedInteractionTimeoutMs
|
|
581
|
+
} = args;
|
|
582
|
+
const result = await this.#handlerFor(nodeId).handleInvoke({
|
|
583
|
+
nodeId: NodeId(nodeId),
|
|
584
|
+
endpointId: EndpointNumber(endpointId),
|
|
585
|
+
clusterId: ClusterId(clusterId),
|
|
586
|
+
commandName: camelize(commandName),
|
|
587
|
+
data: payload,
|
|
588
|
+
timedInteractionTimeoutMs: typeof timedInteractionTimeoutMs === "number" ? Millis(timedInteractionTimeoutMs) : void 0
|
|
589
|
+
});
|
|
590
|
+
if (TestNodeCommandHandler.isTestNodeId(nodeId) || response_type === null) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const cmdResult = this.#convertCommandDataToWebSocketTagBased(ClusterId(clusterId), commandName, result);
|
|
594
|
+
if (cmdResult === void 0) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return cmdResult;
|
|
598
|
+
}
|
|
599
|
+
async #handleInterviewNode(args) {
|
|
600
|
+
const { node_id } = args;
|
|
601
|
+
const nodeId = NodeId(node_id);
|
|
602
|
+
if (TestNodeCommandHandler.isTestNodeId(nodeId)) {
|
|
603
|
+
const testNode = this.#testNodeHandler.getNode(nodeId);
|
|
604
|
+
if (testNode === void 0) {
|
|
605
|
+
throw ServerError.nodeNotExists(nodeId);
|
|
606
|
+
}
|
|
607
|
+
logger.debug(`interview_node called for test node ${nodeId}`);
|
|
608
|
+
this.#broadcastEvent("node_updated", testNode);
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
await this.#commandHandler.interviewNode(nodeId);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
async #handlePingNode(args) {
|
|
615
|
+
const { node_id, attempts = 1 } = args;
|
|
616
|
+
return await this.#handlerFor(node_id).pingNode(NodeId(node_id), attempts);
|
|
617
|
+
}
|
|
618
|
+
async #handleRemoveNode(args) {
|
|
619
|
+
const { node_id } = args;
|
|
620
|
+
await this.#handlerFor(node_id).removeNode(NodeId(node_id));
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
async #handleSetWifiCredentials(args) {
|
|
624
|
+
const { ssid, credentials } = args;
|
|
625
|
+
await this.#config.set({ wifiSsid: ssid, wifiCredentials: credentials });
|
|
626
|
+
try {
|
|
627
|
+
await this.#broadcastServerInfoUpdated();
|
|
628
|
+
} catch (error) {
|
|
629
|
+
logger.warn("Failed to broadcast server info update", error);
|
|
630
|
+
}
|
|
631
|
+
return {};
|
|
632
|
+
}
|
|
633
|
+
async #handleSetThreadDataset(args) {
|
|
634
|
+
const { dataset } = args;
|
|
635
|
+
await this.#config.set({ threadDataset: dataset });
|
|
636
|
+
try {
|
|
637
|
+
await this.#broadcastServerInfoUpdated();
|
|
638
|
+
} catch (error) {
|
|
639
|
+
logger.warn("Failed to broadcast server info update", error);
|
|
640
|
+
}
|
|
641
|
+
return {};
|
|
642
|
+
}
|
|
643
|
+
async #handleOpenCommissioningWindow(args) {
|
|
644
|
+
const {
|
|
645
|
+
node_id,
|
|
646
|
+
timeout
|
|
647
|
+
/*, iteration, option, discriminator*/
|
|
648
|
+
} = args;
|
|
649
|
+
const nodeId = NodeId(node_id);
|
|
650
|
+
const { manualCode, qrCode } = await this.#commandHandler.openCommissioningWindow({
|
|
651
|
+
nodeId,
|
|
652
|
+
timeout
|
|
653
|
+
});
|
|
654
|
+
const pairingCodeCodec = QrPairingCodeCodec.decode(qrCode);
|
|
655
|
+
return { setup_pin_code: pairingCodeCodec[0].passcode, setup_manual_code: manualCode, setup_qr_code: qrCode };
|
|
656
|
+
}
|
|
657
|
+
async #handleDiscoverCommissionableNodes(_args) {
|
|
658
|
+
const result = await this.#commandHandler.handleDiscovery({});
|
|
659
|
+
return result.map(
|
|
660
|
+
({
|
|
661
|
+
commissioningMode,
|
|
662
|
+
deviceName,
|
|
663
|
+
deviceType,
|
|
664
|
+
hostName,
|
|
665
|
+
instanceName,
|
|
666
|
+
longDiscriminator,
|
|
667
|
+
// numIPs,
|
|
668
|
+
pairingHint,
|
|
669
|
+
pairingInstruction,
|
|
670
|
+
port,
|
|
671
|
+
productId,
|
|
672
|
+
rotatingId,
|
|
673
|
+
// rotatingIdLen,
|
|
674
|
+
// shortDiscriminator,
|
|
675
|
+
// supportsTcpClient,
|
|
676
|
+
supportsTcpServer,
|
|
677
|
+
vendorId,
|
|
678
|
+
addresses,
|
|
679
|
+
mrpSessionActiveInterval,
|
|
680
|
+
mrpSessionIdleInterval
|
|
681
|
+
}) => ({
|
|
682
|
+
instance_name: instanceName,
|
|
683
|
+
host_name: hostName,
|
|
684
|
+
// TODO
|
|
685
|
+
port,
|
|
686
|
+
long_discriminator: longDiscriminator,
|
|
687
|
+
vendor_id: vendorId,
|
|
688
|
+
product_id: productId,
|
|
689
|
+
commissioning_mode: commissioningMode,
|
|
690
|
+
device_type: deviceType,
|
|
691
|
+
device_name: deviceName,
|
|
692
|
+
pairing_instruction: pairingInstruction,
|
|
693
|
+
pairing_hint: pairingHint,
|
|
694
|
+
mrp_retry_interval_idle: mrpSessionIdleInterval,
|
|
695
|
+
mrp_retry_interval_active: mrpSessionActiveInterval,
|
|
696
|
+
supports_tcp: supportsTcpServer,
|
|
697
|
+
addresses,
|
|
698
|
+
rotating_id: rotatingId
|
|
699
|
+
})
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
async #handleGetMatterFabrics(args) {
|
|
703
|
+
const { node_id } = args;
|
|
704
|
+
const nodeId = NodeId(node_id);
|
|
705
|
+
const fabrics = await this.#commandHandler.getFabrics(nodeId);
|
|
706
|
+
return fabrics.map(({ fabricId, vendorId, fabricIndex, label }) => ({
|
|
707
|
+
fabric_id: fabricId,
|
|
708
|
+
vendor_id: vendorId,
|
|
709
|
+
fabric_index: fabricIndex,
|
|
710
|
+
fabric_label: label,
|
|
711
|
+
vendor_name: VendorIds[vendorId]
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
async #handleRemoveMatterFabric(args) {
|
|
715
|
+
const { node_id, fabric_index } = args;
|
|
716
|
+
await this.#commandHandler.removeFabric(NodeId(node_id), FabricIndex(fabric_index));
|
|
717
|
+
return {};
|
|
718
|
+
}
|
|
719
|
+
async #handleSetAclEntry(args) {
|
|
720
|
+
const { node_id, entry } = args;
|
|
721
|
+
return await this.#commandHandler.setAclEntry(NodeId(node_id), entry);
|
|
722
|
+
}
|
|
723
|
+
async #handleSetNodeBinding(args) {
|
|
724
|
+
const { node_id, endpoint, bindings } = args;
|
|
725
|
+
return await this.#commandHandler.setNodeBinding(NodeId(node_id), EndpointNumber(endpoint), bindings);
|
|
726
|
+
}
|
|
727
|
+
async #handleImportTestNode(args) {
|
|
728
|
+
const { dump } = args;
|
|
729
|
+
this.#testNodeHandler.importTestNodes(dump);
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
async #handleCheckNodeUpdate(args) {
|
|
733
|
+
const { node_id } = args;
|
|
734
|
+
return await this.#commandHandler.checkNodeUpdate(NodeId(node_id));
|
|
735
|
+
}
|
|
736
|
+
async #handleUpdateNode(args) {
|
|
737
|
+
const { node_id, software_version } = args;
|
|
738
|
+
const targetVersion = typeof software_version === "string" ? parseInt(software_version, 10) : software_version;
|
|
739
|
+
return await this.#commandHandler.updateNode(NodeId(node_id), targetVersion);
|
|
740
|
+
}
|
|
741
|
+
async #collectNodeDetails(nodeId) {
|
|
742
|
+
const lastInterviewDate = this.#lastInterviewDates.get(nodeId);
|
|
743
|
+
return await this.#commandHandler.getNodeDetails(nodeId, lastInterviewDate);
|
|
744
|
+
}
|
|
745
|
+
#convertCommandDataToWebSocketTagBased(clusterId, commandName, value, clusterData) {
|
|
746
|
+
if (!clusterData) {
|
|
747
|
+
const cluster = getClusterById(clusterId);
|
|
748
|
+
clusterData = clusterData ?? ClusterMap[cluster.name.toLowerCase()];
|
|
749
|
+
}
|
|
750
|
+
if (clusterData === void 0 || clusterData.commands[commandName.toLowerCase()] === void 0) {
|
|
751
|
+
logger.warn(
|
|
752
|
+
`Cluster ${clusterId} does not have command ${commandName}. Do not convert data to WebSocket tag based`,
|
|
753
|
+
value
|
|
754
|
+
);
|
|
755
|
+
return {};
|
|
756
|
+
}
|
|
757
|
+
return convertMatterToWebSocketTagBased(
|
|
758
|
+
value,
|
|
759
|
+
clusterData.commands[commandName.toLowerCase()].responseModel,
|
|
760
|
+
clusterData.model
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
export {
|
|
765
|
+
WebSocketControllerHandler
|
|
766
|
+
};
|
|
767
|
+
//# sourceMappingURL=WebSocketControllerHandler.js.map
|