@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,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Simple CLI script to send a command via WebSocket and log responses.
|
|
9
|
+
*
|
|
10
|
+
* Usage: npx ts-node send-command.ts <url> <command> [args-json]
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* npx ts-node send-command.ts ws://localhost:5580/ws server_info
|
|
14
|
+
* npx ts-node send-command.ts ws://localhost:5580/ws get_node '{"node_id": 1}'
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import WebSocket from "ws";
|
|
18
|
+
|
|
19
|
+
const [, , url, command, argsJson] = process.argv;
|
|
20
|
+
|
|
21
|
+
if (!url || !command) {
|
|
22
|
+
console.error("Usage: send-command <url> <command> [args-json]");
|
|
23
|
+
console.error("");
|
|
24
|
+
console.error("Examples:");
|
|
25
|
+
console.error(" npx ts-node send-command.ts ws://localhost:5580/ws server_info");
|
|
26
|
+
console.error(" npx ts-node send-command.ts ws://localhost:5580/ws get_node '{\"node_id\": 1}'");
|
|
27
|
+
console.error(" npx ts-node send-command.ts ws://localhost:5580/ws start_listening");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let args: unknown = undefined;
|
|
32
|
+
if (argsJson) {
|
|
33
|
+
try {
|
|
34
|
+
args = JSON.parse(argsJson);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error("Error parsing args JSON:", (e as Error).message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`Connecting to ${url}...`);
|
|
42
|
+
|
|
43
|
+
const ws = new WebSocket(url);
|
|
44
|
+
|
|
45
|
+
ws.on("open", () => {
|
|
46
|
+
console.log("Connected!");
|
|
47
|
+
|
|
48
|
+
const message = {
|
|
49
|
+
message_id: `cmd-${Date.now()}`,
|
|
50
|
+
command,
|
|
51
|
+
...(args !== undefined && { args }),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
console.log("Sending:", JSON.stringify(message, null, 2));
|
|
55
|
+
ws.send(JSON.stringify(message));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
ws.on("message", (data: WebSocket.Data) => {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(data.toString());
|
|
61
|
+
console.log("\n--- Received ---");
|
|
62
|
+
console.log(JSON.stringify(parsed, null, 2));
|
|
63
|
+
} catch {
|
|
64
|
+
console.log("\n--- Received (raw) ---");
|
|
65
|
+
console.log(data.toString());
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
ws.on("error", (error: Error) => {
|
|
70
|
+
console.error("WebSocket error:", error.message);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ws.on("close", (code: number, reason: Buffer) => {
|
|
74
|
+
console.log(`\nDisconnected (code: ${code}, reason: ${reason.toString() || "none"})`);
|
|
75
|
+
process.exit(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Handle Ctrl+C gracefully
|
|
79
|
+
process.on("SIGINT", () => {
|
|
80
|
+
console.log("\nClosing connection...");
|
|
81
|
+
ws.close();
|
|
82
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @matter-server/ws-controller - Matter controller Websocket library
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Export controller components
|
|
12
|
+
export * from "./controller/ControllerCommandHandler.js";
|
|
13
|
+
export * from "./controller/LegacyDataInjector.js";
|
|
14
|
+
export * from "./controller/MatterController.js";
|
|
15
|
+
|
|
16
|
+
// Export model
|
|
17
|
+
export * from "./model/ModelMapper.js";
|
|
18
|
+
|
|
19
|
+
// Export server handlers and types
|
|
20
|
+
export * from "./server/ConfigStorage.js";
|
|
21
|
+
export * from "./server/Converters.js";
|
|
22
|
+
export * from "./server/WebSocketControllerHandler.js";
|
|
23
|
+
export * from "./types/WebServer.js";
|
|
24
|
+
|
|
25
|
+
// Export message types
|
|
26
|
+
export * from "./types/CommandHandler.js";
|
|
27
|
+
export * from "./types/WebSocketMessageTypes.js";
|
|
28
|
+
|
|
29
|
+
// Export utilities
|
|
30
|
+
export * from "./util/matterVersion.js";
|
|
31
|
+
|
|
32
|
+
// Re-Export classes from matter.js
|
|
33
|
+
export { Crypto, Environment, LogDestination, LogFormat, LogLevel, Logger } from "@matter/main";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ClusterId } from "@matter/main";
|
|
8
|
+
import {
|
|
9
|
+
AcceptedCommandList,
|
|
10
|
+
AttributeList,
|
|
11
|
+
AttributeModel,
|
|
12
|
+
ClusterModel,
|
|
13
|
+
ClusterRevision,
|
|
14
|
+
CommandModel,
|
|
15
|
+
EventModel,
|
|
16
|
+
FeatureMap,
|
|
17
|
+
GeneratedCommandList,
|
|
18
|
+
MatterModel,
|
|
19
|
+
} from "@matter/model";
|
|
20
|
+
|
|
21
|
+
type AttributeDetails = { readonly [key: string]: AttributeModel | undefined };
|
|
22
|
+
|
|
23
|
+
/** Metadata for Global attributes */
|
|
24
|
+
export const GlobalAttributes: AttributeDetails = Object.freeze({
|
|
25
|
+
clusterRevision: ClusterRevision,
|
|
26
|
+
featureMap: FeatureMap,
|
|
27
|
+
attributeList: AttributeList,
|
|
28
|
+
acceptedCommandList: AcceptedCommandList,
|
|
29
|
+
generatedCommandList: GeneratedCommandList,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Metadata for all clusters collected in an optimized form for direct access with the incoming websocket requests.
|
|
34
|
+
* All names are just lowercased to prevent differences in camelize and decamelize handling.
|
|
35
|
+
*/
|
|
36
|
+
export type ClusterMapEntry = {
|
|
37
|
+
readonly clusterId: ClusterId;
|
|
38
|
+
readonly model: ClusterModel;
|
|
39
|
+
readonly commands: { readonly [key: string]: CommandModel | undefined };
|
|
40
|
+
readonly attributes: AttributeDetails;
|
|
41
|
+
readonly events: { readonly [key: string]: EventModel | undefined };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ClusterMapType = {
|
|
45
|
+
readonly [key: string]: ClusterMapEntry | undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Build the cluster map at module load time
|
|
49
|
+
const clusterMapBuilder: { [key: string]: ClusterMapEntry | undefined } = {};
|
|
50
|
+
|
|
51
|
+
MatterModel.standard.clusters.forEach(cluster => {
|
|
52
|
+
if (cluster.id === undefined) {
|
|
53
|
+
return;
|
|
54
|
+
} // Skip clusters without an ID
|
|
55
|
+
const aces = cluster.allAces;
|
|
56
|
+
const commands: { [key: string]: CommandModel | undefined } = {};
|
|
57
|
+
const attributes: { [key: string]: AttributeModel | undefined } = {};
|
|
58
|
+
const events: { [key: string]: EventModel | undefined } = {};
|
|
59
|
+
|
|
60
|
+
aces.forEach(ace => {
|
|
61
|
+
const name = ace.name.toLowerCase();
|
|
62
|
+
if (ace instanceof CommandModel) {
|
|
63
|
+
commands[name] = ace;
|
|
64
|
+
commands[ace.id] = ace;
|
|
65
|
+
} else if (ace instanceof AttributeModel) {
|
|
66
|
+
attributes[name] = ace;
|
|
67
|
+
attributes[ace.id] = ace;
|
|
68
|
+
} else if (ace instanceof EventModel) {
|
|
69
|
+
events[name] = ace;
|
|
70
|
+
events[ace.id] = ace;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const clusterData: ClusterMapEntry = Object.freeze({
|
|
75
|
+
clusterId: ClusterId(cluster.id),
|
|
76
|
+
model: cluster,
|
|
77
|
+
commands: Object.freeze(commands),
|
|
78
|
+
attributes: Object.freeze(attributes),
|
|
79
|
+
events: Object.freeze(events),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
clusterMapBuilder[cluster.name.toLowerCase()] = clusterData;
|
|
83
|
+
clusterMapBuilder[cluster.id] = clusterData;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** Readonly map of all clusters frozen after initialization */
|
|
87
|
+
export const ClusterMap: ClusterMapType = Object.freeze(clusterMapBuilder);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Environment, Logger, StorageContext, StorageManager, StorageService } from "@matter/main";
|
|
8
|
+
|
|
9
|
+
const logger = new Logger("ConfigStorage");
|
|
10
|
+
|
|
11
|
+
interface ConfigData {
|
|
12
|
+
fabricLabel: string;
|
|
13
|
+
nextNodeId: number; // formally wrong, should be bigint
|
|
14
|
+
wifiSsid?: string;
|
|
15
|
+
wifiCredentials?: string;
|
|
16
|
+
threadDataset?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ConfigStorage {
|
|
20
|
+
#env: Environment;
|
|
21
|
+
#storageService?: StorageService;
|
|
22
|
+
#storage?: StorageManager;
|
|
23
|
+
#configStore?: StorageContext;
|
|
24
|
+
readonly #data: ConfigData = {
|
|
25
|
+
nextNodeId: 2, // 2 because controller is Node Id 1
|
|
26
|
+
fabricLabel: "HomeAssistant",
|
|
27
|
+
wifiSsid: undefined,
|
|
28
|
+
wifiCredentials: undefined,
|
|
29
|
+
threadDataset: undefined,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
static async create(env: Environment) {
|
|
33
|
+
const instance = new ConfigStorage(env);
|
|
34
|
+
await instance.open();
|
|
35
|
+
return instance;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
constructor(env: Environment) {
|
|
39
|
+
this.#env = env;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get service() {
|
|
43
|
+
if (this.#storageService === undefined) {
|
|
44
|
+
throw new Error("Storage not open");
|
|
45
|
+
}
|
|
46
|
+
return this.#storageService;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async open() {
|
|
50
|
+
this.#storageService = this.#env.get(StorageService);
|
|
51
|
+
// Use the parameter "--storage-path=NAME-OR-PATH" to specify a different storage location
|
|
52
|
+
// in this directory, use --storage-clear to start with an empty storage.
|
|
53
|
+
// Or Env vars like MATTER_STORAGE_PATH and MATTER_STORAGE_CLEAR
|
|
54
|
+
logger.info(`Storage location: ${this.#storageService.location} (Directory)`);
|
|
55
|
+
this.#storage = await this.#storageService.open("config");
|
|
56
|
+
this.#configStore = this.#storage.createContext("values");
|
|
57
|
+
|
|
58
|
+
const fabricLabel = (await this.#configStore.has("fabricLabel"))
|
|
59
|
+
? await this.#configStore.get<string>("fabricLabel")
|
|
60
|
+
: (this.#env.vars.string("fabricLabel") ?? this.#data.fabricLabel);
|
|
61
|
+
const nextNodeId = await this.#configStore.get<number>("nextNodeId", this.#data.nextNodeId);
|
|
62
|
+
|
|
63
|
+
const wifiSsid = (await this.#configStore.has("wifiSsid"))
|
|
64
|
+
? await this.#configStore.get<string>("wifiSsid", "")
|
|
65
|
+
: undefined;
|
|
66
|
+
const wifiCredentials = (await this.#configStore.has("wifiCredentials"))
|
|
67
|
+
? await this.#configStore.get<string>("wifiCredentials", "")
|
|
68
|
+
: undefined;
|
|
69
|
+
const threadDataset = (await this.#configStore.has("threadDataset"))
|
|
70
|
+
? await this.#configStore.get<string>("threadDataset", "")
|
|
71
|
+
: undefined;
|
|
72
|
+
await this.set({ fabricLabel, nextNodeId, wifiSsid, wifiCredentials, threadDataset });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get fabricLabel() {
|
|
76
|
+
return this.#data.fabricLabel;
|
|
77
|
+
}
|
|
78
|
+
get nextNodeId() {
|
|
79
|
+
return this.#data.nextNodeId;
|
|
80
|
+
}
|
|
81
|
+
get wifiSsid() {
|
|
82
|
+
return this.#data.wifiSsid;
|
|
83
|
+
}
|
|
84
|
+
get wifiCredentials() {
|
|
85
|
+
return this.#data.wifiCredentials;
|
|
86
|
+
}
|
|
87
|
+
get threadDataset() {
|
|
88
|
+
return this.#data.threadDataset;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async set(data: Partial<ConfigData>) {
|
|
92
|
+
if (!this.#configStore) {
|
|
93
|
+
throw new Error("Storage not open");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const key of Object.keys(data)) {
|
|
97
|
+
if (!(key in this.#data)) {
|
|
98
|
+
throw new Error(`Invalid key: ${key}`);
|
|
99
|
+
}
|
|
100
|
+
// @ts-expect-error key is a valid key and TS make sure about the type
|
|
101
|
+
this.#data[key] = data[key];
|
|
102
|
+
logger.info(`Set config key ${key} to ${data[key as keyof ConfigData]}`);
|
|
103
|
+
await this.#configStore.set(key, data[key as keyof ConfigData]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async close() {
|
|
108
|
+
if (this.#storage) {
|
|
109
|
+
await this.#storage.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|