@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.
Files changed (45) hide show
  1. package/dist/esm/controller/ControllerCommandHandler.d.ts +3 -3
  2. package/dist/esm/controller/ControllerCommandHandler.d.ts.map +1 -1
  3. package/dist/esm/controller/ControllerCommandHandler.js +10 -13
  4. package/dist/esm/controller/ControllerCommandHandler.js.map +1 -1
  5. package/dist/esm/controller/CustomClusterPoller.d.ts +2 -2
  6. package/dist/esm/controller/CustomClusterPoller.d.ts.map +1 -1
  7. package/dist/esm/controller/CustomClusterPoller.js +21 -5
  8. package/dist/esm/controller/CustomClusterPoller.js.map +1 -1
  9. package/dist/esm/controller/LegacyDataInjector.d.ts +2 -2
  10. package/dist/esm/controller/LegacyDataInjector.d.ts.map +1 -1
  11. package/dist/esm/controller/LegacyDataInjector.js +5 -1
  12. package/dist/esm/controller/LegacyDataInjector.js.map +1 -1
  13. package/dist/esm/controller/MatterController.d.ts.map +1 -1
  14. package/dist/esm/controller/MatterController.js +3 -3
  15. package/dist/esm/controller/MatterController.js.map +1 -1
  16. package/dist/esm/controller/TestNodeCommandHandler.d.ts +1 -1
  17. package/dist/esm/controller/TestNodeCommandHandler.d.ts.map +1 -1
  18. package/dist/esm/model/ModelMapper.js +2 -2
  19. package/dist/esm/model/ModelMapper.js.map +1 -1
  20. package/dist/esm/server/ConfigStorage.d.ts +2 -2
  21. package/dist/esm/server/ConfigStorage.d.ts.map +1 -1
  22. package/dist/esm/server/ConfigStorage.js.map +1 -1
  23. package/dist/esm/server/Converters.js +4 -4
  24. package/dist/esm/server/Converters.js.map +1 -1
  25. package/dist/esm/server/WebSocketControllerHandler.d.ts.map +1 -1
  26. package/dist/esm/server/WebSocketControllerHandler.js +11 -8
  27. package/dist/esm/server/WebSocketControllerHandler.js.map +1 -1
  28. package/dist/esm/types/CommandHandler.d.ts +2 -4
  29. package/dist/esm/types/CommandHandler.d.ts.map +1 -1
  30. package/dist/esm/types/WebSocketMessageTypes.d.ts +6 -453
  31. package/dist/esm/types/WebSocketMessageTypes.d.ts.map +1 -1
  32. package/dist/esm/types/WebSocketMessageTypes.js +4 -7
  33. package/dist/esm/types/WebSocketMessageTypes.js.map +2 -2
  34. package/package.json +7 -6
  35. package/src/controller/ControllerCommandHandler.ts +9 -12
  36. package/src/controller/CustomClusterPoller.ts +21 -7
  37. package/src/controller/LegacyDataInjector.ts +7 -3
  38. package/src/controller/MatterController.ts +4 -3
  39. package/src/model/ModelMapper.ts +2 -2
  40. package/src/server/ConfigStorage.ts +2 -2
  41. package/src/server/Converters.ts +4 -4
  42. package/src/server/WebSocketControllerHandler.ts +12 -9
  43. package/src/tsconfig.json +9 -3
  44. package/src/types/CommandHandler.ts +2 -4
  45. 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 (30 seconds, same as Python server)
36
- const POLLING_INTERVAL_MS = 30_000;
35
+ // Polling interval in milliseconds (60 seconds)
36
+ const POLLING_INTERVAL_MS = 60_000;
37
37
 
38
- // Maximum initial delay in milliseconds (random 0-30s to stagger startup)
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
- await this.#attributeReader.handleReadAttributes(nodeId, paths, true);
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 Object.entries(nodeDetails.attributes)) {
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
- if (config.nextNodeId <= lastNodeId) {
127
- const newNextNodeId = lastNodeId + 10;
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
  );
@@ -15,7 +15,7 @@ import {
15
15
  EventModel,
16
16
  FeatureMap,
17
17
  GeneratedCommandList,
18
- MatterModel,
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
- MatterModel.standard.clusters.forEach(cluster => {
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; // formally wrong, should be bigint
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", "")
@@ -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[0], clusterModel));
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[0], clusterModel));
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[0], clusterModel, tagBased))
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[0]));
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
- ? convertMatterToWebSocketTagBased(event.data, eventModel, clusterData?.model)
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<any>; enableListeners?: boolean }> {
381
+ ): Promise<{ response: ErrorResultMessage | SuccessResultMessage; enableListeners?: boolean }> {
383
382
  let messageId: string | undefined;
384
383
  try {
385
- logger.debug(`[${connId}] Received WebSocket request`, () => data);
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 request (${command}) handled`, messageId);
493
+ logger.debug(`[${connId}] WebSocket response (${command})`, messageId);
495
494
  } else {
496
- logger.debug(`[${connId}] WebSocket request (${command}) handled`, messageId, result);
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({ nextNodeId: nextNodeId + 1 });
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({ nextNodeId: nextNodeId + 1 });
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
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "extends": "../../tools/tsc/tsconfig.lib.json",
3
3
  "compilerOptions": {
4
- "types": ["globals"]
4
+ "types": [
5
+ "globals"
6
+ ]
5
7
  },
6
- "references": []
7
- }
8
+ "references": [
9
+ {
10
+ "path": "../../ws-client/src"
11
+ }
12
+ ]
13
+ }
@@ -211,11 +211,9 @@ export type OpenCommissioningWindowResponse = {
211
211
  qrCode: string;
212
212
  };
213
213
 
214
- import { AttributesData, MatterNode } from "./WebSocketMessageTypes.js";
214
+ import type { AttributesData, MatterNodeData } from "@matter-server/ws-client";
215
215
 
216
- /** MatterNode details for WebSocket API - re-export from WebSocketMessageTypes */
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.