@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.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +11 -0
  3. package/dist/esm/controller/AttributeDataCache.d.ts +49 -0
  4. package/dist/esm/controller/AttributeDataCache.d.ts.map +1 -0
  5. package/dist/esm/controller/AttributeDataCache.js +154 -0
  6. package/dist/esm/controller/AttributeDataCache.js.map +6 -0
  7. package/dist/esm/controller/ControllerCommandHandler.d.ts +118 -0
  8. package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -0
  9. package/dist/esm/controller/ControllerCommandHandler.js +1015 -0
  10. package/dist/esm/controller/ControllerCommandHandler.js.map +6 -0
  11. package/dist/esm/controller/LegacyDataInjector.d.ts +95 -0
  12. package/dist/esm/controller/LegacyDataInjector.d.ts.map +1 -0
  13. package/dist/esm/controller/LegacyDataInjector.js +196 -0
  14. package/dist/esm/controller/LegacyDataInjector.js.map +6 -0
  15. package/dist/esm/controller/MatterController.d.ts +59 -0
  16. package/dist/esm/controller/MatterController.d.ts.map +1 -0
  17. package/dist/esm/controller/MatterController.js +212 -0
  18. package/dist/esm/controller/MatterController.js.map +6 -0
  19. package/dist/esm/controller/Nodes.d.ts +62 -0
  20. package/dist/esm/controller/Nodes.d.ts.map +1 -0
  21. package/dist/esm/controller/Nodes.js +85 -0
  22. package/dist/esm/controller/Nodes.js.map +6 -0
  23. package/dist/esm/controller/TestNodeCommandHandler.d.ts +84 -0
  24. package/dist/esm/controller/TestNodeCommandHandler.d.ts.map +1 -0
  25. package/dist/esm/controller/TestNodeCommandHandler.js +225 -0
  26. package/dist/esm/controller/TestNodeCommandHandler.js.map +6 -0
  27. package/dist/esm/data/VendorIDs.d.ts +7 -0
  28. package/dist/esm/data/VendorIDs.d.ts.map +1 -0
  29. package/dist/esm/data/VendorIDs.js +1237 -0
  30. package/dist/esm/data/VendorIDs.js.map +6 -0
  31. package/dist/esm/example/send-command.d.ts +7 -0
  32. package/dist/esm/example/send-command.d.ts.map +1 -0
  33. package/dist/esm/example/send-command.js +60 -0
  34. package/dist/esm/example/send-command.js.map +6 -0
  35. package/dist/esm/index.d.ts +21 -0
  36. package/dist/esm/index.d.ts.map +1 -0
  37. package/dist/esm/index.js +26 -0
  38. package/dist/esm/index.js.map +6 -0
  39. package/dist/esm/model/ModelMapper.d.ts +34 -0
  40. package/dist/esm/model/ModelMapper.d.ts.map +1 -0
  41. package/dist/esm/model/ModelMapper.js +62 -0
  42. package/dist/esm/model/ModelMapper.js.map +6 -0
  43. package/dist/esm/package.json +3 -0
  44. package/dist/esm/server/ConfigStorage.d.ts +29 -0
  45. package/dist/esm/server/ConfigStorage.d.ts.map +1 -0
  46. package/dist/esm/server/ConfigStorage.js +84 -0
  47. package/dist/esm/server/ConfigStorage.js.map +6 -0
  48. package/dist/esm/server/Converters.d.ts +53 -0
  49. package/dist/esm/server/Converters.d.ts.map +1 -0
  50. package/dist/esm/server/Converters.js +343 -0
  51. package/dist/esm/server/Converters.js.map +6 -0
  52. package/dist/esm/server/WebSocketControllerHandler.d.ts +21 -0
  53. package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -0
  54. package/dist/esm/server/WebSocketControllerHandler.js +767 -0
  55. package/dist/esm/server/WebSocketControllerHandler.js.map +6 -0
  56. package/dist/esm/types/CommandHandler.d.ts +258 -0
  57. package/dist/esm/types/CommandHandler.d.ts.map +1 -0
  58. package/dist/esm/types/CommandHandler.js +6 -0
  59. package/dist/esm/types/CommandHandler.js.map +6 -0
  60. package/dist/esm/types/WebServer.d.ts +12 -0
  61. package/dist/esm/types/WebServer.d.ts.map +1 -0
  62. package/dist/esm/types/WebServer.js +6 -0
  63. package/dist/esm/types/WebServer.js.map +6 -0
  64. package/dist/esm/types/WebSocketMessageTypes.d.ts +478 -0
  65. package/dist/esm/types/WebSocketMessageTypes.d.ts.map +1 -0
  66. package/dist/esm/types/WebSocketMessageTypes.js +77 -0
  67. package/dist/esm/types/WebSocketMessageTypes.js.map +6 -0
  68. package/dist/esm/util/matterVersion.d.ts +12 -0
  69. package/dist/esm/util/matterVersion.d.ts.map +1 -0
  70. package/dist/esm/util/matterVersion.js +32 -0
  71. package/dist/esm/util/matterVersion.js.map +6 -0
  72. package/dist/esm/util/network.d.ts +14 -0
  73. package/dist/esm/util/network.d.ts.map +1 -0
  74. package/dist/esm/util/network.js +63 -0
  75. package/dist/esm/util/network.js.map +6 -0
  76. package/package.json +45 -0
  77. package/src/controller/AttributeDataCache.ts +194 -0
  78. package/src/controller/ControllerCommandHandler.ts +1256 -0
  79. package/src/controller/LegacyDataInjector.ts +314 -0
  80. package/src/controller/MatterController.ts +265 -0
  81. package/src/controller/Nodes.ts +115 -0
  82. package/src/controller/TestNodeCommandHandler.ts +305 -0
  83. package/src/data/VendorIDs.ts +1234 -0
  84. package/src/example/send-command.ts +82 -0
  85. package/src/index.ts +33 -0
  86. package/src/model/ModelMapper.ts +87 -0
  87. package/src/server/ConfigStorage.ts +112 -0
  88. package/src/server/Converters.ts +483 -0
  89. package/src/server/WebSocketControllerHandler.ts +917 -0
  90. package/src/tsconfig.json +7 -0
  91. package/src/types/CommandHandler.ts +270 -0
  92. package/src/types/WebServer.ts +14 -0
  93. package/src/types/WebSocketMessageTypes.ts +525 -0
  94. package/src/util/matterVersion.ts +45 -0
  95. package/src/util/network.ts +85 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { platform } from "node:os";
8
+ const PLATFORM_MAC = platform() === "darwin";
9
+ async function pingIp(ipAddress, timeout = 2, attempts = 1) {
10
+ const isIpv6 = ipAddress.includes(":");
11
+ let command;
12
+ let args;
13
+ if (isIpv6 && PLATFORM_MAC) {
14
+ command = "ping6";
15
+ args = ["-c", "1", ipAddress];
16
+ } else if (isIpv6) {
17
+ command = "ping";
18
+ args = ["-6", "-c", "1", "-W", timeout.toString(), ipAddress];
19
+ } else {
20
+ command = "ping";
21
+ args = ["-c", "1", "-W", timeout.toString(), ipAddress];
22
+ }
23
+ while (attempts > 0) {
24
+ attempts--;
25
+ try {
26
+ const success = await runPingCommand(command, args, timeout + 2);
27
+ if (success || attempts === 0) {
28
+ return success;
29
+ }
30
+ } catch {
31
+ if (attempts === 0) {
32
+ return false;
33
+ }
34
+ }
35
+ if (attempts > 0) {
36
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+ function runPingCommand(command, args, timeoutSeconds) {
42
+ return new Promise((resolve, reject) => {
43
+ const proc = spawn(command, args, {
44
+ stdio: ["ignore", "pipe", "pipe"]
45
+ });
46
+ const timer = setTimeout(() => {
47
+ proc.kill();
48
+ reject(new Error("Ping timeout"));
49
+ }, timeoutSeconds * 1e3);
50
+ proc.on("close", (code) => {
51
+ clearTimeout(timer);
52
+ resolve(code === 0);
53
+ });
54
+ proc.on("error", (err) => {
55
+ clearTimeout(timer);
56
+ reject(err);
57
+ });
58
+ });
59
+ }
60
+ export {
61
+ pingIp
62
+ };
63
+ //# sourceMappingURL=network.js.map
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/util/network.ts"],
4
+ "mappings": "AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,SAAS,aAAa;AACtB,SAAS,gBAAgB;AAEzB,MAAM,eAAe,SAAS,MAAM;AASpC,eAAsB,OAAO,WAAmB,UAAU,GAAG,WAAW,GAAqB;AACzF,QAAM,SAAS,UAAU,SAAS,GAAG;AAGrC,MAAI;AACJ,MAAI;AAEJ,MAAI,UAAU,cAAc;AAExB,cAAU;AACV,WAAO,CAAC,MAAM,KAAK,SAAS;AAAA,EAChC,WAAW,QAAQ;AAEf,cAAU;AACV,WAAO,CAAC,MAAM,MAAM,KAAK,MAAM,QAAQ,SAAS,GAAG,SAAS;AAAA,EAChE,OAAO;AAEH,cAAU;AACV,WAAO,CAAC,MAAM,KAAK,MAAM,QAAQ,SAAS,GAAG,SAAS;AAAA,EAC1D;AAEA,SAAO,WAAW,GAAG;AACjB;AACA,QAAI;AACA,YAAM,UAAU,MAAM,eAAe,SAAS,MAAM,UAAU,CAAC;AAC/D,UAAI,WAAW,aAAa,GAAG;AAC3B,eAAO;AAAA,MACX;AAAA,IACJ,QAAQ;AAEJ,UAAI,aAAa,GAAG;AAChB,eAAO;AAAA,MACX;AAAA,IACJ;AAEA,QAAI,WAAW,GAAG;AACd,YAAM,IAAI,QAAQ,aAAW,WAAW,SAAS,GAAK,CAAC;AAAA,IAC3D;AAAA,EACJ;AACA,SAAO;AACX;AAKA,SAAS,eAAe,SAAiB,MAAgB,gBAA0C;AAC/F,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACpC,UAAM,OAAO,MAAM,SAAS,MAAM;AAAA,MAC9B,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IACpC,CAAC;AAED,UAAM,QAAQ,WAAW,MAAM;AAC3B,WAAK,KAAK;AACV,aAAO,IAAI,MAAM,cAAc,CAAC;AAAA,IACpC,GAAG,iBAAiB,GAAI;AAExB,SAAK,GAAG,SAAS,UAAQ;AACrB,mBAAa,KAAK;AAClB,cAAQ,SAAS,CAAC;AAAA,IACtB,CAAC;AAED,SAAK,GAAG,SAAS,SAAO;AACpB,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACd,CAAC;AAAA,EACL,CAAC;AACL;",
5
+ "names": []
6
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@matter-server/ws-controller",
3
+ "version": "0.2.0-alpha.0-00000000-000000000",
4
+ "type": "module",
5
+ "description": "matter.js based Matter controller library",
6
+ "bugs": {
7
+ "url": "https://github.com/matter-js/matterjs-server/issues"
8
+ },
9
+ "homepage": "https://github.com/matter-js/matterjs-server",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/matter-js/matterjs-server.git"
13
+ },
14
+ "author": "Apollon77",
15
+ "main": "dist/esm/index.js",
16
+ "scripts": {
17
+ "clean": "matter-build clean",
18
+ "build": "matter-build",
19
+ "build-clean": "matter-build --clean"
20
+ },
21
+ "engines": {
22
+ "node": ">=20.19.0 <22.0.0 || >=22.13.0"
23
+ },
24
+ "optionalDependencies": {
25
+ "@matter/nodejs-ble": "0.16.1"
26
+ },
27
+ "dependencies": {
28
+ "@matter/main": "0.16.1",
29
+ "@project-chip/matter.js": "0.16.1",
30
+ "ws": "^8.18.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.0.3",
34
+ "@types/ws": "^8.18.1"
35
+ },
36
+ "files": [
37
+ "dist/**/*",
38
+ "src/**/*",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { ClusterBehavior, Logger, NodeId } from "@matter/main";
8
+ import { DecodedAttributeReportValue } from "@matter/main/protocol";
9
+ import { AttributeId, ClusterId, EndpointNumber, getClusterById } from "@matter/main/types";
10
+ import { Endpoint, PairedNode } from "@project-chip/matter.js/device";
11
+ import { ClusterMap } from "../model/ModelMapper.js";
12
+ import { buildAttributePath, convertMatterToWebSocketTagBased } from "../server/Converters.js";
13
+ import { AttributesData } from "../types/CommandHandler.js";
14
+
15
+ const logger = Logger.get("AttributeDataCache");
16
+
17
+ /**
18
+ * Nested cache structure for node attributes in WebSocket format.
19
+ * Structure: endpointId -> clusterId -> attributeId -> value
20
+ */
21
+ type EndpointAttributeCache = Map<EndpointNumber, Map<ClusterId, Map<AttributeId, unknown>>>;
22
+
23
+ /**
24
+ * Cache for node attributes in WebSocket format.
25
+ *
26
+ * Stores attributes pre-converted to WebSocket tag-based format for fast retrieval
27
+ * when clients request node data. The cache is structured as:
28
+ * nodeId -> endpointId -> clusterId -> attributeId -> value
29
+ */
30
+ export class AttributeDataCache {
31
+ #cache = new Map<NodeId, EndpointAttributeCache>();
32
+
33
+ /**
34
+ * Add a node to the cache and populate its attributes.
35
+ * If the node is not initialized, the cache entry will be empty.
36
+ */
37
+ add(node: PairedNode): void {
38
+ this.#populateFromNode(node);
39
+ }
40
+
41
+ /**
42
+ * Remove a node from the cache.
43
+ */
44
+ delete(nodeId: NodeId): void {
45
+ this.#cache.delete(nodeId);
46
+ }
47
+
48
+ /**
49
+ * Update (reinitialize) the cache for a node.
50
+ * Creates a fresh cache structure from the node's current state.
51
+ * Use this when the node structure may have changed (endpoints added/removed).
52
+ */
53
+ update(node: PairedNode): void {
54
+ this.#populateFromNode(node);
55
+ }
56
+
57
+ /**
58
+ * Update a single attribute in the cache.
59
+ * Use this for incremental updates when an attribute value changes.
60
+ */
61
+ updateAttribute(nodeId: NodeId, data: DecodedAttributeReportValue<any>): void {
62
+ const { endpointId, clusterId, attributeId } = data.path;
63
+
64
+ let nodeCache = this.#cache.get(nodeId);
65
+ if (!nodeCache) {
66
+ nodeCache = new Map();
67
+ this.#cache.set(nodeId, nodeCache);
68
+ }
69
+
70
+ let endpointCache = nodeCache.get(endpointId);
71
+ if (!endpointCache) {
72
+ endpointCache = new Map();
73
+ nodeCache.set(endpointId, endpointCache);
74
+ }
75
+
76
+ let clusterCache = endpointCache.get(clusterId);
77
+ if (!clusterCache) {
78
+ clusterCache = new Map();
79
+ endpointCache.set(clusterId, clusterCache);
80
+ }
81
+
82
+ // Convert and store the value
83
+ const cluster = getClusterById(clusterId);
84
+ const clusterData = ClusterMap[cluster.name.toLowerCase()];
85
+ const convertedValue = convertMatterToWebSocketTagBased(
86
+ data.value,
87
+ clusterData?.attributes[attributeId],
88
+ clusterData?.model,
89
+ );
90
+ clusterCache.set(attributeId, convertedValue);
91
+ }
92
+
93
+ /**
94
+ * Get cached attributes for a node as flat AttributesData.
95
+ * Returns undefined if no cache exists for the node.
96
+ */
97
+ get(nodeId: NodeId): AttributesData | undefined {
98
+ const nodeCache = this.#cache.get(nodeId);
99
+ if (!nodeCache) {
100
+ return undefined;
101
+ }
102
+
103
+ const attributes: AttributesData = {};
104
+ for (const [endpointId, endpointCache] of nodeCache) {
105
+ for (const [clusterId, clusterCache] of endpointCache) {
106
+ for (const [attributeId, value] of clusterCache) {
107
+ const pathStr = buildAttributePath(endpointId, clusterId, attributeId);
108
+ attributes[pathStr] = value;
109
+ }
110
+ }
111
+ }
112
+ return attributes;
113
+ }
114
+
115
+ /**
116
+ * Check if a node exists in the cache.
117
+ */
118
+ has(nodeId: NodeId): boolean {
119
+ return this.#cache.has(nodeId);
120
+ }
121
+
122
+ /**
123
+ * Populate the cache for a node from its current state.
124
+ * Creates a completely fresh cache structure.
125
+ */
126
+ #populateFromNode(node: PairedNode): void {
127
+ const nodeId = node.nodeId;
128
+ if (!node.initialized) {
129
+ logger.debug(`Node ${nodeId} not initialized, skipping cache population`);
130
+ return;
131
+ }
132
+
133
+ const rootEndpoint = node.getRootEndpoint();
134
+ if (rootEndpoint === undefined) {
135
+ logger.debug(`Node ${nodeId} has no root endpoint, skipping cache population`);
136
+ return;
137
+ }
138
+
139
+ const nodeCache: EndpointAttributeCache = new Map();
140
+ this.#collectEndpointAttributes(rootEndpoint, nodeCache);
141
+ this.#cache.set(nodeId, nodeCache);
142
+ logger.debug(`Populated attribute cache for node ${nodeId}`);
143
+ }
144
+
145
+ /**
146
+ * Recursively collect attributes from an endpoint into the cache structure.
147
+ * Always creates fresh maps for each endpoint and cluster to ensure no stale data.
148
+ */
149
+ #collectEndpointAttributes(endpoint: Endpoint, nodeCache: EndpointAttributeCache): void {
150
+ const endpointId = endpoint.number!;
151
+ // Always create fresh maps for this endpoint
152
+ const endpointCache: Map<ClusterId, Map<AttributeId, unknown>> = new Map();
153
+
154
+ for (const behavior of endpoint.endpoint.behaviors.active) {
155
+ if (!ClusterBehavior.is(behavior)) {
156
+ continue;
157
+ }
158
+ const cluster = behavior.cluster;
159
+ const clusterId = cluster.id;
160
+ const clusterData = ClusterMap[cluster.name.toLowerCase()];
161
+ const clusterState = endpoint.endpoint.stateOf(behavior) as Record<string, unknown>;
162
+
163
+ // Always create a fresh map for this cluster
164
+ const clusterCache: Map<AttributeId, unknown> = new Map();
165
+
166
+ for (const attributeName in cluster.attributes) {
167
+ const attribute = cluster.attributes[attributeName];
168
+ if (attribute === undefined) {
169
+ continue;
170
+ }
171
+ const attributeValue = clusterState[attributeName];
172
+ const convertedValue = convertMatterToWebSocketTagBased(
173
+ attributeValue,
174
+ clusterData?.attributes[attribute.id],
175
+ clusterData?.model,
176
+ );
177
+ clusterCache.set(attribute.id, convertedValue);
178
+ }
179
+
180
+ if (clusterCache.size) {
181
+ endpointCache.set(clusterId, clusterCache);
182
+ }
183
+ }
184
+
185
+ if (endpointCache.size) {
186
+ nodeCache.set(endpointId, endpointCache);
187
+ }
188
+
189
+ // Recursively collect from child endpoints
190
+ for (const childEndpoint of endpoint.getChildEndpoints()) {
191
+ this.#collectEndpointAttributes(childEndpoint, nodeCache);
192
+ }
193
+ }
194
+ }