@matter-server/ws-controller 0.5.6 → 0.5.8
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/ControllerCommandHandler.d.ts +3 -3
- package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -1
- package/dist/esm/controller/ControllerCommandHandler.js +10 -13
- package/dist/esm/controller/ControllerCommandHandler.js.map +1 -1
- package/dist/esm/controller/CustomClusterPoller.d.ts +2 -2
- package/dist/esm/controller/CustomClusterPoller.d.ts.map +1 -1
- package/dist/esm/controller/CustomClusterPoller.js +21 -5
- package/dist/esm/controller/CustomClusterPoller.js.map +1 -1
- package/dist/esm/controller/LegacyDataInjector.d.ts +2 -2
- package/dist/esm/controller/LegacyDataInjector.d.ts.map +1 -1
- package/dist/esm/controller/LegacyDataInjector.js +5 -1
- package/dist/esm/controller/LegacyDataInjector.js.map +1 -1
- package/dist/esm/controller/MatterController.d.ts.map +1 -1
- package/dist/esm/controller/MatterController.js +3 -3
- package/dist/esm/controller/MatterController.js.map +1 -1
- package/dist/esm/controller/TestNodeCommandHandler.d.ts +1 -1
- package/dist/esm/controller/TestNodeCommandHandler.d.ts.map +1 -1
- package/dist/esm/model/ModelMapper.js +2 -2
- package/dist/esm/model/ModelMapper.js.map +1 -1
- package/dist/esm/server/ConfigStorage.d.ts +2 -2
- package/dist/esm/server/ConfigStorage.d.ts.map +1 -1
- package/dist/esm/server/ConfigStorage.js.map +1 -1
- package/dist/esm/server/Converters.js +4 -4
- package/dist/esm/server/Converters.js.map +1 -1
- package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -1
- package/dist/esm/server/WebSocketControllerHandler.js +11 -8
- package/dist/esm/server/WebSocketControllerHandler.js.map +1 -1
- package/dist/esm/types/CommandHandler.d.ts +2 -4
- package/dist/esm/types/CommandHandler.d.ts.map +1 -1
- package/dist/esm/types/WebSocketMessageTypes.d.ts +6 -453
- package/dist/esm/types/WebSocketMessageTypes.d.ts.map +1 -1
- package/dist/esm/types/WebSocketMessageTypes.js +4 -7
- package/dist/esm/types/WebSocketMessageTypes.js.map +2 -2
- package/package.json +7 -6
- package/src/controller/ControllerCommandHandler.ts +9 -12
- package/src/controller/CustomClusterPoller.ts +21 -7
- package/src/controller/LegacyDataInjector.ts +7 -3
- package/src/controller/MatterController.ts +4 -3
- package/src/model/ModelMapper.ts +2 -2
- package/src/server/ConfigStorage.ts +2 -2
- package/src/server/Converters.ts +4 -4
- package/src/server/WebSocketControllerHandler.ts +12 -9
- package/src/tsconfig.json +9 -3
- package/src/types/CommandHandler.ts +2 -4
- package/src/types/WebSocketMessageTypes.ts +38 -456
|
@@ -32,10 +32,10 @@ const EVE_ENERGY_ATTRIBUTE_IDS = {
|
|
|
32
32
|
// Standard Matter ElectricalPowerMeasurement cluster ID
|
|
33
33
|
const ELECTRICAL_POWER_MEASUREMENT_CLUSTER_ID = 0x0090; // 144
|
|
34
34
|
|
|
35
|
-
// Polling interval in milliseconds (
|
|
36
|
-
const POLLING_INTERVAL_MS =
|
|
35
|
+
// Polling interval in milliseconds (60 seconds)
|
|
36
|
+
const POLLING_INTERVAL_MS = 60_000;
|
|
37
37
|
|
|
38
|
-
// Maximum initial delay in milliseconds (random
|
|
38
|
+
// Maximum initial delay in milliseconds (random 30-60s to stagger startup)
|
|
39
39
|
const MAX_INITIAL_DELAY_MS = 30_000;
|
|
40
40
|
|
|
41
41
|
// Attribute path format: endpoint/cluster/attribute
|
|
@@ -117,11 +117,12 @@ export class CustomClusterPoller {
|
|
|
117
117
|
#attributeReader: NodeAttributeReader;
|
|
118
118
|
#isPolling = false;
|
|
119
119
|
#currentDelayPromise?: CancelablePromise;
|
|
120
|
+
#currentReadPromise?: Promise<void>;
|
|
120
121
|
#closed = false;
|
|
121
122
|
|
|
122
123
|
constructor(attributeReader: NodeAttributeReader) {
|
|
123
124
|
this.#attributeReader = attributeReader;
|
|
124
|
-
const delay = Millis(Math.random() * MAX_INITIAL_DELAY_MS);
|
|
125
|
+
const delay = Millis(MAX_INITIAL_DELAY_MS + Math.random() * MAX_INITIAL_DELAY_MS);
|
|
125
126
|
this.#pollerTimer = Time.getTimer("eve-poller", delay, () => this.#pollAllNodes());
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -160,13 +161,16 @@ export class CustomClusterPoller {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
/**
|
|
163
|
-
* Stop all polling and cleanup.
|
|
164
|
+
* Stop all polling and cleanup. Awaits any in-flight read operation.
|
|
164
165
|
*/
|
|
165
|
-
stop(): void {
|
|
166
|
+
async stop(): Promise<void> {
|
|
166
167
|
this.#closed = true;
|
|
167
168
|
this.#currentDelayPromise?.cancel(new Error("Close"));
|
|
168
169
|
this.#pollerTimer?.stop();
|
|
169
170
|
this.#polledAttributes.clear();
|
|
171
|
+
if (this.#currentReadPromise) {
|
|
172
|
+
await this.#currentReadPromise;
|
|
173
|
+
}
|
|
170
174
|
logger.info("Custom attribute poller stopped");
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -210,6 +214,9 @@ export class CustomClusterPoller {
|
|
|
210
214
|
try {
|
|
211
215
|
const entries = Array.from(this.#polledAttributes.entries());
|
|
212
216
|
for (let i = 0; i < entries.length; i++) {
|
|
217
|
+
if (this.#closed) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
213
220
|
const [nodeId, attributePaths] = entries[i];
|
|
214
221
|
if (!this.#polledAttributes.has(nodeId)) {
|
|
215
222
|
// Node was removed, so skip it
|
|
@@ -253,9 +260,16 @@ export class CustomClusterPoller {
|
|
|
253
260
|
try {
|
|
254
261
|
// Read with fabricFiltered=true as per Eve's requirements
|
|
255
262
|
// This automatically updates the attribute cache and triggers change events
|
|
256
|
-
|
|
263
|
+
const readPromise = this.#attributeReader.handleReadAttributes(nodeId, paths, true);
|
|
264
|
+
this.#currentReadPromise = readPromise.then(
|
|
265
|
+
() => {},
|
|
266
|
+
() => {},
|
|
267
|
+
);
|
|
268
|
+
await readPromise;
|
|
257
269
|
} catch (error) {
|
|
258
270
|
logger.warn(`Failed to poll custom attributes for node ${nodeId}: `, error);
|
|
271
|
+
} finally {
|
|
272
|
+
this.#currentReadPromise = undefined;
|
|
259
273
|
}
|
|
260
274
|
}
|
|
261
275
|
}
|
|
@@ -95,7 +95,7 @@ export interface LegacyVendorInfo {
|
|
|
95
95
|
|
|
96
96
|
/** Node data from Python Matter Server nodes map */
|
|
97
97
|
export interface LegacyNodeData {
|
|
98
|
-
node_id: number;
|
|
98
|
+
node_id: number | bigint;
|
|
99
99
|
date_commissioned: string;
|
|
100
100
|
last_interview: string;
|
|
101
101
|
interview_version: number;
|
|
@@ -108,7 +108,7 @@ export interface LegacyNodeData {
|
|
|
108
108
|
/** Structure of the <compressedFabricId>.json file */
|
|
109
109
|
export interface LegacyServerFile {
|
|
110
110
|
vendor_info: Record<string, LegacyVendorInfo>;
|
|
111
|
-
last_node_id: number;
|
|
111
|
+
last_node_id: number | bigint;
|
|
112
112
|
nodes: Record<string, LegacyNodeData>;
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -279,9 +279,13 @@ export namespace LegacyDataInjector {
|
|
|
279
279
|
}
|
|
280
280
|
commissionedNodes.push([NodeId(BigInt(nodeId)), {}]);
|
|
281
281
|
let newNode = true;
|
|
282
|
+
const attributes = Object.entries(nodeDetails.attributes);
|
|
283
|
+
if (attributes.length === 0) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
282
286
|
logger.info(`Injecting node ${nodeId} into storage`);
|
|
283
287
|
const nodeWrites = new Array<MaybePromise<void>>();
|
|
284
|
-
for (const [attributeKey, value] of
|
|
288
|
+
for (const [attributeKey, value] of attributes) {
|
|
285
289
|
let currentEndpointId: string | undefined;
|
|
286
290
|
let currentClusterId: string | undefined;
|
|
287
291
|
let endpointStorage: StorageContext | undefined;
|
|
@@ -122,9 +122,10 @@ export class MatterController {
|
|
|
122
122
|
|
|
123
123
|
// Check if the nextNodeId needs to be updated based on legacy data
|
|
124
124
|
const lastNodeId = legacyData.nodeData?.last_node_id;
|
|
125
|
-
if (typeof lastNodeId === "number") {
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
if (typeof lastNodeId === "number" || typeof lastNodeId === "bigint") {
|
|
126
|
+
// Compare as BigInt to safely handle both number and bigint types
|
|
127
|
+
if (BigInt(config.nextNodeId) <= BigInt(lastNodeId)) {
|
|
128
|
+
const newNextNodeId = BigInt(lastNodeId) + 10n;
|
|
128
129
|
logger.info(
|
|
129
130
|
`Updating nextNodeId from ${config.nextNodeId} to ${newNextNodeId} (legacy last_node_id: ${lastNodeId})`,
|
|
130
131
|
);
|
package/src/model/ModelMapper.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
EventModel,
|
|
16
16
|
FeatureMap,
|
|
17
17
|
GeneratedCommandList,
|
|
18
|
-
|
|
18
|
+
Matter,
|
|
19
19
|
} from "@matter/main/model";
|
|
20
20
|
|
|
21
21
|
type AttributeDetails = { readonly [key: string]: AttributeModel | undefined };
|
|
@@ -53,7 +53,7 @@ export type ClusterMapType = {
|
|
|
53
53
|
// Build the cluster map at module load time
|
|
54
54
|
const clusterMapBuilder: { [key: string]: ClusterMapEntry | undefined } = {};
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Matter.clusters.forEach(cluster => {
|
|
57
57
|
if (cluster.id === undefined) {
|
|
58
58
|
return;
|
|
59
59
|
} // Skip clusters without an ID
|
|
@@ -20,7 +20,7 @@ function sanitizeForLog(key: string, value: unknown): string {
|
|
|
20
20
|
|
|
21
21
|
interface ConfigData {
|
|
22
22
|
fabricLabel: string;
|
|
23
|
-
nextNodeId: number
|
|
23
|
+
nextNodeId: number | bigint;
|
|
24
24
|
wifiSsid?: string;
|
|
25
25
|
wifiCredentials?: string;
|
|
26
26
|
threadDataset?: string;
|
|
@@ -68,7 +68,7 @@ export class ConfigStorage {
|
|
|
68
68
|
const fabricLabel = (await this.#configStore.has("fabricLabel"))
|
|
69
69
|
? await this.#configStore.get<string>("fabricLabel")
|
|
70
70
|
: (this.#env.vars.string("fabricLabel") ?? this.#data.fabricLabel);
|
|
71
|
-
const nextNodeId = await this.#configStore.get<number>("nextNodeId", this.#data.nextNodeId);
|
|
71
|
+
const nextNodeId = await this.#configStore.get<number | bigint>("nextNodeId", this.#data.nextNodeId);
|
|
72
72
|
|
|
73
73
|
const wifiSsid = (await this.#configStore.has("wifiSsid"))
|
|
74
74
|
? await this.#configStore.get<string>("wifiSsid", "")
|
package/src/server/Converters.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function convertWebSocketTagBasedToMatter(
|
|
|
90
90
|
|
|
91
91
|
// Handle lists
|
|
92
92
|
if (Array.isArray(value) && model.type === "list") {
|
|
93
|
-
return value.map(v => convertWebSocketTagBasedToMatter(v, model.members
|
|
93
|
+
return value.map(v => convertWebSocketTagBasedToMatter(v, model.members.at(0), clusterModel));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// Handle structs - convert numeric keys to camelCased member names
|
|
@@ -136,7 +136,7 @@ export function convertCommandDataToMatter(
|
|
|
136
136
|
|
|
137
137
|
// Handle lists
|
|
138
138
|
if (Array.isArray(value) && model.type === "list") {
|
|
139
|
-
return value.map(v => convertCommandDataToMatter(v, model.members
|
|
139
|
+
return value.map(v => convertCommandDataToMatter(v, model.members.at(0), clusterModel));
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
// Handle structs - convert numeric keys to camelCased member names
|
|
@@ -303,7 +303,7 @@ function convertMatterToWebSocket(
|
|
|
303
303
|
|
|
304
304
|
case ConvKind.List:
|
|
305
305
|
return Array.isArray(value)
|
|
306
|
-
? value.map(v => convertMatterToWebSocket(v, model.members
|
|
306
|
+
? value.map(v => convertMatterToWebSocket(v, model.members.at(0), clusterModel, tagBased))
|
|
307
307
|
: value;
|
|
308
308
|
|
|
309
309
|
case ConvKind.Struct: {
|
|
@@ -527,7 +527,7 @@ export function convertWebsocketDataToMatter(value: any, model: ValueModel): any
|
|
|
527
527
|
value = parseChipJSON(value);
|
|
528
528
|
}
|
|
529
529
|
if (Array.isArray(value)) {
|
|
530
|
-
return value.map(v => convertWebsocketDataToMatter(v, model.members
|
|
530
|
+
return value.map(v => convertWebsocketDataToMatter(v, model.members.at(0)!));
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
|
|
@@ -165,7 +165,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
165
165
|
|
|
166
166
|
// Register all event listeners using ObserverGroup for easy cleanup
|
|
167
167
|
observers.on(this.#commandHandler.events.attributeChanged, (nodeId, data) => {
|
|
168
|
-
if (this.#closed) return;
|
|
168
|
+
if (this.#closed || !listening) return;
|
|
169
169
|
const { endpointId, clusterId, attributeId } = data.path;
|
|
170
170
|
const pathStr = `${endpointId}/${clusterId}/${attributeId}`;
|
|
171
171
|
const clusterData = ClusterMap[clusterId];
|
|
@@ -206,7 +206,7 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
206
206
|
const eventModel = clusterData?.events[eventId];
|
|
207
207
|
const convertedData =
|
|
208
208
|
event.data !== undefined
|
|
209
|
-
?
|
|
209
|
+
? convertMatterToWebSocketNameBased(event.data, eventModel, clusterData?.model)
|
|
210
210
|
: null;
|
|
211
211
|
|
|
212
212
|
const nodeEvent: MatterNodeEvent = {
|
|
@@ -361,7 +361,6 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
});
|
|
364
|
-
console.log("send close to clients");
|
|
365
364
|
|
|
366
365
|
const wss = this.#wss;
|
|
367
366
|
// Wait for the WebSocket server to close properly
|
|
@@ -379,10 +378,10 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
379
378
|
async #handleWebSocketRequest(
|
|
380
379
|
connId: string,
|
|
381
380
|
data: string,
|
|
382
|
-
): Promise<{ response: ErrorResultMessage | SuccessResultMessage
|
|
381
|
+
): Promise<{ response: ErrorResultMessage | SuccessResultMessage; enableListeners?: boolean }> {
|
|
383
382
|
let messageId: string | undefined;
|
|
384
383
|
try {
|
|
385
|
-
logger.debug(`[${connId}]
|
|
384
|
+
logger.debug(`[${connId}] WebSocket request`, () => data);
|
|
386
385
|
const request = parseBigIntAwareJson(data) as { message_id: string; command: string; args: any };
|
|
387
386
|
const { command, args } = request;
|
|
388
387
|
messageId = request.message_id;
|
|
@@ -491,9 +490,9 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
491
490
|
throw ServerError.sdkStackError("Command handler returned no response");
|
|
492
491
|
}
|
|
493
492
|
if (skipMessageContentInLogFor.includes(command)) {
|
|
494
|
-
logger.debug(`[${connId}] WebSocket
|
|
493
|
+
logger.debug(`[${connId}] WebSocket response (${command})`, messageId);
|
|
495
494
|
} else {
|
|
496
|
-
logger.debug(`[${connId}] WebSocket
|
|
495
|
+
logger.debug(`[${connId}] WebSocket response (${command})`, messageId, result);
|
|
497
496
|
}
|
|
498
497
|
return {
|
|
499
498
|
response: {
|
|
@@ -603,7 +602,9 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
603
602
|
}
|
|
604
603
|
}
|
|
605
604
|
|
|
606
|
-
await this.#config.set({
|
|
605
|
+
await this.#config.set({
|
|
606
|
+
nextNodeId: typeof nextNodeId === "bigint" ? nextNodeId + 1n : nextNodeId + 1,
|
|
607
|
+
});
|
|
607
608
|
|
|
608
609
|
const { nodeId } = await this.#commandHandler.commissionNode({
|
|
609
610
|
nodeId: NodeId(nextNodeId),
|
|
@@ -657,7 +658,9 @@ export class WebSocketControllerHandler implements WebServerHandler {
|
|
|
657
658
|
break;
|
|
658
659
|
}
|
|
659
660
|
|
|
660
|
-
await this.#config.set({
|
|
661
|
+
await this.#config.set({
|
|
662
|
+
nextNodeId: typeof nextNodeId === "bigint" ? nextNodeId + 1n : nextNodeId + 1,
|
|
663
|
+
});
|
|
661
664
|
|
|
662
665
|
const { nodeId } = await this.#commandHandler.commissionNode(commissionRequest);
|
|
663
666
|
|
package/src/tsconfig.json
CHANGED
|
@@ -211,11 +211,9 @@ export type OpenCommissioningWindowResponse = {
|
|
|
211
211
|
qrCode: string;
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
-
import { AttributesData,
|
|
214
|
+
import type { AttributesData, MatterNodeData } from "@matter-server/ws-client";
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
export type { AttributesData };
|
|
218
|
-
export type MatterNodeData = MatterNode;
|
|
216
|
+
export type { AttributesData, MatterNodeData };
|
|
219
217
|
|
|
220
218
|
/**
|
|
221
219
|
* Interface for node command handlers.
|