@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,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Logger, NodeId, Observable } from "@matter/main";
|
|
8
|
+
import { parseBigIntAwareJson, splitAttributePath } from "../server/Converters.js";
|
|
9
|
+
import {
|
|
10
|
+
AttributeResponseStatus,
|
|
11
|
+
AttributesData,
|
|
12
|
+
InvokeRequest,
|
|
13
|
+
MatterNodeData,
|
|
14
|
+
NodeCommandHandler,
|
|
15
|
+
ReadAttributeRequest,
|
|
16
|
+
ReadAttributeResponse,
|
|
17
|
+
WriteAttributeRequest,
|
|
18
|
+
} from "../types/CommandHandler.js";
|
|
19
|
+
import { MatterNode, TEST_NODE_START } from "../types/WebSocketMessageTypes.js";
|
|
20
|
+
|
|
21
|
+
const logger = Logger.get("TestNodeCommandHandler");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Command handler for test nodes (imported diagnostic dumps).
|
|
25
|
+
* Test nodes are stored in memory and provide mock responses for commands.
|
|
26
|
+
*/
|
|
27
|
+
export class TestNodeCommandHandler implements NodeCommandHandler {
|
|
28
|
+
#testNodes = new Map<bigint, MatterNode>();
|
|
29
|
+
|
|
30
|
+
/** Observable for node added events */
|
|
31
|
+
readonly nodeAdded = new Observable<[nodeId: NodeId, node: MatterNode]>();
|
|
32
|
+
|
|
33
|
+
/** Observable for node removed events */
|
|
34
|
+
readonly nodeRemoved = new Observable<[nodeId: NodeId]>();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a node ID is in the test node range (>= TEST_NODE_START).
|
|
38
|
+
*/
|
|
39
|
+
static isTestNodeId(nodeId: number | bigint): boolean {
|
|
40
|
+
const bigId = typeof nodeId === "bigint" ? nodeId : BigInt(nodeId);
|
|
41
|
+
return bigId >= TEST_NODE_START;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if this handler manages the given node ID.
|
|
46
|
+
*/
|
|
47
|
+
hasNode(nodeId: NodeId): boolean {
|
|
48
|
+
return this.#testNodes.has(BigInt(nodeId));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all test node IDs.
|
|
53
|
+
*/
|
|
54
|
+
getNodeIds(): NodeId[] {
|
|
55
|
+
return Array.from(this.#testNodes.keys()).map(id => NodeId(id));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get all test nodes.
|
|
60
|
+
*/
|
|
61
|
+
getNodes(): MatterNode[] {
|
|
62
|
+
return Array.from(this.#testNodes.values());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get a test node by ID.
|
|
67
|
+
*/
|
|
68
|
+
getNode(nodeId: NodeId): MatterNode | undefined {
|
|
69
|
+
return this.#testNodes.get(BigInt(nodeId));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get full node details in WebSocket API format.
|
|
74
|
+
*/
|
|
75
|
+
async getNodeDetails(nodeId: NodeId): Promise<MatterNodeData> {
|
|
76
|
+
const testNode = this.#testNodes.get(BigInt(nodeId));
|
|
77
|
+
if (testNode === undefined) {
|
|
78
|
+
throw new Error(`Test node ${nodeId} not found`);
|
|
79
|
+
}
|
|
80
|
+
return testNode;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read multiple attributes from a test node by path strings.
|
|
85
|
+
* Handles wildcards in paths.
|
|
86
|
+
*/
|
|
87
|
+
async handleReadAttributes(
|
|
88
|
+
nodeId: NodeId,
|
|
89
|
+
attributePaths: string[],
|
|
90
|
+
_fabricFiltered?: boolean,
|
|
91
|
+
): Promise<AttributesData> {
|
|
92
|
+
const testNode = this.#testNodes.get(BigInt(nodeId));
|
|
93
|
+
if (testNode === undefined) {
|
|
94
|
+
throw new Error(`Test node ${nodeId} not found`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const result: AttributesData = {};
|
|
98
|
+
for (const path of attributePaths) {
|
|
99
|
+
const { endpointId, clusterId, attributeId } = splitAttributePath(path);
|
|
100
|
+
|
|
101
|
+
// Handle wildcards by matching all attributes
|
|
102
|
+
if (path.includes("*")) {
|
|
103
|
+
for (const [attrPath, value] of Object.entries(testNode.attributes)) {
|
|
104
|
+
const parts = attrPath.split("/").map(Number);
|
|
105
|
+
if (
|
|
106
|
+
(endpointId === undefined || parts[0] === endpointId) &&
|
|
107
|
+
(clusterId === undefined || parts[1] === clusterId) &&
|
|
108
|
+
(attributeId === undefined || parts[2] === attributeId)
|
|
109
|
+
) {
|
|
110
|
+
result[attrPath] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
result[path] = testNode.attributes[path];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Import test nodes from a diagnostic dump.
|
|
122
|
+
* @param dump JSON string containing the diagnostic dump
|
|
123
|
+
* @returns Array of imported node IDs
|
|
124
|
+
*/
|
|
125
|
+
importTestNodes(dump: string): NodeId[] {
|
|
126
|
+
// Parse the JSON dump (handles large node IDs as BigInt)
|
|
127
|
+
const dumpData = parseBigIntAwareJson(dump) as any;
|
|
128
|
+
|
|
129
|
+
// Extract nodes from dump - can be single node or multiple nodes
|
|
130
|
+
// Format from Home Assistant diagnostics:
|
|
131
|
+
// - Single node: dump_data.data.node
|
|
132
|
+
// - Multiple nodes (server dump): dump_data.data.server.nodes
|
|
133
|
+
let dumpNodes: Array<MatterNode>;
|
|
134
|
+
|
|
135
|
+
if (dumpData?.data?.node) {
|
|
136
|
+
dumpNodes = [dumpData.data.node];
|
|
137
|
+
} else if (dumpData?.data?.server?.nodes) {
|
|
138
|
+
dumpNodes = Object.values(dumpData.data.server.nodes);
|
|
139
|
+
} else if (dumpData?.data?.nodes) {
|
|
140
|
+
// Alternative format: direct nodes array
|
|
141
|
+
dumpNodes = Object.values(dumpData.data.nodes);
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error("Invalid dump format: cannot find node data");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Find the next available test node ID
|
|
147
|
+
let nextTestNodeId: bigint = TEST_NODE_START;
|
|
148
|
+
for (const existingId of this.#testNodes.keys()) {
|
|
149
|
+
if (existingId >= nextTestNodeId) {
|
|
150
|
+
nextTestNodeId = existingId + 1n;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const importedNodeIds: NodeId[] = [];
|
|
155
|
+
|
|
156
|
+
// Process each node from the dump
|
|
157
|
+
for (const nodeDict of dumpNodes) {
|
|
158
|
+
const testNodeId: bigint = nextTestNodeId++;
|
|
159
|
+
const nodeId = NodeId(testNodeId);
|
|
160
|
+
|
|
161
|
+
// Create MatterNode with test node ID, keeping original attributes as-is
|
|
162
|
+
const testNode: MatterNode = {
|
|
163
|
+
node_id: testNodeId,
|
|
164
|
+
date_commissioned: nodeDict.date_commissioned,
|
|
165
|
+
last_interview: nodeDict.last_interview,
|
|
166
|
+
interview_version: nodeDict.interview_version,
|
|
167
|
+
available: nodeDict.available,
|
|
168
|
+
is_bridge: nodeDict.is_bridge,
|
|
169
|
+
attributes: nodeDict.attributes,
|
|
170
|
+
attribute_subscriptions: [],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Store the test node
|
|
174
|
+
this.#testNodes.set(testNodeId, testNode);
|
|
175
|
+
importedNodeIds.push(nodeId);
|
|
176
|
+
|
|
177
|
+
logger.info(`Imported test node ${testNodeId} with ${Object.keys(testNode.attributes).length} attributes`);
|
|
178
|
+
|
|
179
|
+
// Emit node_added event
|
|
180
|
+
this.nodeAdded.emit(nodeId, testNode);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return importedNodeIds;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read attributes from a test node.
|
|
188
|
+
* Returns values from the stored attributes map.
|
|
189
|
+
*/
|
|
190
|
+
async handleReadAttribute(data: ReadAttributeRequest): Promise<ReadAttributeResponse> {
|
|
191
|
+
const { nodeId, endpointId, clusterId, attributeId } = data;
|
|
192
|
+
const testNode = this.#testNodes.get(BigInt(nodeId));
|
|
193
|
+
|
|
194
|
+
if (testNode === undefined) {
|
|
195
|
+
throw new Error(`Test node ${nodeId} not found`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const values: ReadAttributeResponse["values"] = [];
|
|
199
|
+
|
|
200
|
+
// Build the path pattern for matching
|
|
201
|
+
const hasWildcards = endpointId === undefined || clusterId === undefined || attributeId === undefined;
|
|
202
|
+
|
|
203
|
+
if (hasWildcards) {
|
|
204
|
+
// Match against all stored attributes
|
|
205
|
+
for (const [attrPath, value] of Object.entries(testNode.attributes)) {
|
|
206
|
+
const parts = attrPath.split("/").map(Number);
|
|
207
|
+
if (
|
|
208
|
+
(endpointId === undefined || parts[0] === endpointId) &&
|
|
209
|
+
(clusterId === undefined || parts[1] === clusterId) &&
|
|
210
|
+
(attributeId === undefined || parts[2] === attributeId)
|
|
211
|
+
) {
|
|
212
|
+
values.push({
|
|
213
|
+
endpointId: parts[0],
|
|
214
|
+
clusterId: parts[1],
|
|
215
|
+
attributeId: parts[2],
|
|
216
|
+
dataVersion: 0,
|
|
217
|
+
value,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// Direct path lookup
|
|
223
|
+
const path = `${endpointId}/${clusterId}/${attributeId}`;
|
|
224
|
+
const value = testNode.attributes[path];
|
|
225
|
+
if (value !== undefined) {
|
|
226
|
+
values.push({
|
|
227
|
+
endpointId,
|
|
228
|
+
clusterId,
|
|
229
|
+
attributeId,
|
|
230
|
+
dataVersion: 0,
|
|
231
|
+
value,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
logger.debug(`read_attribute for test node ${nodeId}: ${values.length} values`);
|
|
237
|
+
return { values };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Write an attribute to a test node.
|
|
242
|
+
* Logs the write and returns success (no actual write occurs).
|
|
243
|
+
*/
|
|
244
|
+
async handleWriteAttribute(data: WriteAttributeRequest): Promise<AttributeResponseStatus> {
|
|
245
|
+
const { nodeId, endpointId, clusterId, attributeId, value } = data;
|
|
246
|
+
|
|
247
|
+
logger.debug(
|
|
248
|
+
`write_attribute for test node ${nodeId} on ${endpointId}/${clusterId}/${attributeId} - value: ${JSON.stringify(value)}`,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
endpointId,
|
|
253
|
+
clusterId,
|
|
254
|
+
attributeId,
|
|
255
|
+
status: 0, // Success
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Invoke a command on a test node.
|
|
261
|
+
* Logs the command and returns null (no actual command execution).
|
|
262
|
+
*/
|
|
263
|
+
async handleInvoke(data: InvokeRequest): Promise<unknown> {
|
|
264
|
+
const { nodeId, endpointId, clusterId, commandName, data: payload } = data;
|
|
265
|
+
|
|
266
|
+
logger.debug(
|
|
267
|
+
`device_command for test node ${nodeId} on endpoint ${endpointId} - ` +
|
|
268
|
+
`cluster ${clusterId} - command ${commandName} - payload: ${JSON.stringify(payload)}`,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get IP addresses for a test node.
|
|
276
|
+
* Returns mock IP addresses.
|
|
277
|
+
*/
|
|
278
|
+
async getNodeIpAddresses(_nodeId: NodeId, _preferCache?: boolean): Promise<string[]> {
|
|
279
|
+
return ["0.0.0.0", "0000:1111:2222:3333:4444"];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Ping a test node.
|
|
284
|
+
* Returns mock success results.
|
|
285
|
+
*/
|
|
286
|
+
async pingNode(_nodeId: NodeId, _attempts?: number): Promise<Record<string, boolean>> {
|
|
287
|
+
return { "0.0.0.0": true, "0000:1111:2222:3333:4444": true };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Remove a test node.
|
|
292
|
+
*/
|
|
293
|
+
async removeNode(nodeId: NodeId): Promise<void> {
|
|
294
|
+
const bigId = BigInt(nodeId);
|
|
295
|
+
if (!this.#testNodes.has(bigId)) {
|
|
296
|
+
throw new Error(`Test node ${nodeId} not found`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
logger.info(`Removing test node ${nodeId}`);
|
|
300
|
+
this.#testNodes.delete(bigId);
|
|
301
|
+
|
|
302
|
+
// Emit node_removed event
|
|
303
|
+
this.nodeRemoved.emit(nodeId);
|
|
304
|
+
}
|
|
305
|
+
}
|