@matter/protocol 0.14.0-alpha.0-20250521-979eda05d → 0.14.0-alpha.0-20250524-51a7e1721

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 (35) hide show
  1. package/dist/cjs/action/server/AttributeWriteResponse.d.ts.map +1 -1
  2. package/dist/cjs/action/server/AttributeWriteResponse.js +1 -14
  3. package/dist/cjs/action/server/AttributeWriteResponse.js.map +1 -1
  4. package/dist/cjs/events/OccurrenceManager.d.ts +10 -2
  5. package/dist/cjs/events/OccurrenceManager.d.ts.map +1 -1
  6. package/dist/cjs/events/OccurrenceManager.js +57 -11
  7. package/dist/cjs/events/OccurrenceManager.js.map +1 -1
  8. package/dist/cjs/fabric/Fabric.d.ts +2 -11
  9. package/dist/cjs/fabric/Fabric.d.ts.map +1 -1
  10. package/dist/cjs/fabric/Fabric.js +1 -38
  11. package/dist/cjs/fabric/Fabric.js.map +1 -1
  12. package/dist/cjs/mdns/MdnsScanner.d.ts +23 -1
  13. package/dist/cjs/mdns/MdnsScanner.d.ts.map +1 -1
  14. package/dist/cjs/mdns/MdnsScanner.js +124 -11
  15. package/dist/cjs/mdns/MdnsScanner.js.map +2 -2
  16. package/dist/esm/action/server/AttributeWriteResponse.d.ts.map +1 -1
  17. package/dist/esm/action/server/AttributeWriteResponse.js +2 -15
  18. package/dist/esm/action/server/AttributeWriteResponse.js.map +1 -1
  19. package/dist/esm/events/OccurrenceManager.d.ts +10 -2
  20. package/dist/esm/events/OccurrenceManager.d.ts.map +1 -1
  21. package/dist/esm/events/OccurrenceManager.js +62 -12
  22. package/dist/esm/events/OccurrenceManager.js.map +1 -1
  23. package/dist/esm/fabric/Fabric.d.ts +2 -11
  24. package/dist/esm/fabric/Fabric.d.ts.map +1 -1
  25. package/dist/esm/fabric/Fabric.js +1 -38
  26. package/dist/esm/fabric/Fabric.js.map +1 -1
  27. package/dist/esm/mdns/MdnsScanner.d.ts +23 -1
  28. package/dist/esm/mdns/MdnsScanner.d.ts.map +1 -1
  29. package/dist/esm/mdns/MdnsScanner.js +127 -11
  30. package/dist/esm/mdns/MdnsScanner.js.map +2 -2
  31. package/package.json +6 -6
  32. package/src/action/server/AttributeWriteResponse.ts +2 -20
  33. package/src/events/OccurrenceManager.ts +91 -11
  34. package/src/fabric/Fabric.ts +1 -49
  35. package/src/mdns/MdnsScanner.ts +187 -12
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import {
8
+ BasicSet,
8
9
  Bytes,
9
10
  ChannelType,
10
11
  Diagnostic,
@@ -16,10 +17,12 @@ import {
16
17
  DnsRecordClass,
17
18
  DnsRecordType,
18
19
  ImplementationError,
20
+ InternalError,
19
21
  Lifespan,
20
22
  Logger,
21
23
  MAX_MDNS_MESSAGE_SIZE,
22
24
  Network,
25
+ ObserverGroup,
23
26
  ServerAddressIp,
24
27
  SrvRecordValue,
25
28
  Time,
@@ -90,6 +93,28 @@ type StructuredDnsAnswers = {
90
93
  /** The initial number of seconds between two announcements. MDNS specs require 1-2 seconds, so lets use the middle. */
91
94
  const START_ANNOUNCE_INTERVAL_SECONDS = 1.5;
92
95
 
96
+ /**
97
+ * Interface to add criteria for MDNS discovery a node is interested in
98
+ *
99
+ * This interface is used to define criteria for mDNS scanner targets.
100
+ * It includes the information if commissionable devices are relevant for the target
101
+ * and a list of operational targets. Operational targets can consist of operational IDs
102
+ * and optional node IDs.
103
+ *
104
+ * When no commissionable devices are relevant and no operational targets are defined, it is
105
+ * not required to add a criteria to the scanner.
106
+ */
107
+ export interface MdnsScannerTargetCriteria {
108
+ /** Are commissionable MDNS records relevant? */
109
+ commissionable: boolean;
110
+
111
+ /** List of operational targets. */
112
+ operationalTargets: {
113
+ operationalId: Uint8Array;
114
+ nodeId?: NodeId;
115
+ }[];
116
+ }
117
+
93
118
  /**
94
119
  * This class implements the Scanner interface for a MDNS scanner via UDP messages in a IP based network. It sends out
95
120
  * queries to discover various types of Matter device types and listens for announcements.
@@ -133,6 +158,7 @@ export class MdnsScanner implements Scanner {
133
158
  timer?: Timer;
134
159
  resolveOnUpdatedRecords: boolean;
135
160
  cancelResolver?: (value: void) => void;
161
+ commissionable: boolean;
136
162
  }
137
163
  >();
138
164
 
@@ -143,6 +169,15 @@ export class MdnsScanner implements Scanner {
143
169
  readonly #multicastServer: UdpMulticastServer;
144
170
  readonly #enableIpv4?: boolean;
145
171
 
172
+ readonly #targetCriteriaProviders = new BasicSet<MdnsScannerTargetCriteria>();
173
+ #scanForCommissionableDevices = false;
174
+ #hasCommissionableWaiters = false;
175
+ readonly #operationalScanTargets = new Set<string>();
176
+ readonly #observers = new ObserverGroup();
177
+
178
+ /** True, if any node is interested in MDNS traffic, else we ignore all traffic */
179
+ #listening = false;
180
+
146
181
  constructor(multicastServer: UdpMulticastServer, enableIpv4?: boolean) {
147
182
  multicastServer.onMessage((message, remoteIp, netInterface) =>
148
183
  this.#handleDnsMessage(message, remoteIp, netInterface),
@@ -152,6 +187,73 @@ export class MdnsScanner implements Scanner {
152
187
  this.#periodicTimer = Time.getPeriodicTimer("Discovered node expiration", 60 * 1000 /* 1 mn */, () =>
153
188
  this.#expire(),
154
189
  ).start();
190
+
191
+ this.#observers.on(this.#targetCriteriaProviders.added, () => this.#handleChangedScanTargets());
192
+ this.#observers.on(this.#targetCriteriaProviders.deleted, () => this.#handleChangedScanTargets());
193
+ }
194
+
195
+ /** Set to add or delete criteria for MDNS discovery */
196
+ get targetCriteriaProviders() {
197
+ return this.#targetCriteriaProviders;
198
+ }
199
+
200
+ #handleChangedScanTargets() {
201
+ this.#updateScanTargets();
202
+ logger.info(
203
+ "MDNS Scan targets updated :",
204
+ `commissionable = ${this.#scanForCommissionableDevices}`,
205
+ "Targets:",
206
+ this.#operationalScanTargets,
207
+ );
208
+ }
209
+
210
+ /** Update the MDNS scan criteria state and collect the desired operational targets */
211
+ #updateScanTargets() {
212
+ if (this.#closing) {
213
+ return;
214
+ }
215
+
216
+ // Add all operational targets from the criteria providers
217
+ this.#operationalScanTargets.clear();
218
+ let cacheCommissionableDevices = false;
219
+ for (const criteria of this.#targetCriteriaProviders) {
220
+ const { operationalTargets, commissionable } = criteria;
221
+ cacheCommissionableDevices = cacheCommissionableDevices || commissionable;
222
+ for (const { operationalId, nodeId } of operationalTargets) {
223
+ const operationalIdString = Bytes.toHex(operationalId).toUpperCase();
224
+ if (nodeId === undefined) {
225
+ this.#operationalScanTargets.add(operationalIdString);
226
+ } else {
227
+ this.#operationalScanTargets.add(`${operationalIdString}-${NodeId.toHexString(nodeId)}`);
228
+ }
229
+ }
230
+ }
231
+ this.#scanForCommissionableDevices = cacheCommissionableDevices;
232
+
233
+ // Register all operational targets for running queries
234
+ for (const queryId of this.#recordWaiters.keys()) {
235
+ this.#registerOperationalQuery(queryId);
236
+ }
237
+ this.#updateListeningStatus();
238
+ }
239
+
240
+ /** Update the status of we care about MDNS messages or not */
241
+ #updateListeningStatus() {
242
+ const formerListenStatus = this.#listening;
243
+ // Are we interested in MDNS traffic or not?
244
+ this.#listening =
245
+ this.#scanForCommissionableDevices ||
246
+ this.#operationalScanTargets.size > 0 ||
247
+ this.#recordWaiters.size > 0 ||
248
+ this.#activeAnnounceQueries.size > 0;
249
+ if (!this.#listening) {
250
+ this.#discoveredIpRecords.clear();
251
+ this.#operationalDeviceRecords.clear();
252
+ this.#commissionableDeviceRecords.clear();
253
+ }
254
+ if (this.#listening !== formerListenStatus) {
255
+ logger.debug(`MDNS Scanner ${this.#listening ? "started" : "stopped"} listening for MDNS messages`);
256
+ }
155
257
  }
156
258
 
157
259
  #effectiveTTL(ttl: number) {
@@ -348,12 +450,22 @@ export class MdnsScanner implements Scanner {
348
450
  });
349
451
  }
350
452
 
453
+ #registerOperationalQuery(queryId: string) {
454
+ const separator = queryId.indexOf(".");
455
+ if (separator !== -1) {
456
+ this.#operationalScanTargets.add(queryId.substring(0, separator));
457
+ } else {
458
+ throw new InternalError(`Invalid queryId ${queryId} for operational device, no separator found`);
459
+ }
460
+ }
461
+
351
462
  /**
352
463
  * Registers a deferred promise for a specific queryId together with a timeout and return the promise.
353
464
  * The promise will be resolved when the timer runs out latest.
354
465
  */
355
466
  async #registerWaiterPromise(
356
467
  queryId: string,
468
+ commissionable: boolean,
357
469
  timeoutSeconds?: number,
358
470
  resolveOnUpdatedRecords = true,
359
471
  cancelResolver?: (value: void) => void,
@@ -366,7 +478,12 @@ export class MdnsScanner implements Scanner {
366
478
  this.#finishWaiter(queryId, true);
367
479
  }).start()
368
480
  : undefined;
369
- this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver });
481
+ this.#listening = true;
482
+ this.#recordWaiters.set(queryId, { resolver, timer, resolveOnUpdatedRecords, cancelResolver, commissionable });
483
+ this.#hasCommissionableWaiters = this.#hasCommissionableWaiters || commissionable;
484
+ if (!commissionable) {
485
+ this.#registerOperationalQuery(queryId);
486
+ }
370
487
  logger.debug(
371
488
  `Registered waiter for query ${queryId} with ${
372
489
  timeoutSeconds !== undefined ? `timeout ${timeoutSeconds} seconds` : "no timeout"
@@ -382,7 +499,7 @@ export class MdnsScanner implements Scanner {
382
499
  #finishWaiter(queryId: string, resolvePromise: boolean, isUpdatedRecord = false) {
383
500
  const waiter = this.#recordWaiters.get(queryId);
384
501
  if (waiter === undefined) return;
385
- const { timer, resolver, resolveOnUpdatedRecords } = waiter;
502
+ const { timer, resolver, resolveOnUpdatedRecords, commissionable } = waiter;
386
503
  if (isUpdatedRecord && !resolveOnUpdatedRecords) return;
387
504
  logger.debug(`Finishing waiter for query ${queryId}, resolving: ${resolvePromise}`);
388
505
  if (timer !== undefined) {
@@ -392,6 +509,33 @@ export class MdnsScanner implements Scanner {
392
509
  resolver();
393
510
  }
394
511
  this.#recordWaiters.delete(queryId);
512
+ this.#removeQuery(queryId);
513
+
514
+ if (!this.#closing) {
515
+ // We removed a waiter, so update what we still have left
516
+ this.#hasCommissionableWaiters = false;
517
+ let hasOperationalWaiters = false;
518
+ for (const { commissionable } of this.#recordWaiters.values()) {
519
+ if (commissionable) {
520
+ this.#hasCommissionableWaiters = true;
521
+ if (hasOperationalWaiters) {
522
+ break; // No need to check further
523
+ }
524
+ } else {
525
+ hasOperationalWaiters = true;
526
+ if (this.#hasCommissionableWaiters) {
527
+ break; // No need to check further
528
+ }
529
+ }
530
+ }
531
+
532
+ if (!commissionable) {
533
+ // We removed an operational device waiter, so we need to update the scan targets
534
+ this.#updateScanTargets();
535
+ } else {
536
+ this.#updateListeningStatus();
537
+ }
538
+ }
395
539
  }
396
540
 
397
541
  /** Returns weather a waiter promise is registered for a specific queryId. */
@@ -421,7 +565,7 @@ export class MdnsScanner implements Scanner {
421
565
 
422
566
  let storedDevice = ignoreExistingRecords ? undefined : this.#getOperationalDeviceRecords(deviceMatterQname);
423
567
  if (storedDevice === undefined) {
424
- const promise = this.#registerWaiterPromise(deviceMatterQname, timeoutSeconds);
568
+ const promise = this.#registerWaiterPromise(deviceMatterQname, false, timeoutSeconds);
425
569
 
426
570
  this.#setQueryRecords(deviceMatterQname, [
427
571
  {
@@ -433,7 +577,6 @@ export class MdnsScanner implements Scanner {
433
577
 
434
578
  await promise;
435
579
  storedDevice = this.#getOperationalDeviceRecords(deviceMatterQname);
436
- this.#removeQuery(deviceMatterQname);
437
580
  }
438
581
  return storedDevice;
439
582
  }
@@ -446,7 +589,7 @@ export class MdnsScanner implements Scanner {
446
589
  cancelCommissionableDeviceDiscovery(identifier: CommissionableDeviceIdentifiers, resolvePromise = true) {
447
590
  const queryId = this.#buildCommissionableQueryIdentifier(identifier);
448
591
  const { cancelResolver } = this.#recordWaiters.get(queryId) ?? {};
449
- // Mark as cancelled to not loop further in discovery, if cancel resolver is used
592
+ // Mark as canceled to not loop further in discovery, if cancel-resolver is used
450
593
  cancelResolver?.();
451
594
  this.#finishWaiter(queryId, resolvePromise);
452
595
  }
@@ -649,13 +792,12 @@ export class MdnsScanner implements Scanner {
649
792
  : this.#getCommissionableDeviceRecords(identifier).filter(({ addresses }) => addresses.length > 0);
650
793
  if (storedRecords.length === 0) {
651
794
  const queryId = this.#buildCommissionableQueryIdentifier(identifier);
652
- const promise = this.#registerWaiterPromise(queryId, timeoutSeconds);
795
+ const promise = this.#registerWaiterPromise(queryId, true, timeoutSeconds);
653
796
 
654
797
  this.#setQueryRecords(queryId, this.#getCommissionableQueryRecords(identifier));
655
798
 
656
799
  await promise;
657
800
  storedRecords = this.#getCommissionableDeviceRecords(identifier);
658
- this.#removeQuery(queryId);
659
801
  }
660
802
 
661
803
  return storedRecords;
@@ -714,7 +856,7 @@ export class MdnsScanner implements Scanner {
714
856
  break;
715
857
  }
716
858
  }
717
- await this.#registerWaiterPromise(queryId, remainingTime, false, queryResolver);
859
+ await this.#registerWaiterPromise(queryId, true, remainingTime, false, queryResolver);
718
860
  }
719
861
  return this.#getCommissionableDeviceRecords(identifier);
720
862
  }
@@ -728,6 +870,7 @@ export class MdnsScanner implements Scanner {
728
870
  */
729
871
  async close() {
730
872
  this.#closing = true;
873
+ this.#observers.close();
731
874
  this.#periodicTimer.stop();
732
875
  this.#queryTimer?.stop();
733
876
  await this.#multicastServer.close();
@@ -1039,6 +1182,19 @@ export class MdnsScanner implements Scanner {
1039
1182
  }
1040
1183
  }
1041
1184
 
1185
+ #matchesOperationalCriteria(matterName: string) {
1186
+ const nameParts = matterName.match(/^([\dA-F]{16})-([\dA-F]{16})\._matter\._tcp\.local$/i);
1187
+ if (!nameParts) {
1188
+ return false;
1189
+ }
1190
+ const operationalId = nameParts[1];
1191
+ const nodeId = nameParts[2];
1192
+ return (
1193
+ this.#operationalScanTargets.has(operationalId) ||
1194
+ this.#operationalScanTargets.has(`${operationalId}-${nodeId}`)
1195
+ );
1196
+ }
1197
+
1042
1198
  #handleOperationalTxtRecord(record: DnsRecord<any>, netInterface: string) {
1043
1199
  const { name: matterName, value, ttl } = record as DnsRecord<string[]>;
1044
1200
  const discoveredAt = Time.nowMs();
@@ -1055,8 +1211,15 @@ export class MdnsScanner implements Scanner {
1055
1211
  }
1056
1212
  if (!Array.isArray(value)) return;
1057
1213
 
1214
+ // Existing records are always updated if relevant, but no new are added if they are not matching the criteria
1215
+ if (!this.#operationalDeviceRecords.has(matterName) && !this.#matchesOperationalCriteria(matterName)) {
1216
+ //logger.debug(`Operational device ${matterName} is not in the list of operational scan targets, ignoring.`);
1217
+ return;
1218
+ }
1219
+
1058
1220
  const txtData = this.#parseTxtRecord(record);
1059
1221
  if (txtData === undefined) return;
1222
+
1060
1223
  let device = this.#operationalDeviceRecords.get(matterName);
1061
1224
  if (device !== undefined) {
1062
1225
  device = {
@@ -1093,9 +1256,8 @@ export class MdnsScanner implements Scanner {
1093
1256
  ttl,
1094
1257
  value: { target, port },
1095
1258
  } = record;
1096
- const discoveredAt = Time.nowMs();
1097
1259
 
1098
- // we got an expiry info, so we can remove the record if we know it already and are done
1260
+ // We got device expiry info, so we can remove the record if we know it already and are done
1099
1261
  if (ttl === 0) {
1100
1262
  if (this.#operationalDeviceRecords.has(matterName)) {
1101
1263
  logger.debug(
@@ -1103,11 +1265,19 @@ export class MdnsScanner implements Scanner {
1103
1265
  );
1104
1266
  this.#operationalDeviceRecords.delete(matterName);
1105
1267
  }
1106
- return true;
1268
+ return;
1107
1269
  }
1108
1270
 
1109
1271
  const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
1110
1272
  const deviceExisted = this.#operationalDeviceRecords.has(matterName);
1273
+
1274
+ // Existing records are always updated if relevant, but no new are added if they are not matching the criteria
1275
+ if (!deviceExisted && !this.#matchesOperationalCriteria(matterName)) {
1276
+ //logger.debug(`Operational device ${matterName} is not in the list of operational scan targets, ignoring.`);
1277
+ return;
1278
+ }
1279
+
1280
+ const discoveredAt = Time.nowMs();
1111
1281
  const device = this.#operationalDeviceRecords.get(matterName) ?? {
1112
1282
  deviceIdentifier: matterName,
1113
1283
  addresses: new Map<string, MatterServerRecordWithExpire>(),
@@ -1152,7 +1322,7 @@ export class MdnsScanner implements Scanner {
1152
1322
  } else if (addresses.size > 0) {
1153
1323
  this.#finishWaiter(matterName, true, deviceExisted);
1154
1324
  }
1155
- return true;
1325
+ return;
1156
1326
  }
1157
1327
 
1158
1328
  #handleCommissionableRecords(
@@ -1160,6 +1330,11 @@ export class MdnsScanner implements Scanner {
1160
1330
  formerAnswers: StructuredDnsAnswers,
1161
1331
  netInterface: string,
1162
1332
  ) {
1333
+ if (!this.#scanForCommissionableDevices && !this.#hasCommissionableWaiters) {
1334
+ // We are not interested in commissionable devices, so we can skip this
1335
+ return;
1336
+ }
1337
+
1163
1338
  // Does the message contain a SRV record for an operational service we are interested in?
1164
1339
  let commissionableRecords = answers.commissionable ?? {};
1165
1340
  if (!commissionableRecords[DnsRecordType.SRV]?.length && !commissionableRecords[DnsRecordType.TXT]?.length) {