@project-chip/matter.js 0.11.0-alpha.0-20240926-407400ecb → 0.11.0-alpha.0-20241002-e7b377c34

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 (119) hide show
  1. package/dist/cjs/CommissioningController.d.ts +17 -35
  2. package/dist/cjs/CommissioningController.d.ts.map +1 -1
  3. package/dist/cjs/CommissioningController.js +37 -5
  4. package/dist/cjs/CommissioningController.js.map +1 -1
  5. package/dist/cjs/CommissioningServer.d.ts.map +1 -1
  6. package/dist/cjs/CommissioningServer.js +28 -13
  7. package/dist/cjs/CommissioningServer.js.map +1 -1
  8. package/dist/cjs/MatterController.d.ts +32 -70
  9. package/dist/cjs/MatterController.d.ts.map +1 -1
  10. package/dist/cjs/MatterController.js +138 -579
  11. package/dist/cjs/MatterController.js.map +2 -2
  12. package/dist/cjs/PaseCommissioner.d.ts.map +1 -1
  13. package/dist/cjs/PaseCommissioner.js +9 -10
  14. package/dist/cjs/PaseCommissioner.js.map +2 -2
  15. package/dist/cjs/cluster/server/AccessControlServer.d.ts.map +1 -1
  16. package/dist/cjs/cluster/server/AccessControlServer.js +4 -2
  17. package/dist/cjs/cluster/server/AccessControlServer.js.map +1 -1
  18. package/dist/cjs/cluster/server/AdministratorCommissioningServer.d.ts.map +1 -1
  19. package/dist/cjs/cluster/server/AdministratorCommissioningServer.js +1 -1
  20. package/dist/cjs/cluster/server/AdministratorCommissioningServer.js.map +1 -1
  21. package/dist/cjs/cluster/server/ClusterServer.d.ts.map +1 -1
  22. package/dist/cjs/cluster/server/ClusterServer.js +3 -0
  23. package/dist/cjs/cluster/server/ClusterServer.js.map +1 -1
  24. package/dist/cjs/cluster/server/ClusterServerTypes.d.ts +5 -5
  25. package/dist/cjs/cluster/server/ClusterServerTypes.d.ts.map +1 -1
  26. package/dist/cjs/cluster/server/OperationalCredentialsServer.d.ts.map +1 -1
  27. package/dist/cjs/cluster/server/OperationalCredentialsServer.js +9 -10
  28. package/dist/cjs/cluster/server/OperationalCredentialsServer.js.map +1 -1
  29. package/dist/cjs/compat/interaction.d.ts +3 -2
  30. package/dist/cjs/compat/interaction.d.ts.map +1 -1
  31. package/dist/cjs/compat/interaction.js +8 -6
  32. package/dist/cjs/compat/interaction.js.map +1 -1
  33. package/dist/cjs/compat/protocol.d.ts +1 -1
  34. package/dist/cjs/compat/protocol.d.ts.map +1 -1
  35. package/dist/cjs/compat/protocol.js +1 -2
  36. package/dist/cjs/compat/protocol.js.map +1 -1
  37. package/dist/cjs/compat/securechannel.d.ts +2 -1
  38. package/dist/cjs/compat/securechannel.d.ts.map +1 -1
  39. package/dist/cjs/compat/securechannel.js +4 -3
  40. package/dist/cjs/compat/securechannel.js.map +1 -1
  41. package/dist/cjs/compat/util.d.ts +1 -1
  42. package/dist/cjs/compat/util.d.ts.map +1 -1
  43. package/dist/cjs/compat/util.js +1 -1
  44. package/dist/cjs/device/LegacyInteractionServer.d.ts +2 -2
  45. package/dist/cjs/device/LegacyInteractionServer.d.ts.map +1 -1
  46. package/dist/cjs/device/LegacyInteractionServer.js +3 -4
  47. package/dist/cjs/device/LegacyInteractionServer.js.map +1 -1
  48. package/dist/cjs/device/PairedNode.d.ts +1 -2
  49. package/dist/cjs/device/PairedNode.d.ts.map +1 -1
  50. package/dist/cjs/device/PairedNode.js +2 -3
  51. package/dist/cjs/device/PairedNode.js.map +1 -1
  52. package/dist/esm/CommissioningController.d.ts +17 -35
  53. package/dist/esm/CommissioningController.d.ts.map +1 -1
  54. package/dist/esm/CommissioningController.js +41 -5
  55. package/dist/esm/CommissioningController.js.map +1 -1
  56. package/dist/esm/CommissioningServer.d.ts.map +1 -1
  57. package/dist/esm/CommissioningServer.js +28 -13
  58. package/dist/esm/CommissioningServer.js.map +1 -1
  59. package/dist/esm/MatterController.d.ts +32 -70
  60. package/dist/esm/MatterController.d.ts.map +1 -1
  61. package/dist/esm/MatterController.js +144 -601
  62. package/dist/esm/MatterController.js.map +2 -2
  63. package/dist/esm/PaseCommissioner.d.ts.map +1 -1
  64. package/dist/esm/PaseCommissioner.js +12 -11
  65. package/dist/esm/PaseCommissioner.js.map +1 -1
  66. package/dist/esm/cluster/server/AccessControlServer.d.ts.map +1 -1
  67. package/dist/esm/cluster/server/AccessControlServer.js +4 -2
  68. package/dist/esm/cluster/server/AccessControlServer.js.map +1 -1
  69. package/dist/esm/cluster/server/AdministratorCommissioningServer.d.ts.map +1 -1
  70. package/dist/esm/cluster/server/AdministratorCommissioningServer.js +1 -1
  71. package/dist/esm/cluster/server/AdministratorCommissioningServer.js.map +1 -1
  72. package/dist/esm/cluster/server/ClusterServer.d.ts.map +1 -1
  73. package/dist/esm/cluster/server/ClusterServer.js +3 -0
  74. package/dist/esm/cluster/server/ClusterServer.js.map +1 -1
  75. package/dist/esm/cluster/server/ClusterServerTypes.d.ts +5 -5
  76. package/dist/esm/cluster/server/ClusterServerTypes.d.ts.map +1 -1
  77. package/dist/esm/cluster/server/OperationalCredentialsServer.d.ts.map +1 -1
  78. package/dist/esm/cluster/server/OperationalCredentialsServer.js +9 -11
  79. package/dist/esm/cluster/server/OperationalCredentialsServer.js.map +1 -1
  80. package/dist/esm/compat/interaction.d.ts +3 -2
  81. package/dist/esm/compat/interaction.d.ts.map +1 -1
  82. package/dist/esm/compat/interaction.js +14 -15
  83. package/dist/esm/compat/interaction.js.map +1 -1
  84. package/dist/esm/compat/protocol.d.ts +1 -1
  85. package/dist/esm/compat/protocol.d.ts.map +1 -1
  86. package/dist/esm/compat/protocol.js +2 -4
  87. package/dist/esm/compat/protocol.js.map +1 -1
  88. package/dist/esm/compat/securechannel.d.ts +2 -1
  89. package/dist/esm/compat/securechannel.d.ts.map +1 -1
  90. package/dist/esm/compat/securechannel.js +1 -3
  91. package/dist/esm/compat/securechannel.js.map +1 -1
  92. package/dist/esm/compat/util.d.ts +1 -1
  93. package/dist/esm/compat/util.d.ts.map +1 -1
  94. package/dist/esm/compat/util.js +2 -2
  95. package/dist/esm/compat/util.js.map +1 -1
  96. package/dist/esm/device/LegacyInteractionServer.d.ts +2 -2
  97. package/dist/esm/device/LegacyInteractionServer.d.ts.map +1 -1
  98. package/dist/esm/device/LegacyInteractionServer.js +3 -4
  99. package/dist/esm/device/LegacyInteractionServer.js.map +1 -1
  100. package/dist/esm/device/PairedNode.d.ts +1 -2
  101. package/dist/esm/device/PairedNode.d.ts.map +1 -1
  102. package/dist/esm/device/PairedNode.js +1 -1
  103. package/dist/esm/device/PairedNode.js.map +1 -1
  104. package/package.json +8 -8
  105. package/src/CommissioningController.ts +56 -47
  106. package/src/CommissioningServer.ts +27 -12
  107. package/src/MatterController.ts +177 -816
  108. package/src/PaseCommissioner.ts +11 -11
  109. package/src/cluster/server/AccessControlServer.ts +2 -0
  110. package/src/cluster/server/AdministratorCommissioningServer.ts +1 -1
  111. package/src/cluster/server/ClusterServer.ts +4 -0
  112. package/src/cluster/server/ClusterServerTypes.ts +5 -5
  113. package/src/cluster/server/OperationalCredentialsServer.ts +13 -13
  114. package/src/compat/interaction.ts +14 -13
  115. package/src/compat/protocol.ts +2 -3
  116. package/src/compat/securechannel.ts +2 -3
  117. package/src/compat/util.ts +1 -1
  118. package/src/device/LegacyInteractionServer.ts +4 -4
  119. package/src/device/PairedNode.ts +1 -1
@@ -11,65 +11,44 @@
11
11
  */
12
12
 
13
13
  import { GeneralCommissioning } from "#clusters";
14
+ import { NodeCommissioningOptions } from "#CommissioningController.js";
14
15
  import {
15
16
  CRYPTO_SYMMETRIC_KEY_LENGTH,
16
- Channel,
17
+ ChannelType,
17
18
  Construction,
18
19
  Crypto,
19
20
  ImplementationError,
20
21
  Logger,
21
- NetInterface,
22
- NoProviderError,
23
- NoResponseTimeoutError,
24
- ServerAddress,
22
+ NetInterfaceSet,
25
23
  ServerAddressIp,
26
24
  StorageBackendMemory,
27
25
  StorageContext,
28
26
  StorageManager,
29
27
  SupportedStorageTypes,
30
- Time,
31
- Timer,
32
- anyPromise,
33
- createPromise,
34
- isIPv6,
35
28
  serverAddressToString,
36
29
  } from "#general";
37
- import { Specification } from "#model";
38
30
  import {
39
- Ble,
40
- CaseClient,
41
31
  ChannelManager,
42
32
  ClusterClient,
43
- CommissionableDevice,
44
33
  CommissioningError,
45
- CommissioningSuccessfullyFinished,
46
34
  ControllerCommissioner,
47
- ControllerCommissioningOptions,
48
- ControllerDiscovery,
49
35
  DiscoveryData,
50
- DiscoveryError,
36
+ DiscoveryOptions,
51
37
  ExchangeManager,
52
- ExchangeProvider,
53
38
  Fabric,
54
39
  FabricBuilder,
55
40
  FabricJsonObject,
56
- InteractionClient,
57
- MdnsScanner,
58
- MessageChannel,
59
- NoChannelError,
60
- PairRetransmissionLimitReachedError,
61
- PaseClient,
41
+ FabricManager,
42
+ NodeDiscoveryType,
43
+ OperationalPeer,
44
+ PeerCommissioningOptions,
45
+ PeerSet,
46
+ PeerStore,
62
47
  ResumptionRecord,
63
48
  RetransmissionLimitReachedError,
64
49
  RootCertificateManager,
65
- SECURE_CHANNEL_PROTOCOL_ID,
66
- SESSION_ACTIVE_INTERVAL_MS,
67
- SESSION_ACTIVE_THRESHOLD_MS,
68
- SESSION_IDLE_INTERVAL_MS,
69
- Scanner,
70
- SessionContext,
50
+ ScannerSet,
71
51
  SessionManager,
72
- SessionParameters,
73
52
  StatusReportOnlySecureChannelProtocol,
74
53
  } from "#protocol";
75
54
  import {
@@ -87,7 +66,6 @@ import {
87
66
  TypeFromSchema,
88
67
  VendorId,
89
68
  } from "#types";
90
- import { NodeCommissioningOptions } from "./CommissioningController.js";
91
69
 
92
70
  const TlvCommissioningSuccessFailureResponse = TlvObject({
93
71
  /** Contain the result of the operation. */
@@ -104,47 +82,29 @@ export type CommissionedNodeDetails = {
104
82
  basicInformationData?: Record<string, SupportedStorageTypes>;
105
83
  };
106
84
 
107
- type DiscoveryOptions = {
108
- discoveryType?: NodeDiscoveryType;
109
- timeoutSeconds?: number;
110
- discoveryData?: DiscoveryData;
111
- };
112
-
85
+ const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1);
113
86
  const DEFAULT_FABRIC_INDEX = FabricIndex(1);
114
87
  const DEFAULT_FABRIC_ID = FabricId(1);
115
- const DEFAULT_ADMIN_VENDOR_ID = VendorId(0xfff1);
116
-
117
- const RECONNECTION_POLLING_INTERVAL_MS = 600_000; // 10 minutes
118
- const RETRANSMISSION_DISCOVERY_TIMEOUT_MS = 5_000;
119
88
 
120
89
  const CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE = 3;
121
90
  const CONTROLLER_MAX_PATHS_PER_INVOKE = 10;
122
91
 
123
92
  const logger = Logger.get("MatterController");
124
93
 
125
- export enum NodeDiscoveryType {
126
- /** No discovery is done, in calls means that only known addresses are tried. */
127
- None = 0,
128
-
129
- /** Retransmission discovery means that we ignore known addresses and start a query for 5s. */
130
- RetransmissionDiscovery = 1,
94
+ // Operational peer extended with basic information as required for conversion to CommissionedNodeDetails
95
+ type CommissionedPeer = OperationalPeer & { basicInformationData?: Record<string, SupportedStorageTypes> };
131
96
 
132
- /** Timed discovery means that the device is discovered for a defined timeframe, including known addresses. */
133
- TimedDiscovery = 2,
97
+ // Backward-compatible persistence record for nodes
98
+ type StoredOperationalPeer = [NodeId, CommissionedNodeDetails];
134
99
 
135
- /** Full discovery means that the device is discovered until it is found, excluding known addresses. */
136
- FullDiscovery = 3,
137
- }
138
-
139
- export class MatterController implements SessionContext {
100
+ export class MatterController {
140
101
  public static async create(options: {
141
102
  sessionStorage: StorageContext;
142
103
  rootCertificateStorage: StorageContext;
143
104
  fabricStorage: StorageContext;
144
105
  nodesStorage: StorageContext;
145
- mdnsScanner: MdnsScanner;
146
- netInterfaceIpv4: NetInterface | undefined;
147
- netInterfaceIpv6: NetInterface;
106
+ scanners: ScannerSet;
107
+ netInterfaces: NetInterfaceSet;
148
108
  sessionClosedCallback?: (peerNodeId: NodeId) => void;
149
109
  adminVendorId?: VendorId;
150
110
  adminFabricId?: FabricId;
@@ -156,11 +116,10 @@ export class MatterController implements SessionContext {
156
116
  rootCertificateStorage,
157
117
  fabricStorage,
158
118
  nodesStorage,
159
- mdnsScanner,
160
- netInterfaceIpv4,
161
- netInterfaceIpv6,
119
+ scanners,
120
+ netInterfaces,
162
121
  sessionClosedCallback,
163
- adminVendorId = VendorId(DEFAULT_ADMIN_VENDOR_ID),
122
+ adminVendorId,
164
123
  adminFabricId = FabricId(DEFAULT_FABRIC_ID),
165
124
  adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX),
166
125
  caseAuthenticatedTags,
@@ -176,12 +135,10 @@ export class MatterController implements SessionContext {
176
135
  sessionStorage,
177
136
  fabricStorage,
178
137
  nodesStorage,
179
- mdnsScanner,
180
- netInterfaceIpv4,
181
- netInterfaceIpv6,
138
+ scanners,
139
+ netInterfaces,
182
140
  certificateManager,
183
141
  fabric,
184
- adminVendorId: fabric.rootVendorId,
185
142
  sessionClosedCallback,
186
143
  });
187
144
  } else {
@@ -191,7 +148,7 @@ export class MatterController implements SessionContext {
191
148
  .setRootCert(certificateManager.rootCert)
192
149
  .setRootNodeId(rootNodeId)
193
150
  .setIdentityProtectionKey(ipkValue)
194
- .setRootVendorId(adminVendorId);
151
+ .setRootVendorId(adminVendorId ?? DEFAULT_ADMIN_VENDOR_ID);
195
152
  fabricBuilder.setOperationalCert(
196
153
  certificateManager.generateNoc(
197
154
  fabricBuilder.publicKey,
@@ -206,12 +163,10 @@ export class MatterController implements SessionContext {
206
163
  sessionStorage,
207
164
  fabricStorage,
208
165
  nodesStorage,
209
- mdnsScanner,
210
- netInterfaceIpv4,
211
- netInterfaceIpv6,
166
+ scanners,
167
+ netInterfaces,
212
168
  certificateManager,
213
169
  fabric,
214
- adminVendorId,
215
170
  sessionClosedCallback,
216
171
  });
217
172
  }
@@ -222,27 +177,15 @@ export class MatterController implements SessionContext {
222
177
  public static async createAsPaseCommissioner(options: {
223
178
  rootCertificateData: RootCertificateManager.Data;
224
179
  fabricData: FabricJsonObject;
225
- mdnsScanner?: MdnsScanner;
226
- netInterfaceIpv4?: NetInterface | undefined;
227
- netInterfaceIpv6?: NetInterface;
180
+ scanners: ScannerSet;
181
+ netInterfaces: NetInterfaceSet;
228
182
  sessionClosedCallback?: (peerNodeId: NodeId) => void;
229
183
  }): Promise<MatterController> {
230
- const {
231
- rootCertificateData,
232
- fabricData,
233
- mdnsScanner,
234
- netInterfaceIpv4,
235
- netInterfaceIpv6,
236
- sessionClosedCallback,
237
- } = options;
238
-
239
- // Verify BLE initialization
240
- try {
241
- Ble.get();
242
- } catch (error) {
243
- NoProviderError.accept(error);
184
+ const { rootCertificateData, fabricData, scanners, netInterfaces, sessionClosedCallback } = options;
244
185
 
245
- if (!mdnsScanner || !netInterfaceIpv6) {
186
+ // Verify an appropriate network interface is available
187
+ if (!netInterfaces.hasInterfaceFor(ChannelType.BLE)) {
188
+ if (!scanners.hasScannerFor(ChannelType.UDP) || !netInterfaces.hasInterfaceFor(ChannelType.UDP, "::")) {
246
189
  throw new ImplementationError(
247
190
  "Ble must be initialized to create a Sub Commissioner without an IP network!",
248
191
  );
@@ -263,12 +206,10 @@ export class MatterController implements SessionContext {
263
206
  const controller = new MatterController({
264
207
  sessionStorage,
265
208
  nodesStorage,
266
- mdnsScanner,
267
- netInterfaceIpv4,
268
- netInterfaceIpv6,
209
+ scanners,
210
+ netInterfaces,
269
211
  certificateManager,
270
212
  fabric,
271
- adminVendorId: fabric.rootVendorId,
272
213
  sessionClosedCallback,
273
214
  });
274
215
  await controller.construction;
@@ -276,33 +217,20 @@ export class MatterController implements SessionContext {
276
217
  }
277
218
 
278
219
  readonly sessionManager: SessionManager;
220
+ private readonly netInterfaces = new NetInterfaceSet();
279
221
  private readonly channelManager = new ChannelManager(CONTROLLER_CONNECTIONS_PER_FABRIC_AND_NODE);
280
222
  private readonly exchangeManager: ExchangeManager;
281
- private readonly paseClient = new PaseClient();
282
- private readonly caseClient = new CaseClient();
283
- private netInterfaceBle: NetInterface | undefined;
284
- private bleScanner: Scanner | undefined;
285
- private readonly commissionedNodes = new Map<NodeId, CommissionedNodeDetails>();
223
+ private readonly peers: PeerSet;
224
+ private readonly commissioner: ControllerCommissioner;
286
225
  #construction: Construction<MatterController>;
287
226
 
288
227
  readonly sessionStorage: StorageContext;
289
228
  readonly fabricStorage?: StorageContext;
290
- readonly nodesStorage: StorageContext;
291
- private readonly mdnsScanner: MdnsScanner | undefined;
292
- private readonly netInterfaceIpv4: NetInterface | undefined;
293
- private readonly netInterfaceIpv6: NetInterface | undefined;
229
+ readonly nodesStore: CommissionedNodeStore;
230
+ private readonly scanners: ScannerSet;
294
231
  private readonly certificateManager: RootCertificateManager;
295
232
  private readonly fabric: Fabric;
296
- private readonly adminVendorId: VendorId;
297
233
  private readonly sessionClosedCallback?: (peerNodeId: NodeId) => void;
298
- readonly #runningNodeDiscoveries = new Map<
299
- NodeId,
300
- {
301
- type: NodeDiscoveryType;
302
- promises?: (() => Promise<MessageChannel>)[];
303
- timer?: Timer;
304
- }
305
- >();
306
234
 
307
235
  get construction() {
308
236
  return this.#construction;
@@ -312,68 +240,75 @@ export class MatterController implements SessionContext {
312
240
  sessionStorage: StorageContext;
313
241
  fabricStorage?: StorageContext;
314
242
  nodesStorage: StorageContext;
315
- mdnsScanner?: MdnsScanner;
316
- netInterfaceIpv4?: NetInterface;
317
- netInterfaceIpv6?: NetInterface;
243
+ scanners: ScannerSet;
244
+ netInterfaces: NetInterfaceSet;
318
245
  certificateManager: RootCertificateManager;
319
246
  fabric: Fabric;
320
- adminVendorId: VendorId;
321
247
  sessionClosedCallback?: (peerNodeId: NodeId) => void;
322
248
  }) {
323
249
  const {
324
250
  sessionStorage,
325
251
  fabricStorage,
326
252
  nodesStorage,
327
- mdnsScanner,
328
- netInterfaceIpv4,
329
- netInterfaceIpv6,
253
+ scanners,
254
+ netInterfaces,
330
255
  certificateManager,
331
256
  fabric,
332
257
  sessionClosedCallback,
333
- adminVendorId,
334
258
  } = options;
335
259
  this.sessionStorage = sessionStorage;
336
260
  this.fabricStorage = fabricStorage;
337
- this.nodesStorage = nodesStorage;
338
- this.mdnsScanner = mdnsScanner;
339
- this.netInterfaceIpv4 = netInterfaceIpv4;
340
- this.netInterfaceIpv6 = netInterfaceIpv6;
261
+ this.scanners = scanners;
262
+ this.netInterfaces = netInterfaces;
341
263
  this.certificateManager = certificateManager;
342
264
  this.fabric = fabric;
343
265
  this.sessionClosedCallback = sessionClosedCallback;
344
- this.adminVendorId = adminVendorId;
345
266
 
346
- this.sessionManager = new SessionManager(this, sessionStorage);
347
- this.sessionManager.sessionClosed.on(async session => {
348
- if (!session.closingAfterExchangeFinished) {
349
- // Delayed closing is executed when exchange is closed
350
- await this.exchangeManager.closeSession(session);
351
- }
267
+ const fabricManager = new FabricManager();
268
+ fabricManager.addFabric(fabric);
269
+
270
+ this.sessionManager = new SessionManager({
271
+ fabrics: fabricManager,
272
+ storage: sessionStorage,
273
+ parameters: {
274
+ maxPathsPerInvoke: CONTROLLER_MAX_PATHS_PER_INVOKE,
275
+ },
276
+ });
277
+ this.sessionManager.sessions.deleted.on(async session => {
352
278
  this.sessionClosedCallback?.(session.peerNodeId);
353
279
  });
354
280
 
355
- this.exchangeManager = new ExchangeManager(this.sessionManager, this.channelManager);
281
+ this.exchangeManager = new ExchangeManager({
282
+ sessionManager: this.sessionManager,
283
+ channelManager: this.channelManager,
284
+ transportInterfaces: this.netInterfaces,
285
+ });
356
286
  this.exchangeManager.addProtocolHandler(new StatusReportOnlySecureChannelProtocol());
357
287
 
358
- if (netInterfaceIpv4 !== undefined) {
359
- this.addTransportInterface(netInterfaceIpv4);
360
- }
361
- if (netInterfaceIpv6 !== undefined) {
362
- this.addTransportInterface(netInterfaceIpv6);
363
- }
288
+ // Adapts the historical storage format for MatterController to OperationalPeer objects
289
+ this.nodesStore = new CommissionedNodeStore(nodesStorage, fabric);
364
290
 
365
- this.#construction = Construction(this, async () => {
366
- // If controller has a stored operational server address, use it, irrelevant what was passed in the constructor
367
- if (await this.nodesStorage.has("commissionedNodes")) {
368
- const commissionedNodes =
369
- await this.nodesStorage.get<[NodeId, CommissionedNodeDetails][]>("commissionedNodes");
370
- this.commissionedNodes.clear();
371
- for (const [nodeId, details] of commissionedNodes) {
372
- this.commissionedNodes.set(nodeId, details);
373
- }
374
- }
291
+ this.nodesStore.peers = this.peers = new PeerSet({
292
+ sessions: this.sessionManager,
293
+ channels: this.channelManager,
294
+ exchanges: this.exchangeManager,
295
+ scanners: this.scanners,
296
+ netInterfaces: this.netInterfaces,
297
+ store: this.nodesStore,
298
+ });
299
+
300
+ this.commissioner = new ControllerCommissioner({
301
+ peers: this.peers,
302
+ scanners: this.scanners,
303
+ netInterfaces: this.netInterfaces,
304
+ exchanges: this.exchangeManager,
305
+ sessions: this.sessionManager,
306
+ certificates: this.certificateManager,
307
+ });
375
308
 
376
- await this.sessionManager.initFromStorage([this.fabric]);
309
+ this.#construction = Construction(this, async () => {
310
+ await this.peers.construction.ready;
311
+ await this.sessionManager.construction.ready;
377
312
  });
378
313
  }
379
314
 
@@ -393,53 +328,14 @@ export class MatterController implements SessionContext {
393
328
  return [this.fabric];
394
329
  }
395
330
 
396
- /** Our own client/controller session parameters. */
397
- get sessionParameters(): SessionParameters {
398
- return {
399
- idleIntervalMs: SESSION_IDLE_INTERVAL_MS,
400
- activeIntervalMs: SESSION_ACTIVE_INTERVAL_MS,
401
- activeThresholdMs: SESSION_ACTIVE_THRESHOLD_MS,
402
- dataModelRevision: Specification.DATA_MODEL_REVISION,
403
- interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
404
- specificationVersion: Specification.SPECIFICATION_VERSION,
405
- maxPathsPerInvoke: CONTROLLER_MAX_PATHS_PER_INVOKE,
406
- };
407
- }
408
-
409
- public addTransportInterface(netInterface: NetInterface) {
410
- this.exchangeManager.addTransportInterface(netInterface);
411
- }
412
-
413
331
  public collectScanners(
414
332
  discoveryCapabilities: TypeFromPartialBitSchema<typeof DiscoveryCapabilitiesBitmap> = { onIpNetwork: true },
415
333
  ) {
416
- const scannersToUse = new Array<Scanner>();
417
-
418
- if (this.mdnsScanner !== undefined) {
419
- scannersToUse.push(this.mdnsScanner); // Scan always on IP Network if available
420
- }
421
-
422
- if (discoveryCapabilities.ble) {
423
- if (this.bleScanner === undefined) {
424
- let ble: Ble;
425
- try {
426
- ble = Ble.get();
427
- this.netInterfaceBle = ble.getBleCentralInterface();
428
- this.addTransportInterface(this.netInterfaceBle);
429
-
430
- this.bleScanner = ble.getBleScanner();
431
- } catch (error) {
432
- NoProviderError.accept(error);
433
-
434
- logger.warn("BLE is not supported on this platform. The device to commission might not be found!");
435
- }
436
- }
437
- // If we have an BLE Scanner then we use it
438
- if (this.bleScanner !== undefined) {
439
- scannersToUse.push(this.bleScanner);
440
- }
441
- }
442
- return scannersToUse;
334
+ // Note we always scan via MDNS if available
335
+ return this.scanners.filter(
336
+ scanner =>
337
+ scanner.type === ChannelType.UDP || (discoveryCapabilities.ble && scanner.type === ChannelType.BLE),
338
+ );
443
339
  }
444
340
 
445
341
  /**
@@ -457,259 +353,35 @@ export class MatterController implements SessionContext {
457
353
  options: NodeCommissioningOptions,
458
354
  completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>,
459
355
  ): Promise<NodeId> {
460
- const {
461
- commissioning: commissioningOptions = {
462
- regulatoryLocation: GeneralCommissioning.RegulatoryLocationType.Outdoor, // Set to the most restrictive if relevant
463
- regulatoryCountryCode: "XX",
464
- },
465
- discovery: { timeoutSeconds = 30 },
466
- passcode,
467
- } = options;
468
- const commissionableDevice =
469
- "commissionableDevice" in options.discovery ? options.discovery.commissionableDevice : undefined;
470
- let {
471
- discovery: { discoveryCapabilities = {}, knownAddress },
472
- } = options;
473
- let identifierData = "identifierData" in options.discovery ? options.discovery.identifierData : {};
474
-
475
- if (this.mdnsScanner !== undefined && this.netInterfaceIpv6 !== undefined) {
476
- discoveryCapabilities.onIpNetwork = true; // We always discover on network as defined by specs
477
- }
478
- if (commissionableDevice !== undefined) {
479
- let { addresses } = commissionableDevice;
480
- if (discoveryCapabilities.ble === true) {
481
- discoveryCapabilities = { onIpNetwork: true, ble: addresses.some(address => address.type === "ble") };
482
- } else if (discoveryCapabilities.onIpNetwork === true) {
483
- // do not use BLE if not specified, even if existing
484
- addresses = addresses.filter(address => address.type !== "ble");
485
- }
486
- addresses.sort(a => (a.type === "udp" ? -1 : 1)); // Sort addresses to use UDP first
487
- knownAddress = addresses[0];
488
- if ("instanceId" in commissionableDevice && commissionableDevice.instanceId !== undefined) {
489
- // it is an UDP discovery
490
- identifierData = { instanceId: commissionableDevice.instanceId as string };
491
- } else {
492
- identifierData = { longDiscriminator: commissionableDevice.D };
493
- }
494
- }
495
-
496
- const scannersToUse = this.collectScanners(discoveryCapabilities);
497
-
498
- logger.info(
499
- `Commissioning device with identifier ${Logger.toJSON(identifierData)} and ${
500
- scannersToUse.length
501
- } scanners and knownAddress ${Logger.toJSON(knownAddress)}`,
502
- );
503
-
504
- // If we have a known address we try this first before we discover the device
505
- let paseSecureChannel: MessageChannel | undefined;
506
- let discoveryData: DiscoveryData | undefined;
356
+ const commissioningOptions: PeerCommissioningOptions = {
357
+ ...options.commissioning,
358
+ fabric: this.fabric,
359
+ discovery: options.discovery,
360
+ passcode: options.passcode,
361
+ };
507
362
 
508
- // If we have a last known address, try this first
509
- if (knownAddress !== undefined) {
510
- try {
511
- paseSecureChannel = await this.initializePaseSecureChannel(knownAddress, passcode);
512
- } catch (error) {
513
- NoResponseTimeoutError.accept(error);
514
- }
363
+ if (completeCommissioningCallback) {
364
+ commissioningOptions.performCaseCommissioning = async (peerAddress, discoveryData) => {
365
+ const result = await completeCommissioningCallback(peerAddress.nodeId, discoveryData);
366
+ if (!result) {
367
+ throw new RetransmissionLimitReachedError("Device could not be discovered");
368
+ }
369
+ };
515
370
  }
516
- if (paseSecureChannel === undefined) {
517
- const discoveredDevices = await ControllerDiscovery.discoverDeviceAddressesByIdentifier(
518
- scannersToUse,
519
- identifierData,
520
- timeoutSeconds,
521
- );
522
371
 
523
- const { result } = await ControllerDiscovery.iterateServerAddresses(
524
- discoveredDevices,
525
- NoResponseTimeoutError,
526
- async () =>
527
- scannersToUse.flatMap(scanner => scanner.getDiscoveredCommissionableDevices(identifierData)),
528
- async (address, device) => {
529
- const channel = await this.initializePaseSecureChannel(address, passcode, device);
530
- discoveryData = device;
531
- return channel;
532
- },
533
- );
372
+ const address = await this.commissioner.commission(commissioningOptions);
534
373
 
535
- // Pairing was successful, so store the address and assign the established secure channel
536
- paseSecureChannel = result;
537
- }
374
+ await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
538
375
 
539
- return await this.commissionDevice(
540
- paseSecureChannel,
541
- commissioningOptions,
542
- discoveryData,
543
- completeCommissioningCallback,
544
- );
376
+ return address.nodeId;
545
377
  }
546
378
 
547
379
  async disconnect(nodeId: NodeId) {
548
- await this.sessionManager.removeAllSessionsForNode(nodeId, true);
549
- await this.channelManager.removeAllNodeChannels(this.fabric, nodeId);
380
+ return this.peers.disconnect(this.fabric.addressOf(nodeId));
550
381
  }
551
382
 
552
383
  async removeNode(nodeId: NodeId) {
553
- logger.info(`Removing commissioned node ${nodeId} from controller.`);
554
- await this.sessionManager.removeAllSessionsForNode(nodeId);
555
- await this.sessionManager.removeResumptionRecord(nodeId);
556
- await this.channelManager.removeAllNodeChannels(this.fabric, nodeId);
557
- this.commissionedNodes.delete(nodeId);
558
- await this.storeCommissionedNodes();
559
- }
560
-
561
- /**
562
- * Method to start commission process with a PASE pairing.
563
- * If this not successful and throws an RetransmissionLimitReachedError the address is invalid or the passcode
564
- * is wrong.
565
- */
566
- private async initializePaseSecureChannel(
567
- address: ServerAddress,
568
- passcode: number,
569
- device?: CommissionableDevice,
570
- ): Promise<MessageChannel> {
571
- let paseChannel: Channel<Uint8Array>;
572
- if (device !== undefined) {
573
- logger.info(`Commissioning device`, MdnsScanner.discoveryDataDiagnostics(device));
574
- }
575
- if (address.type === "udp") {
576
- const { ip } = address;
577
-
578
- const isIpv6Address = isIPv6(ip);
579
- const paseInterface = isIpv6Address ? this.netInterfaceIpv6 : this.netInterfaceIpv4;
580
- if (paseInterface === undefined) {
581
- // mainly IPv6 address when IPv4 is disabled
582
- throw new PairRetransmissionLimitReachedError(
583
- `IPv${isIpv6Address ? "6" : "4"} interface not initialized. Cannot use ${ip} for commissioning.`,
584
- );
585
- }
586
- paseChannel = await paseInterface.openChannel(address);
587
- } else {
588
- if (this.netInterfaceBle === undefined) {
589
- throw new PairRetransmissionLimitReachedError(
590
- `BLE interface not initialized. Cannot use ${address.peripheralAddress} for commissioning.`,
591
- );
592
- }
593
- // TODO Have a Timeout mechanism here for connections
594
- paseChannel = await this.netInterfaceBle.openChannel(address);
595
- }
596
-
597
- // Do PASE paring
598
- const unsecureSession = this.sessionManager.createUnsecureSession({
599
- // Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
600
- sessionParameters: {
601
- idleIntervalMs: device?.SII,
602
- activeIntervalMs: device?.SAI,
603
- activeThresholdMs: device?.SAT,
604
- },
605
- isInitiator: true,
606
- });
607
- const paseUnsecureMessageChannel = new MessageChannel(paseChannel, unsecureSession);
608
- const paseExchange = this.exchangeManager.initiateExchangeWithChannel(
609
- paseUnsecureMessageChannel,
610
- SECURE_CHANNEL_PROTOCOL_ID,
611
- );
612
-
613
- let paseSecureSession;
614
- try {
615
- paseSecureSession = await this.paseClient.pair(this, paseExchange, passcode);
616
- } catch (e) {
617
- // Close the exchange and rethrow
618
- await paseExchange.close();
619
- throw e;
620
- }
621
-
622
- await unsecureSession.destroy();
623
- return new MessageChannel(paseChannel, paseSecureSession);
624
- }
625
-
626
- /**
627
- * Method to commission a device with a PASE secure channel. It returns the NodeId of the commissioned device on
628
- * success.
629
- */
630
- private async commissionDevice(
631
- paseSecureMessageChannel: MessageChannel,
632
- commissioningOptions: ControllerCommissioningOptions,
633
- discoveryData?: DiscoveryData,
634
- completeCommissioningCallback?: (peerNodeId: NodeId, discoveryData?: DiscoveryData) => Promise<boolean>,
635
- ): Promise<NodeId> {
636
- // TODO: Create the fabric only when needed before commissioning (to do when refactoring MatterController away)
637
- // TODO also move certificateManager and other parts into that class to get rid of them here
638
- // TODO Depending on the Error type during commissioning we can do a retry ...
639
- /*
640
- Whenever the Fail-Safe timer is armed, Commissioners and Administrators SHALL NOT consider any cluster
641
- operation to have timed-out before waiting at least 30 seconds for a valid response from the cluster server.
642
- Some commands and attributes with complex side-effects MAY require longer and have specific timing requirements
643
- stated in their respective cluster specification.
644
-
645
- In concurrent connection commissioning flow, the failure of any of the steps 2 through 10 SHALL result in the
646
- Commissioner and Commissionee returning to step 2 (device discovery and commissioning channel establishment) and
647
- repeating each step. The failure of any of the steps 11 through 15 in concurrent connection commissioning flow
648
- SHALL result in the Commissioner and Commissionee returning to step 11 (configuration of operational network
649
- information). In the case of failure of any of the steps 11 through 15 in concurrent connection commissioning
650
- flow, the Commissioner and Commissionee SHALL reuse the existing PASE-derived encryption keys over the
651
- commissioning channel and all steps up to and including step 10 are considered to have been successfully
652
- completed.
653
- In non-concurrent connection commissioning flow, the failure of any of the steps 2 through 15 SHALL result in
654
- the Commissioner and Commissionee returning to step 2 (device discovery and commissioning channel establishment)
655
- and repeating each step.
656
-
657
- Commissioners that need to restart from step 2 MAY immediately expire the fail-safe by invoking the ArmFailSafe
658
- command with an ExpiryLengthSeconds field set to 0. Otherwise, Commissioners will need to wait until the current
659
- fail-safe timer has expired for the Commissionee to begin accepting PASE again.
660
- In both concurrent connection commissioning flow and non-concurrent connection commissioning flow, the
661
- Commissionee SHALL exit Commissioning Mode after 20 failed attempts.
662
- */
663
-
664
- const peerNodeId = commissioningOptions.nodeId ?? NodeId.randomOperationalNodeId();
665
- const commissioningManager = new ControllerCommissioner(
666
- // Use the created secure session to do the commissioning
667
- new InteractionClient(new ExchangeProvider(this.exchangeManager, paseSecureMessageChannel), peerNodeId),
668
- this.certificateManager,
669
- this.fabric,
670
- commissioningOptions,
671
- peerNodeId,
672
- this.adminVendorId,
673
- async () => {
674
- // TODO Right now we always close after step 12 because we do not check for commissioning flow requirements
675
- /*
676
- In concurrent connection commissioning flow the commissioning channel SHALL terminate after
677
- successful step 15 (CommissioningComplete command invocation). In non-concurrent connection
678
- commissioning flow the commissioning channel SHALL terminate after successful step 12 (trigger
679
- joining of operational network at Commissionee). The PASE-derived encryption keys SHALL be deleted
680
- when commissioning channel terminates. The PASE session SHALL be terminated by both Commissioner and
681
- Commissionee once the CommissioningComplete command is received by the Commissionee.
682
- */
683
- await paseSecureMessageChannel.close(); // We reconnect using Case, so close PASE connection
684
-
685
- if (completeCommissioningCallback !== undefined) {
686
- if (!(await completeCommissioningCallback(peerNodeId, discoveryData))) {
687
- throw new RetransmissionLimitReachedError("Device could not be discovered");
688
- }
689
- throw new CommissioningSuccessfullyFinished();
690
- }
691
- // Look for the device broadcast over MDNS and do CASE pairing
692
- return await this.connect(peerNodeId, {
693
- discoveryType: NodeDiscoveryType.TimedDiscovery,
694
- timeoutSeconds: 120,
695
- discoveryData,
696
- }); // Wait maximum 120s to find the operational device for commissioning process
697
- },
698
- );
699
-
700
- try {
701
- await commissioningManager.executeCommissioning();
702
- } catch (error) {
703
- if (this.commissionedNodes.has(peerNodeId)) {
704
- // We might have added data for an operational address that we need to cleanup
705
- this.commissionedNodes.delete(peerNodeId);
706
- }
707
- throw error;
708
- }
709
-
710
- await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
711
-
712
- return peerNodeId;
384
+ return this.peers.delete(this.fabric.addressOf(nodeId));
713
385
  }
714
386
 
715
387
  /**
@@ -731,354 +403,45 @@ export class MatterController implements SessionContext {
731
403
  useExtendedFailSafeMessageResponseTimeout: true,
732
404
  });
733
405
  if (errorCode !== GeneralCommissioning.CommissioningError.Ok) {
734
- if (this.commissionedNodes.has(peerNodeId)) {
735
- // We might have added data for an operational address that we need to cleanup
736
- this.commissionedNodes.delete(peerNodeId);
737
- }
406
+ // We might have added data for an operational address that we need to cleanup
407
+ await this.peers.delete(this.fabric.addressOf(peerNodeId));
738
408
  throw new CommissioningError(`Commission error on commissioningComplete: ${errorCode}, ${debugText}`);
739
409
  }
740
410
  await this.fabricStorage?.set("fabric", this.fabric.toStorageObject());
741
411
  }
742
412
 
743
- handleResubmissionStarted(peerNodeId: NodeId) {
744
- if (this.#runningNodeDiscoveries.has(peerNodeId)) {
745
- // We already discover for this node, so we do not need to start a new discovery
746
- return;
747
- }
748
- this.#runningNodeDiscoveries.set(peerNodeId, { type: NodeDiscoveryType.RetransmissionDiscovery });
749
- this.mdnsScanner
750
- ?.findOperationalDevice(this.fabric, peerNodeId, RETRANSMISSION_DISCOVERY_TIMEOUT_MS, true)
751
- .catch(error => {
752
- logger.error(`Failed to discover device ${peerNodeId} after resubmission started.`, error);
753
- })
754
- .finally(() => {
755
- if (this.#runningNodeDiscoveries.get(peerNodeId)?.type === NodeDiscoveryType.RetransmissionDiscovery) {
756
- this.#runningNodeDiscoveries.delete(peerNodeId);
757
- }
758
- });
759
- }
760
-
761
- private async reconnectKnownAddress(
762
- peerNodeId: NodeId,
763
- operationalAddress: ServerAddressIp,
764
- discoveryData?: DiscoveryData,
765
- expectedProcessingTimeMs?: number,
766
- ): Promise<MessageChannel | undefined> {
767
- const { ip, port } = operationalAddress;
768
- try {
769
- logger.debug(
770
- `Resume device connection to configured server at ${ip}:${port}${expectedProcessingTimeMs !== undefined ? ` with expected processing time of ${expectedProcessingTimeMs}ms` : ""} ...`,
771
- );
772
- const channel = await this.pair(peerNodeId, operationalAddress, discoveryData, expectedProcessingTimeMs);
773
- await this.setOperationalDeviceData(peerNodeId, operationalAddress);
774
- return channel;
775
- } catch (error) {
776
- if (error instanceof NoResponseTimeoutError) {
777
- logger.debug(
778
- `Failed to resume connection to node ${peerNodeId} connection with ${ip}:${port}, discover the device ...`,
779
- error,
780
- );
781
- // We remove all sessions, this also informs the PairedNode class
782
- await this.sessionManager.removeAllSessionsForNode(peerNodeId);
783
- return undefined;
784
- } else {
785
- throw error;
786
- }
787
- }
788
- }
789
-
790
- private async connectOrDiscoverNode(
791
- peerNodeId: NodeId,
792
- operationalAddress?: ServerAddressIp,
793
- discoveryOptions: DiscoveryOptions = {},
794
- ) {
795
- const {
796
- discoveryType: requestedDiscoveryType = NodeDiscoveryType.FullDiscovery,
797
- timeoutSeconds,
798
- discoveryData = this.commissionedNodes.get(peerNodeId)?.discoveryData,
799
- } = discoveryOptions;
800
- if (timeoutSeconds !== undefined && requestedDiscoveryType !== NodeDiscoveryType.TimedDiscovery) {
801
- throw new ImplementationError("Cannot set timeout without timed discovery.");
802
- }
803
- if (requestedDiscoveryType === NodeDiscoveryType.RetransmissionDiscovery) {
804
- throw new ImplementationError("Cannot set retransmission discovery type.");
805
- }
806
-
807
- if (this.mdnsScanner === undefined) {
808
- throw new ImplementationError("Cannot discover device without mDNS scanner.");
809
- }
810
- const mdnsScanner = this.mdnsScanner;
811
-
812
- const existingDiscoveryDetails = this.#runningNodeDiscoveries.get(peerNodeId) ?? {
813
- type: NodeDiscoveryType.None,
814
- };
815
-
816
- // If we currently run another "lower" retransmission type we cancel it
817
- if (
818
- existingDiscoveryDetails.type !== NodeDiscoveryType.None &&
819
- existingDiscoveryDetails.type < requestedDiscoveryType
820
- ) {
821
- mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
822
- this.#runningNodeDiscoveries.delete(peerNodeId);
823
- existingDiscoveryDetails.type = NodeDiscoveryType.None;
824
- }
825
-
826
- const { type: runningDiscoveryType, promises } = existingDiscoveryDetails;
827
-
828
- // If we have a last known address try to reach the device directly when we are not already discovering
829
- // In worst case parallel cases we do this step twice, but that's ok
830
- if (
831
- operationalAddress !== undefined &&
832
- (runningDiscoveryType === NodeDiscoveryType.None || requestedDiscoveryType === NodeDiscoveryType.None)
833
- ) {
834
- const directReconnection = await this.reconnectKnownAddress(peerNodeId, operationalAddress, discoveryData);
835
- if (directReconnection !== undefined) {
836
- return directReconnection;
837
- }
838
- if (requestedDiscoveryType === NodeDiscoveryType.None) {
839
- throw new DiscoveryError(`Node ${peerNodeId} is not reachable right now.`);
840
- }
841
- }
842
-
843
- if (promises !== undefined) {
844
- if (runningDiscoveryType > requestedDiscoveryType) {
845
- // We already run a "longer" discovery, so we know it is unreachable for now
846
- throw new DiscoveryError(
847
- `Node ${peerNodeId} is not reachable right now and discovery already running.`,
848
- );
849
- } else {
850
- // If we are already discovering this node, so we reuse promises
851
- return await anyPromise(promises);
852
- }
853
- }
854
-
855
- const discoveryPromises = new Array<() => Promise<MessageChannel>>();
856
- let reconnectionPollingTimer: Timer | undefined;
857
-
858
- if (operationalAddress !== undefined) {
859
- // Additionally to general discovery we also try to poll the formerly known operational address
860
- if (requestedDiscoveryType === NodeDiscoveryType.FullDiscovery) {
861
- const { promise, resolver, rejecter } = createPromise<MessageChannel>();
862
-
863
- reconnectionPollingTimer = Time.getPeriodicTimer(
864
- "Controller reconnect",
865
- RECONNECTION_POLLING_INTERVAL_MS,
866
- async () => {
867
- try {
868
- logger.debug(`Polling for device at ${serverAddressToString(operationalAddress)} ...`);
869
- const result = await this.reconnectKnownAddress(
870
- peerNodeId,
871
- operationalAddress,
872
- discoveryData,
873
- );
874
- if (result !== undefined && reconnectionPollingTimer?.isRunning) {
875
- reconnectionPollingTimer?.stop();
876
- mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
877
- this.#runningNodeDiscoveries.delete(peerNodeId);
878
- resolver(result);
879
- }
880
- } catch (error) {
881
- if (reconnectionPollingTimer?.isRunning) {
882
- reconnectionPollingTimer?.stop();
883
- mdnsScanner.cancelOperationalDeviceDiscovery(this.fabric, peerNodeId);
884
- this.#runningNodeDiscoveries.delete(peerNodeId);
885
- rejecter(error);
886
- }
887
- }
888
- },
889
- ).start();
890
-
891
- discoveryPromises.push(() => promise);
892
- }
893
- }
894
-
895
- discoveryPromises.push(async () => {
896
- const scanResult = await ControllerDiscovery.discoverOperationalDevice(
897
- this.fabric,
898
- peerNodeId,
899
- mdnsScanner,
900
- timeoutSeconds,
901
- timeoutSeconds === undefined,
902
- );
903
- const { timer } = this.#runningNodeDiscoveries.get(peerNodeId) ?? {};
904
- timer?.stop();
905
- this.#runningNodeDiscoveries.delete(peerNodeId);
906
-
907
- const { result } = await ControllerDiscovery.iterateServerAddresses(
908
- [scanResult],
909
- NoResponseTimeoutError,
910
- async () => {
911
- const device = mdnsScanner.getDiscoveredOperationalDevice(this.fabric, peerNodeId);
912
- return device !== undefined ? [device] : [];
913
- },
914
- async (address, device) => {
915
- const result = await this.pair(peerNodeId, address, device);
916
- await this.setOperationalDeviceData(peerNodeId, address, {
917
- ...discoveryData,
918
- ...device,
919
- });
920
- return result;
921
- },
922
- );
923
-
924
- return result;
925
- });
926
-
927
- this.#runningNodeDiscoveries.set(peerNodeId, {
928
- type: requestedDiscoveryType,
929
- promises: discoveryPromises,
930
- timer: reconnectionPollingTimer,
931
- });
932
-
933
- return await anyPromise(discoveryPromises).finally(() => this.#runningNodeDiscoveries.delete(peerNodeId));
934
- }
935
-
936
- /**
937
- * Resume a device connection and establish a CASE session that was previously paired with the controller. This
938
- * method will try to connect to the device using the previously used server address (if set). If that fails, the
939
- * device is discovered again using its operational instance details.
940
- * It returns the operational MessageChannel on success.
941
- */
942
- private async resume(peerNodeId: NodeId, discoveryOptions?: DiscoveryOptions) {
943
- const operationalAddress = this.getLastOperationalAddress(peerNodeId);
944
-
945
- try {
946
- return await this.connectOrDiscoverNode(peerNodeId, operationalAddress, discoveryOptions);
947
- } catch (error) {
948
- if (
949
- (error instanceof DiscoveryError || error instanceof NoResponseTimeoutError) &&
950
- this.commissionedNodes.has(peerNodeId)
951
- ) {
952
- logger.info(`Resume failed, remove all sessions for node ${peerNodeId}`);
953
- // We remove all sessions, this also informs the PairedNode class
954
- await this.sessionManager.removeAllSessionsForNode(peerNodeId);
955
- }
956
- throw error;
957
- }
958
- }
959
-
960
- /** Pair with an operational device (already commissioned) and establish a CASE session. */
961
- private async pair(
962
- peerNodeId: NodeId,
963
- operationalServerAddress: ServerAddressIp,
964
- discoveryData?: DiscoveryData,
965
- expectedProcessingTimeMs?: number,
966
- ) {
967
- const { ip, port } = operationalServerAddress;
968
- // Do CASE pairing
969
- const isIpv6Address = isIPv6(ip);
970
- const operationalInterface = isIpv6Address ? this.netInterfaceIpv6 : this.netInterfaceIpv4;
971
-
972
- if (operationalInterface === undefined) {
973
- throw new PairRetransmissionLimitReachedError(
974
- `IPv${
975
- isIpv6Address ? "6" : "4"
976
- } interface not initialized for port ${port}. Cannot use ${ip} for pairing.`,
977
- );
978
- }
979
-
980
- const operationalChannel = await operationalInterface.openChannel(operationalServerAddress);
981
- const { sessionParameters } = this.findResumptionRecordByNodeId(peerNodeId) ?? {};
982
- const unsecureSession = this.sessionManager.createUnsecureSession({
983
- // Use the session parameters from MDNS announcements when available and rest is assumed to be fallbacks
984
- sessionParameters: {
985
- idleIntervalMs: discoveryData?.SII ?? sessionParameters?.idleIntervalMs,
986
- activeIntervalMs: discoveryData?.SAI ?? sessionParameters?.activeIntervalMs,
987
- activeThresholdMs: discoveryData?.SAT ?? sessionParameters?.activeThresholdMs,
988
- },
989
- isInitiator: true,
990
- });
991
- const operationalUnsecureMessageExchange = new MessageChannel(operationalChannel, unsecureSession);
992
- let operationalSecureSession;
993
- try {
994
- const exchange = this.exchangeManager.initiateExchangeWithChannel(
995
- operationalUnsecureMessageExchange,
996
- SECURE_CHANNEL_PROTOCOL_ID,
997
- );
998
-
999
- try {
1000
- operationalSecureSession = await this.caseClient.pair(
1001
- this,
1002
- exchange,
1003
- this.fabric,
1004
- peerNodeId,
1005
- expectedProcessingTimeMs,
1006
- );
1007
- } catch (e) {
1008
- await exchange.close();
1009
- throw e;
1010
- }
1011
- } catch (e) {
1012
- NoResponseTimeoutError.accept(e);
1013
-
1014
- // Convert error
1015
- throw new PairRetransmissionLimitReachedError(e.message);
1016
- }
1017
- await unsecureSession.destroy();
1018
- const channel = new MessageChannel(operationalChannel, operationalSecureSession);
1019
- await this.channelManager.setChannel(this.fabric, peerNodeId, channel);
1020
- return channel;
1021
- }
1022
-
1023
413
  isCommissioned() {
1024
- return this.commissionedNodes.size > 0;
414
+ return this.peers.size > 0;
1025
415
  }
1026
416
 
1027
417
  getCommissionedNodes() {
1028
- return Array.from(this.commissionedNodes.keys());
418
+ return this.peers.map(peer => peer.address.nodeId);
1029
419
  }
1030
420
 
1031
421
  getCommissionedNodesDetails() {
1032
- return Array.from(this.commissionedNodes.entries()).map(
1033
- ([nodeId, { operationalServerAddress, discoveryData, basicInformationData }]) => ({
1034
- nodeId,
1035
- operationalAddress: operationalServerAddress
1036
- ? serverAddressToString(operationalServerAddress)
1037
- : undefined,
422
+ return this.peers.map(peer => {
423
+ const { address, operationalAddress, discoveryData, basicInformationData } = peer as CommissionedPeer;
424
+ return {
425
+ nodeId: address.nodeId,
426
+ operationalAddress: operationalAddress ? serverAddressToString(operationalAddress) : undefined,
1038
427
  advertisedName: discoveryData?.DN,
1039
428
  discoveryData,
1040
429
  basicInformationData,
1041
- }),
1042
- );
1043
- }
1044
-
1045
- private async setOperationalDeviceData(
1046
- nodeId: NodeId,
1047
- operationalServerAddress: ServerAddressIp,
1048
- discoveryData?: DiscoveryData,
1049
- ) {
1050
- const nodeDetails = this.commissionedNodes.get(nodeId) ?? {};
1051
- nodeDetails.operationalServerAddress = operationalServerAddress;
1052
- if (discoveryData !== undefined) {
1053
- nodeDetails.discoveryData = {
1054
- ...nodeDetails.discoveryData,
1055
- ...discoveryData,
1056
430
  };
1057
- }
1058
- this.commissionedNodes.set(nodeId, nodeDetails);
1059
- await this.storeCommissionedNodes();
431
+ });
1060
432
  }
1061
433
 
1062
434
  async enhanceCommissionedNodeDetails(
1063
435
  nodeId: NodeId,
1064
436
  data: { basicInformationData: Record<string, SupportedStorageTypes> },
1065
437
  ) {
1066
- const nodeDetails = this.commissionedNodes.get(nodeId);
438
+ const nodeDetails = this.peers.get(this.fabric.addressOf(nodeId)) as CommissionedPeer;
1067
439
  if (nodeDetails === undefined) {
1068
440
  throw new Error(`Node ${nodeId} is not commissioned.`);
1069
441
  }
1070
442
  const { basicInformationData } = data;
1071
443
  nodeDetails.basicInformationData = basicInformationData;
1072
- this.commissionedNodes.set(nodeId, nodeDetails);
1073
- await this.storeCommissionedNodes();
1074
- }
1075
-
1076
- private getLastOperationalAddress(nodeId: NodeId) {
1077
- return this.commissionedNodes.get(nodeId)?.operationalServerAddress;
1078
- }
1079
-
1080
- private async storeCommissionedNodes() {
1081
- await this.nodesStorage.set("commissionedNodes", Array.from(this.commissionedNodes.entries()));
444
+ await this.nodesStore.save();
1082
445
  }
1083
446
 
1084
447
  /**
@@ -1086,58 +449,7 @@ export class MatterController implements SessionContext {
1086
449
  * Returns a InteractionClient on success.
1087
450
  */
1088
451
  async connect(peerNodeId: NodeId, discoveryOptions: DiscoveryOptions) {
1089
- const { discoveryData } = discoveryOptions;
1090
-
1091
- let channel: MessageChannel;
1092
- try {
1093
- channel = this.channelManager.getChannel(this.fabric, peerNodeId);
1094
- } catch (error) {
1095
- NoChannelError.accept(error);
1096
-
1097
- channel = await this.resume(peerNodeId, discoveryOptions);
1098
- }
1099
- return new InteractionClient(
1100
- new ExchangeProvider(this.exchangeManager, channel, async () => {
1101
- if (!this.channelManager.hasChannel(this.fabric, peerNodeId)) {
1102
- throw new RetransmissionLimitReachedError(`Device ${peerNodeId} is currently not reachable.`);
1103
- }
1104
- await this.channelManager.removeAllNodeChannels(this.fabric, peerNodeId);
1105
-
1106
- const discoveredAddresses = this.mdnsScanner?.getDiscoveredOperationalDevice(this.fabric, peerNodeId);
1107
- const lastKnownAddress = this.getLastOperationalAddress(peerNodeId);
1108
-
1109
- if (
1110
- lastKnownAddress !== undefined &&
1111
- discoveredAddresses !== undefined &&
1112
- discoveredAddresses.addresses.some(
1113
- ({ ip, port }) => ip === lastKnownAddress.ip && port === lastKnownAddress.port,
1114
- )
1115
- ) {
1116
- // We found the same address, so assume somehow cached response because we just tried to connect,
1117
- // and it failed, so clear list
1118
- discoveredAddresses.addresses.length = 0;
1119
- }
1120
-
1121
- // Try to use first result for one last try before we need to reconnect
1122
- const operationalAddress = discoveredAddresses?.addresses[0];
1123
- if (operationalAddress === undefined) {
1124
- logger.info(
1125
- `Re-Discovering device failed (no address found), remove all sessions for node ${peerNodeId}`,
1126
- );
1127
- // We remove all sessions, this also informs the PairedNode class
1128
- await this.sessionManager.removeAllSessionsForNode(peerNodeId);
1129
- throw new RetransmissionLimitReachedError(`No operational address found for node ${peerNodeId}`);
1130
- }
1131
- if (
1132
- (await this.reconnectKnownAddress(peerNodeId, operationalAddress, discoveryData, 2_000)) ===
1133
- undefined
1134
- ) {
1135
- throw new RetransmissionLimitReachedError(`Device ${peerNodeId} is not reachable.`);
1136
- }
1137
- return this.channelManager.getChannel(this.fabric, peerNodeId);
1138
- }),
1139
- peerNodeId,
1140
- );
452
+ return this.peers.connect(this.fabric.addressOf(peerNodeId), discoveryOptions);
1141
453
  }
1142
454
 
1143
455
  async getNextAvailableSessionId() {
@@ -1149,7 +461,7 @@ export class MatterController implements SessionContext {
1149
461
  }
1150
462
 
1151
463
  findResumptionRecordByNodeId(nodeId: NodeId) {
1152
- return this.sessionManager.findResumptionRecordByNodeId(nodeId);
464
+ return this.sessionManager.findResumptionRecordByAddress(this.fabric.addressOf(nodeId));
1153
465
  }
1154
466
 
1155
467
  async saveResumptionRecord(resumptionRecord: ResumptionRecord) {
@@ -1161,19 +473,68 @@ export class MatterController implements SessionContext {
1161
473
  }
1162
474
 
1163
475
  async close() {
1164
- for (const [nodeId, { timer }] of this.#runningNodeDiscoveries.entries()) {
1165
- timer?.stop();
1166
- this.mdnsScanner?.cancelOperationalDeviceDiscovery(this.fabric, nodeId, false); // This ends discovery without triggering promises
1167
- }
476
+ await this.peers.close();
1168
477
  await this.exchangeManager.close();
1169
478
  await this.sessionManager.close();
1170
479
  await this.channelManager.close();
1171
- await this.netInterfaceBle?.close();
1172
- await this.netInterfaceIpv4?.close();
1173
- await this.netInterfaceIpv6?.close();
480
+ await this.netInterfaces.close();
1174
481
  }
1175
482
 
1176
483
  getActiveSessionInformation() {
1177
484
  return this.sessionManager.getActiveSessionInformation();
1178
485
  }
1179
486
  }
487
+
488
+ class CommissionedNodeStore extends PeerStore {
489
+ declare peers: PeerSet;
490
+
491
+ constructor(
492
+ private nodesStorage: StorageContext,
493
+ private fabric: Fabric,
494
+ ) {
495
+ super();
496
+ }
497
+
498
+ async loadPeers() {
499
+ if (!(await this.nodesStorage.has("commissionedNodes"))) {
500
+ return [];
501
+ }
502
+
503
+ const commissionedNodes = await this.nodesStorage.get<StoredOperationalPeer[]>("commissionedNodes");
504
+ return commissionedNodes.map(
505
+ ([nodeId, { operationalServerAddress, discoveryData, basicInformationData }]) =>
506
+ ({
507
+ address: this.fabric.addressOf(nodeId),
508
+ operationalAddress: operationalServerAddress,
509
+ discoveryData,
510
+ basicInformationData,
511
+ }) satisfies CommissionedPeer,
512
+ );
513
+ }
514
+
515
+ async updatePeer() {
516
+ return this.save();
517
+ }
518
+
519
+ async deletePeer() {
520
+ return this.save();
521
+ }
522
+
523
+ async save() {
524
+ await this.nodesStorage.set(
525
+ "commissionedNodes",
526
+ this.peers.map(peer => {
527
+ const {
528
+ address,
529
+ operationalAddress: operationalServerAddress,
530
+ basicInformationData,
531
+ discoveryData,
532
+ } = peer as CommissionedPeer;
533
+ return [
534
+ address.nodeId,
535
+ { operationalServerAddress, basicInformationData, discoveryData },
536
+ ] satisfies StoredOperationalPeer;
537
+ }),
538
+ );
539
+ }
540
+ }