@matter/node 0.16.0-alpha.0-20251115-d89e62680 → 0.16.0-alpha.0-20251129-96ad07c6e

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 (166) hide show
  1. package/dist/cjs/behavior/Events.js +1 -1
  2. package/dist/cjs/behavior/cluster/ClientBehavior.d.ts +1 -1
  3. package/dist/cjs/behavior/cluster/ClusterBehavior.d.ts +1 -1
  4. package/dist/cjs/behavior/cluster/ClusterBehaviorType.d.ts +1 -1
  5. package/dist/cjs/behavior/cluster/ClusterBehaviorType.js +3 -3
  6. package/dist/cjs/behavior/cluster/ClusterBehaviorType.js.map +1 -1
  7. package/dist/cjs/behavior/state/validation/ValueValidator.js +14 -4
  8. package/dist/cjs/behavior/state/validation/ValueValidator.js.map +1 -1
  9. package/dist/cjs/behavior/state/validation/assertions.d.ts.map +1 -1
  10. package/dist/cjs/behavior/state/validation/assertions.js +21 -0
  11. package/dist/cjs/behavior/state/validation/assertions.js.map +1 -1
  12. package/dist/cjs/behavior/system/commissioning/CommissioningClient.d.ts.map +1 -1
  13. package/dist/cjs/behavior/system/commissioning/CommissioningClient.js +1 -3
  14. package/dist/cjs/behavior/system/commissioning/CommissioningClient.js.map +1 -1
  15. package/dist/cjs/behavior/system/controller/ControllerBehavior.d.ts +2 -0
  16. package/dist/cjs/behavior/system/controller/ControllerBehavior.d.ts.map +1 -1
  17. package/dist/cjs/behavior/system/controller/ControllerBehavior.js +5 -2
  18. package/dist/cjs/behavior/system/controller/ControllerBehavior.js.map +1 -1
  19. package/dist/cjs/behavior/system/network/ServerNetworkRuntime.d.ts +1 -0
  20. package/dist/cjs/behavior/system/network/ServerNetworkRuntime.d.ts.map +1 -1
  21. package/dist/cjs/behavior/system/network/ServerNetworkRuntime.js +8 -2
  22. package/dist/cjs/behavior/system/network/ServerNetworkRuntime.js.map +1 -1
  23. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
  24. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js +88 -34
  25. package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
  26. package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.d.ts.map +1 -1
  27. package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.js +15 -2
  28. package/dist/cjs/behaviors/group-key-management/GroupKeyManagementServer.js.map +1 -1
  29. package/dist/cjs/behaviors/identify/IdentifyServer.d.ts.map +1 -1
  30. package/dist/cjs/behaviors/identify/IdentifyServer.js +5 -0
  31. package/dist/cjs/behaviors/identify/IdentifyServer.js.map +1 -1
  32. package/dist/cjs/behaviors/service-area/ServiceAreaServer.js +1 -1
  33. package/dist/cjs/behaviors/switch/SwitchServer.js +2 -2
  34. package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts +10 -0
  35. package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
  36. package/dist/cjs/behaviors/thermostat/ThermostatServer.js +59 -22
  37. package/dist/cjs/behaviors/thermostat/ThermostatServer.js.map +1 -1
  38. package/dist/cjs/endpoint/Endpoint.d.ts +2 -2
  39. package/dist/cjs/endpoint/Endpoint.d.ts.map +1 -1
  40. package/dist/cjs/endpoint/properties/Endpoints.d.ts +2 -2
  41. package/dist/cjs/endpoint/properties/Endpoints.d.ts.map +1 -1
  42. package/dist/cjs/endpoint/properties/Endpoints.js +12 -1
  43. package/dist/cjs/endpoint/properties/Endpoints.js.map +1 -1
  44. package/dist/cjs/node/ClientNode.d.ts +7 -2
  45. package/dist/cjs/node/ClientNode.d.ts.map +1 -1
  46. package/dist/cjs/node/ClientNode.js +10 -3
  47. package/dist/cjs/node/ClientNode.js.map +1 -1
  48. package/dist/cjs/node/Node.d.ts.map +1 -1
  49. package/dist/cjs/node/Node.js +9 -3
  50. package/dist/cjs/node/Node.js.map +1 -1
  51. package/dist/cjs/node/NodeLifecycle.d.ts +1 -1
  52. package/dist/cjs/node/NodeLifecycle.js +1 -1
  53. package/dist/cjs/node/ServerNode.d.ts.map +1 -1
  54. package/dist/cjs/node/ServerNode.js +11 -4
  55. package/dist/cjs/node/ServerNode.js.map +1 -1
  56. package/dist/cjs/node/client/ClientEndpointInitializer.js +1 -1
  57. package/dist/cjs/node/client/ClientEndpointInitializer.js.map +1 -1
  58. package/dist/cjs/node/client/ClientEventEmitter.d.ts.map +1 -1
  59. package/dist/cjs/node/client/ClientEventEmitter.js +8 -3
  60. package/dist/cjs/node/client/ClientEventEmitter.js.map +1 -1
  61. package/dist/cjs/node/client/ClientStructure.d.ts.map +1 -1
  62. package/dist/cjs/node/client/ClientStructure.js +80 -96
  63. package/dist/cjs/node/client/ClientStructure.js.map +1 -1
  64. package/dist/cjs/node/client/Peers.d.ts.map +1 -1
  65. package/dist/cjs/node/client/Peers.js +22 -4
  66. package/dist/cjs/node/client/Peers.js.map +1 -1
  67. package/dist/cjs/node/server/ServerEnvironment.d.ts.map +1 -1
  68. package/dist/cjs/node/server/ServerEnvironment.js +0 -3
  69. package/dist/cjs/node/server/ServerEnvironment.js.map +1 -1
  70. package/dist/esm/behavior/Events.js +1 -1
  71. package/dist/esm/behavior/cluster/ClientBehavior.d.ts +1 -1
  72. package/dist/esm/behavior/cluster/ClusterBehavior.d.ts +1 -1
  73. package/dist/esm/behavior/cluster/ClusterBehaviorType.d.ts +1 -1
  74. package/dist/esm/behavior/cluster/ClusterBehaviorType.js +3 -3
  75. package/dist/esm/behavior/cluster/ClusterBehaviorType.js.map +1 -1
  76. package/dist/esm/behavior/state/validation/ValueValidator.js +14 -4
  77. package/dist/esm/behavior/state/validation/ValueValidator.js.map +1 -1
  78. package/dist/esm/behavior/state/validation/assertions.d.ts.map +1 -1
  79. package/dist/esm/behavior/state/validation/assertions.js +22 -1
  80. package/dist/esm/behavior/state/validation/assertions.js.map +1 -1
  81. package/dist/esm/behavior/system/commissioning/CommissioningClient.d.ts.map +1 -1
  82. package/dist/esm/behavior/system/commissioning/CommissioningClient.js +1 -3
  83. package/dist/esm/behavior/system/commissioning/CommissioningClient.js.map +1 -1
  84. package/dist/esm/behavior/system/controller/ControllerBehavior.d.ts +2 -0
  85. package/dist/esm/behavior/system/controller/ControllerBehavior.d.ts.map +1 -1
  86. package/dist/esm/behavior/system/controller/ControllerBehavior.js +5 -2
  87. package/dist/esm/behavior/system/controller/ControllerBehavior.js.map +1 -1
  88. package/dist/esm/behavior/system/network/ServerNetworkRuntime.d.ts +1 -0
  89. package/dist/esm/behavior/system/network/ServerNetworkRuntime.d.ts.map +1 -1
  90. package/dist/esm/behavior/system/network/ServerNetworkRuntime.js +8 -2
  91. package/dist/esm/behavior/system/network/ServerNetworkRuntime.js.map +1 -1
  92. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
  93. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js +88 -34
  94. package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
  95. package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.d.ts.map +1 -1
  96. package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.js +16 -3
  97. package/dist/esm/behaviors/group-key-management/GroupKeyManagementServer.js.map +1 -1
  98. package/dist/esm/behaviors/identify/IdentifyServer.d.ts.map +1 -1
  99. package/dist/esm/behaviors/identify/IdentifyServer.js +5 -0
  100. package/dist/esm/behaviors/identify/IdentifyServer.js.map +1 -1
  101. package/dist/esm/behaviors/service-area/ServiceAreaServer.js +1 -1
  102. package/dist/esm/behaviors/switch/SwitchServer.js +2 -2
  103. package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts +10 -0
  104. package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
  105. package/dist/esm/behaviors/thermostat/ThermostatServer.js +59 -22
  106. package/dist/esm/behaviors/thermostat/ThermostatServer.js.map +1 -1
  107. package/dist/esm/endpoint/Endpoint.d.ts +2 -2
  108. package/dist/esm/endpoint/Endpoint.d.ts.map +1 -1
  109. package/dist/esm/endpoint/properties/Endpoints.d.ts +2 -2
  110. package/dist/esm/endpoint/properties/Endpoints.d.ts.map +1 -1
  111. package/dist/esm/endpoint/properties/Endpoints.js +12 -1
  112. package/dist/esm/endpoint/properties/Endpoints.js.map +1 -1
  113. package/dist/esm/node/ClientNode.d.ts +7 -2
  114. package/dist/esm/node/ClientNode.d.ts.map +1 -1
  115. package/dist/esm/node/ClientNode.js +10 -3
  116. package/dist/esm/node/ClientNode.js.map +1 -1
  117. package/dist/esm/node/Node.d.ts.map +1 -1
  118. package/dist/esm/node/Node.js +9 -3
  119. package/dist/esm/node/Node.js.map +1 -1
  120. package/dist/esm/node/NodeLifecycle.d.ts +1 -1
  121. package/dist/esm/node/NodeLifecycle.js +1 -1
  122. package/dist/esm/node/ServerNode.d.ts.map +1 -1
  123. package/dist/esm/node/ServerNode.js +11 -4
  124. package/dist/esm/node/ServerNode.js.map +1 -1
  125. package/dist/esm/node/client/ClientEndpointInitializer.js +1 -1
  126. package/dist/esm/node/client/ClientEndpointInitializer.js.map +1 -1
  127. package/dist/esm/node/client/ClientEventEmitter.d.ts.map +1 -1
  128. package/dist/esm/node/client/ClientEventEmitter.js +9 -4
  129. package/dist/esm/node/client/ClientEventEmitter.js.map +1 -1
  130. package/dist/esm/node/client/ClientStructure.d.ts.map +1 -1
  131. package/dist/esm/node/client/ClientStructure.js +80 -96
  132. package/dist/esm/node/client/ClientStructure.js.map +1 -1
  133. package/dist/esm/node/client/Peers.d.ts.map +1 -1
  134. package/dist/esm/node/client/Peers.js +24 -5
  135. package/dist/esm/node/client/Peers.js.map +1 -1
  136. package/dist/esm/node/server/ServerEnvironment.d.ts.map +1 -1
  137. package/dist/esm/node/server/ServerEnvironment.js +1 -4
  138. package/dist/esm/node/server/ServerEnvironment.js.map +1 -1
  139. package/package.json +7 -7
  140. package/src/behavior/Events.ts +1 -1
  141. package/src/behavior/cluster/ClientBehavior.ts +1 -1
  142. package/src/behavior/cluster/ClusterBehavior.ts +1 -1
  143. package/src/behavior/cluster/ClusterBehaviorType.ts +4 -5
  144. package/src/behavior/cluster/ValidatedElements.ts +1 -1
  145. package/src/behavior/state/validation/ValueValidator.ts +16 -4
  146. package/src/behavior/state/validation/assertions.ts +27 -1
  147. package/src/behavior/system/commissioning/CommissioningClient.ts +2 -4
  148. package/src/behavior/system/controller/ControllerBehavior.ts +8 -3
  149. package/src/behavior/system/network/ServerNetworkRuntime.ts +11 -2
  150. package/src/behaviors/general-diagnostics/GeneralDiagnosticsServer.ts +2 -1
  151. package/src/behaviors/group-key-management/GroupKeyManagementServer.ts +19 -3
  152. package/src/behaviors/identify/IdentifyServer.ts +9 -0
  153. package/src/behaviors/service-area/ServiceAreaServer.ts +1 -1
  154. package/src/behaviors/switch/SwitchServer.ts +2 -2
  155. package/src/behaviors/thermostat/ThermostatServer.ts +65 -22
  156. package/src/endpoint/Endpoint.ts +2 -2
  157. package/src/endpoint/properties/Endpoints.ts +16 -3
  158. package/src/node/ClientNode.ts +11 -4
  159. package/src/node/Node.ts +10 -3
  160. package/src/node/NodeLifecycle.ts +1 -1
  161. package/src/node/ServerNode.ts +12 -4
  162. package/src/node/client/ClientEndpointInitializer.ts +1 -1
  163. package/src/node/client/ClientEventEmitter.ts +9 -4
  164. package/src/node/client/ClientStructure.ts +67 -22
  165. package/src/node/client/Peers.ts +31 -4
  166. package/src/node/server/ServerEnvironment.ts +1 -6
@@ -22,6 +22,8 @@ import {
22
22
  Observable,
23
23
  } from "#general";
24
24
  import { FieldElement } from "#model";
25
+ import { Node } from "#node/index.js";
26
+ import { ServerNode } from "#node/ServerNode.js";
25
27
  import { hasLocalActor, Val } from "#protocol";
26
28
  import { ClusterType, StatusResponse, TypeFromPartialBitSchema } from "#types";
27
29
  import { AtomicWriteHandler } from "./AtomicWriteHandler.js";
@@ -123,10 +125,11 @@ export class ThermostatBaseServer extends ThermostatBehaviorLogicBase {
123
125
  throw new ImplementationError("minSetpointDeadBand is out of valid range 0..127");
124
126
  }
125
127
 
128
+ const node = Node.forEndpoint(this.endpoint);
129
+ this.reactTo(node.lifecycle.online, this.#nodeOnline);
130
+
126
131
  // Initialize all the validation and logic handling
127
132
  this.#setupValidations();
128
- this.#setupTemperatureMeasurementIntegration();
129
- this.#setupOccupancyIntegration();
130
133
  this.#setupModeHandling();
131
134
  this.#setupThermostatLogic();
132
135
  this.#setupPresets();
@@ -136,6 +139,13 @@ export class ThermostatBaseServer extends ThermostatBehaviorLogicBase {
136
139
  this.internal.controlSequenceOfOperation = this.state.controlSequenceOfOperation;
137
140
  }
138
141
 
142
+ #nodeOnline() {
143
+ // TODO Also react to structure changes to add/remove endpoints that are being added or removed but might be used
144
+ // for temperature or occupancy sensing
145
+ this.#setupTemperatureMeasurementIntegration();
146
+ this.#setupOccupancyIntegration();
147
+ }
148
+
139
149
  /**
140
150
  * The default implementation of the SetpointRaiseLower command. It handles all validation and setpoint adjustments
141
151
  * required by the Matter specification. This method only changes the Occupied setpoints.
@@ -415,20 +425,29 @@ export class ThermostatBaseServer extends ThermostatBehaviorLogicBase {
415
425
  }
416
426
 
417
427
  let localTemperature = null;
418
- if (!preferRemoteTemperature && this.agent.has(TemperatureMeasurementServer)) {
419
- logger.debug(
420
- "Using existing TemperatureMeasurement cluster on same endpoint for local temperature measurement",
421
- );
422
- if (this.state.externalMeasuredIndoorTemperature !== undefined) {
428
+ const localTempEndpoint = this.state.localIndoorTemperatureMeasurementEndpoint;
429
+ if (!preferRemoteTemperature && localTempEndpoint !== undefined) {
430
+ const endpoints = this.env.get(ServerNode).endpoints;
431
+ const endpoint = endpoints.has(localTempEndpoint) ? endpoints.for(localTempEndpoint) : undefined;
432
+ if (endpoint !== undefined && endpoint.behaviors.has(TemperatureMeasurementServer)) {
433
+ logger.debug(
434
+ `Using existing TemperatureMeasurement cluster on endpoint #${localTempEndpoint} for local temperature measurement`,
435
+ );
436
+ if (this.state.externalMeasuredIndoorTemperature !== undefined) {
437
+ logger.warn(
438
+ "Both local TemperatureMeasurement cluster and externalMeasuredIndoorTemperature state are set, using local cluster",
439
+ );
440
+ }
441
+ this.reactTo(
442
+ endpoint.eventsOf(TemperatureMeasurementServer).measuredValue$Changed,
443
+ this.#handleMeasuredTemperatureChange,
444
+ );
445
+ localTemperature = endpoint.stateOf(TemperatureMeasurementServer).measuredValue;
446
+ } else {
423
447
  logger.warn(
424
- "Both local TemperatureMeasurement cluster and externalMeasuredIndoorTemperature state are set, using local cluster",
448
+ `No TemperatureMeasurement cluster found on endpoint #${localTempEndpoint}, falling back to externalMeasuredIndoorTemperature state if set`,
425
449
  );
426
450
  }
427
- this.reactTo(
428
- this.agent.get(TemperatureMeasurementServer).events.measuredValue$Changed,
429
- this.#handleMeasuredTemperatureChange,
430
- );
431
- localTemperature = this.endpoint.stateOf(TemperatureMeasurementServer).measuredValue;
432
451
  } else {
433
452
  if (this.state.externalMeasuredIndoorTemperature === undefined) {
434
453
  logger.warn(
@@ -440,7 +459,9 @@ export class ThermostatBaseServer extends ThermostatBehaviorLogicBase {
440
459
  }
441
460
  this.reactTo(this.events.externalMeasuredIndoorTemperature$Changed, this.#handleMeasuredTemperatureChange);
442
461
  }
443
- this.#handleMeasuredTemperatureChange(localTemperature); // and initialize
462
+ if (localTemperature !== null) {
463
+ this.#handleMeasuredTemperatureChange(localTemperature); // and initialize
464
+ }
444
465
  }
445
466
 
446
467
  /**
@@ -472,20 +493,30 @@ export class ThermostatBaseServer extends ThermostatBehaviorLogicBase {
472
493
  if (!this.features.occupancy) {
473
494
  return;
474
495
  }
475
- let currentOccupancy: boolean;
496
+ let currentOccupancy = true;
476
497
  const preferRemoteOccupancy = !!this.state.remoteSensing?.occupancy;
477
- if (!preferRemoteOccupancy && this.agent.has(OccupancySensingServer)) {
478
- logger.debug("Using existing OccupancySensing cluster on same endpoint for local occupancy sensing");
479
- if (this.state.externallyMeasuredOccupancy !== undefined) {
498
+ const localOccupancyEndpoint = this.state.localOccupancyMeasurementEndpoint;
499
+ if (!preferRemoteOccupancy && localOccupancyEndpoint !== undefined) {
500
+ const endpoints = this.env.get(ServerNode).endpoints;
501
+ const endpoint = endpoints.has(localOccupancyEndpoint) ? endpoints.for(localOccupancyEndpoint) : undefined;
502
+ if (endpoint !== undefined && endpoint.behaviors.has(OccupancySensingServer)) {
503
+ logger.debug(
504
+ `Using existing OccupancySensing cluster on endpoint ${localOccupancyEndpoint} for local occupancy sensing`,
505
+ );
506
+ if (this.state.externallyMeasuredOccupancy !== undefined) {
507
+ logger.warn(
508
+ "Both local OccupancySensing cluster and externallyMeasuredOccupancy state are set, using local cluster",
509
+ );
510
+ }
511
+ this.reactTo(endpoint.eventsOf(OccupancySensingServer).occupancy$Changed, this.#handleOccupancyChange);
512
+ currentOccupancy = !!endpoint.stateOf(OccupancySensingServer).occupancy.occupied;
513
+ } else {
480
514
  logger.warn(
481
- "Both local OccupancySensing cluster and externallyMeasuredOccupancy state are set, using local cluster",
515
+ `No OccupancySensing cluster found on endpoint ${localOccupancyEndpoint}, falling back to externallyMeasuredOccupancy state if set`,
482
516
  );
483
517
  }
484
- this.reactTo(this.agent.get(OccupancySensingServer).events.occupancy$Changed, this.#handleOccupancyChange);
485
- currentOccupancy = !!this.endpoint.stateOf(OccupancySensingServer).occupancy.occupied;
486
518
  } else {
487
519
  if (this.state.externallyMeasuredOccupancy === undefined) {
488
- currentOccupancy = true;
489
520
  logger.warn(
490
521
  "No local OccupancySensing cluster available and externallyMeasuredOccupancy state not set",
491
522
  );
@@ -1354,6 +1385,12 @@ export namespace ThermostatBaseServer {
1354
1385
  */
1355
1386
  externalMeasuredIndoorTemperature?: number;
1356
1387
 
1388
+ /**
1389
+ * Endpoint (Number or string-Id) where to find the indoor temperature measurement cluster to use as
1390
+ * local temperature measurement for the thermostat behavior.
1391
+ */
1392
+ localIndoorTemperatureMeasurementEndpoint?: number | string;
1393
+
1357
1394
  /**
1358
1395
  * Otherwise measured occupancy as boolean.
1359
1396
  * Use this if you have an external occupancy sensor that should be used for thermostat control instead of a
@@ -1361,6 +1398,12 @@ export namespace ThermostatBaseServer {
1361
1398
  */
1362
1399
  externallyMeasuredOccupancy?: boolean;
1363
1400
 
1401
+ /**
1402
+ * Endpoint (Number or string-Id) where to find the occupancy sensing cluster to use as
1403
+ * local occupancy measurement for the thermostat behavior.
1404
+ */
1405
+ localOccupancyMeasurementEndpoint?: number | string;
1406
+
1364
1407
  /**
1365
1408
  * Use to enable the automatic mode management, implemented by this standard implementation. This is beyond
1366
1409
  * Matter specification! It reacts to temperature changes to adjust system running mode automatically. It also
@@ -203,12 +203,12 @@ export class Endpoint<T extends EndpointType = EndpointType.Empty> {
203
203
  /**
204
204
  * Version of {@link stateOf} that returns undefined instead of throwing if the requested behavior unsupported.
205
205
  */
206
- maybeStateOf(type: string): Immutable<Val.Struct>;
206
+ maybeStateOf(type: string): Immutable<Val.Struct> | undefined;
207
207
 
208
208
  /**
209
209
  * Version of {@link stateOf} that returns undefined instead of throwing if the requested behavior unsupported.
210
210
  */
211
- maybeStateOf<T extends Behavior.Type>(type: T): Immutable<Behavior.StateOf<T>>;
211
+ maybeStateOf<T extends Behavior.Type>(type: T): Immutable<Behavior.StateOf<T>> | undefined;
212
212
 
213
213
  maybeStateOf(type: Behavior.Type | string) {
214
214
  if (typeof type === "string") {
@@ -25,7 +25,7 @@ export class Endpoints implements ImmutableSet<Endpoint> {
25
25
  return this.#node;
26
26
  }
27
27
 
28
- has(endpoint: Endpoint | number): boolean {
28
+ has(endpoint: Endpoint | number | string): boolean {
29
29
  if (endpoint === this.#node || endpoint === 0) {
30
30
  return true;
31
31
  }
@@ -34,6 +34,10 @@ export class Endpoints implements ImmutableSet<Endpoint> {
34
34
  return endpoint in this.#index;
35
35
  }
36
36
 
37
+ if (typeof endpoint === "string") {
38
+ return endpoint in this.#idIndex;
39
+ }
40
+
37
41
  return endpoint.lifecycle.hasNumber && endpoint.number in this.#index;
38
42
  }
39
43
 
@@ -57,12 +61,12 @@ export class Endpoints implements ImmutableSet<Endpoint> {
57
61
  return this.#list[Symbol.iterator]();
58
62
  }
59
63
 
60
- for(number: number): Endpoint {
64
+ for(number: number | string): Endpoint {
61
65
  if (number === 0) {
62
66
  return this.#node;
63
67
  }
64
68
 
65
- const endpoint = this.#index[number];
69
+ const endpoint = typeof number === "string" ? this.#idIndex[number] : this.#index[number];
66
70
  if (endpoint === undefined) {
67
71
  throw new StatusResponse.NotFoundError(`Endpoint ${number} does not exist`);
68
72
  }
@@ -78,6 +82,15 @@ export class Endpoints implements ImmutableSet<Endpoint> {
78
82
  return this.#node.behaviors.internalsOf(IndexBehavior).partsByNumber;
79
83
  }
80
84
 
85
+ /**
86
+ * Object mapping Endpoint-Id -> Endpoint.
87
+ *
88
+ * Note that this does not include endpoint 0, but we have that in #node.
89
+ */
90
+ get #idIndex() {
91
+ return this.#node.behaviors.internalsOf(IndexBehavior).partsById;
92
+ }
93
+
81
94
  /**
82
95
  * Full list of endpoints. Includes endpoint 0.
83
96
  */
@@ -110,22 +110,29 @@ export class ClientNode extends Node<ClientNode.RootEndpoint> {
110
110
 
111
111
  /**
112
112
  * Remove this node from the fabric (if commissioned) and locally.
113
+ * This method tries to communicate with the device to decommission it properly and will fail if the device is
114
+ * unreachable.
115
+ * If you can not reach the device, use {@link delete} instead.
113
116
  */
114
- override async delete() {
117
+ async decommission() {
115
118
  if (this.lifecycle.isCommissioned) {
116
119
  this.statusUpdate("decommissioning");
117
120
 
118
121
  await this.act("decommission", agent => agent.commissioning.decommission());
119
122
  }
120
-
121
- await super.delete();
123
+ await this.delete();
122
124
  }
123
125
 
124
126
  /**
125
127
  * Force-remove the node without first decommissioning.
126
128
  *
127
- * If the node is still available you should use {@link delete} to remove it from the fabric.
129
+ * If the node is still available you should use {@link decommission} to remove it properly from the fabric and only use
130
+ * this method as fallback. You should also tell the user that he needs to manually factory-reset the device.
128
131
  */
132
+ override async delete() {
133
+ await super.delete();
134
+ }
135
+
129
136
  override async erase() {
130
137
  await this.lifecycle.mutex.produce(this.eraseWithMutex.bind(this));
131
138
  }
package/src/node/Node.ts CHANGED
@@ -40,6 +40,7 @@ const logger = Logger.get("Node");
40
40
  export abstract class Node<T extends Node.CommonRootEndpoint = Node.CommonRootEndpoint> extends Endpoint<T> {
41
41
  #environment: Environment;
42
42
  #runtime?: NetworkRuntime;
43
+ #startInProgress = false;
43
44
 
44
45
  constructor(config: Node.Configuration<T>) {
45
46
  const parentEnvironment = config.environment ?? config.owner?.env ?? Environment.default;
@@ -96,12 +97,18 @@ export abstract class Node<T extends Node.CommonRootEndpoint = Node.CommonRootEn
96
97
  * Bring the node online.
97
98
  */
98
99
  async start() {
99
- if (this.lifecycle.isOnline) {
100
+ if (this.lifecycle.isOnline || this.#startInProgress) {
100
101
  return;
101
102
  }
102
- this.lifecycle.targetState = "online";
103
103
 
104
- await this.lifecycle.mutex.produce(this.startWithMutex.bind(this));
104
+ this.#startInProgress = true;
105
+ try {
106
+ this.lifecycle.targetState = "online";
107
+
108
+ await this.lifecycle.mutex.produce(this.startWithMutex.bind(this));
109
+ } finally {
110
+ this.#startInProgress = false;
111
+ }
105
112
  }
106
113
 
107
114
  protected async startWithMutex() {
@@ -103,7 +103,7 @@ export class NodeLifecycle extends EndpointLifecycle {
103
103
  }
104
104
 
105
105
  /**
106
- * Emits when node is no longer comissioned.
106
+ * Emits when node is no longer commissioned.
107
107
  */
108
108
  get decommissioned() {
109
109
  return this.#decommissioned;
@@ -105,13 +105,16 @@ export class ServerNode<T extends ServerNode.RootEndpoint = ServerNode.RootEndpo
105
105
  }
106
106
 
107
107
  override async [Construction.destruct]() {
108
+ if (this.#peers) {
109
+ await this.#peers.close();
110
+ }
111
+
108
112
  await super[Construction.destruct]();
109
113
  await ServerEnvironment.close(this);
110
114
  }
111
115
 
112
116
  override async prepareRuntimeShutdown() {
113
- const sessions = this.env.get(SessionManager);
114
- await sessions.close();
117
+ await this.env.get(SessionManager).closeAllSessions();
115
118
  }
116
119
 
117
120
  /**
@@ -160,8 +163,13 @@ export class ServerNode<T extends ServerNode.RootEndpoint = ServerNode.RootEndpo
160
163
  */
161
164
  get peers() {
162
165
  if (!this.#peers) {
163
- this.#peers = new Peers(this);
164
- this.#peers.initialize();
166
+ try {
167
+ this.#peers = new Peers(this);
168
+ this.#peers.initialize();
169
+ } catch (e) {
170
+ this.#peers = undefined;
171
+ throw e;
172
+ }
165
173
  }
166
174
 
167
175
  return this.#peers;
@@ -59,7 +59,7 @@ export class ClientEndpointInitializer extends EndpointInitializer {
59
59
 
60
60
  // Activate remote behavior
61
61
  const store = this.structure.storeForRemote(endpoint, peerType);
62
- return new ClientBehaviorBacking(endpoint, type, store, endpoint.behaviors.optionsFor(peerType));
62
+ return new ClientBehaviorBacking(endpoint, peerType, store, endpoint.behaviors.optionsFor(peerType));
63
63
  }
64
64
 
65
65
  get structure() {
@@ -5,11 +5,11 @@
5
5
  */
6
6
 
7
7
  import type { ElementEvent, Events } from "#behavior/Events.js";
8
- import { camelize, Diagnostic, Logger } from "#general";
8
+ import { camelize, Diagnostic, isObject, Logger } from "#general";
9
9
  import { ClusterModel, EventModel, MatterModel } from "#model";
10
10
  import type { ClientNode } from "#node/ClientNode.js";
11
11
  import type { ReadResult } from "#protocol";
12
- import type { ClusterId, EndpointNumber, EventId } from "#types";
12
+ import type { ClusterId, EventId } from "#types";
13
13
  import type { ClientStructure } from "./ClientStructure.js";
14
14
 
15
15
  const logger = Logger.get("ClientEventEmitter");
@@ -46,7 +46,7 @@ export function ClientEventEmitter(node: ClientNode, structure: ClientStructure)
46
46
  return;
47
47
  }
48
48
 
49
- const event = getEvent(node, occurrence.path.endpointId, names.cluster, names.event);
49
+ const event = getEvent(node, occurrence, names.cluster, names.event);
50
50
  if (event) {
51
51
  node.act(agent => {
52
52
  // Current ActionContext is not writable, could skip act() but meh, see TODO above
@@ -56,7 +56,11 @@ export function ClientEventEmitter(node: ClientNode, structure: ClientStructure)
56
56
  }
57
57
  }
58
58
 
59
- function getEvent(node: ClientNode, endpointId: EndpointNumber, clusterName: string, eventName: string) {
59
+ function getEvent(node: ClientNode, occurrence: ReadResult.EventValue, clusterName: string, eventName: string) {
60
+ const {
61
+ value,
62
+ path: { endpointId },
63
+ } = occurrence;
60
64
  const endpoint = structure.endpointFor(endpointId);
61
65
  if (endpoint === undefined) {
62
66
  logger.warn(`Received event for unsupported endpoint #${endpointId} on ${node}`);
@@ -74,6 +78,7 @@ export function ClientEventEmitter(node: ClientNode, structure: ClientStructure)
74
78
  Diagnostic.strong(`${clusterName}.${eventName}`),
75
79
  " on ",
76
80
  Diagnostic.strong(endpoint.toString()),
81
+ Diagnostic.weak(isObject(value) ? Diagnostic.dict(value) : value),
77
82
  );
78
83
 
79
84
  return events[eventName];
@@ -28,6 +28,7 @@ import { ReadScope, type Read, type ReadResult } from "#protocol";
28
28
  import { DatasourceCache } from "#storage/client/DatasourceCache.js";
29
29
  import { ClientNodeStore } from "#storage/index.js";
30
30
  import type { AttributeId, ClusterId, ClusterType, CommandId, EndpointNumber } from "#types";
31
+ import { Status } from "#types";
31
32
  import { ClientEventEmitter } from "./ClientEventEmitter.js";
32
33
  import { ClientStructureEvents } from "./ClientStructureEvents.js";
33
34
  import { PeerBehavior } from "./PeerBehavior.js";
@@ -44,11 +45,12 @@ const PARTS_LIST_ATTR_ID = Descriptor.Cluster.attributes.partsList.id;
44
45
  export class ClientStructure {
45
46
  #nodeStore: ClientNodeStore;
46
47
  #endpoints = new Map<EndpointNumber, EndpointStructure>();
47
- #emitEvent: ClientEventEmitter;
48
+ #eventEmitter: ClientEventEmitter;
48
49
  #node: ClientNode;
49
50
  #subscribedFabricFiltered?: boolean;
50
51
  #pendingChanges = new Map<EndpointStructure, PendingChange>();
51
- #pendingEvents = Array<PendingEvent>();
52
+ #pendingStructureEvents = Array<PendingEvent>();
53
+ #delayedClusterEvents = new Array<ReadResult.EventValue>();
52
54
  #events: ClientStructureEvents;
53
55
 
54
56
  constructor(node: ClientNode) {
@@ -58,7 +60,7 @@ export class ClientStructure {
58
60
  endpoint: node,
59
61
  clusters: new Map(),
60
62
  });
61
- this.#emitEvent = ClientEventEmitter(node, this);
63
+ this.#eventEmitter = ClientEventEmitter(node, this);
62
64
  this.#events = this.#node.env.get(ClientStructureEvents);
63
65
  }
64
66
 
@@ -103,7 +105,7 @@ export class ClientStructure {
103
105
  this.#install(structure);
104
106
  }
105
107
 
106
- this.#emitPendingEvents();
108
+ this.#emitPendingStructureEvents();
107
109
  }
108
110
 
109
111
  /**
@@ -167,9 +169,6 @@ export class ClientStructure {
167
169
  * Update the node structure by applying attribute changes.
168
170
  */
169
171
  async *mutate(request: Read, changes: ReadResult) {
170
- // Ensure mutations run serially and integrate properly with node lifecycle
171
- using _lock = await this.#node.lifecycle.mutex.lock();
172
-
173
172
  // We collect updates and only apply when we transition clusters
174
173
  let currentUpdates: AttributeUpdates | undefined;
175
174
 
@@ -183,10 +182,18 @@ export class ClientStructure {
183
182
  break;
184
183
 
185
184
  case "event-value":
186
- this.#emitEvent(change);
185
+ this.#emitEvent(change, currentUpdates);
187
186
  break;
188
187
 
189
- // we ignore attr-status and event-status for now
188
+ case "attr-status":
189
+ case "event-status":
190
+ logger.debug(
191
+ "Received status for",
192
+ change.kind === "attr-status" ? "attribute" : "event",
193
+ Diagnostic.strong(change.path.toString()),
194
+ `: ${Status[change.status]}#${change.status}${change.clusterStatus !== undefined ? `/${Status[change.clusterStatus]}#${change.clusterStatus}` : ""}`,
195
+ );
196
+ break;
190
197
  }
191
198
  }
192
199
 
@@ -218,7 +225,7 @@ export class ClientStructure {
218
225
  }
219
226
 
220
227
  // Likewise, we don't emit events until we've applied all structural changes
221
- this.#emitPendingEvents();
228
+ this.#emitPendingStructureEvents();
222
229
  }
223
230
 
224
231
  /** Reference to the default subscription used when the node was started. */
@@ -273,6 +280,21 @@ export class ClientStructure {
273
280
  return currentUpdates;
274
281
  }
275
282
 
283
+ #emitEvent(occurrence: ReadResult.EventValue, currentUpdates?: AttributeUpdates) {
284
+ const { endpointId, clusterId } = occurrence.path;
285
+
286
+ const endpoint = this.#endpoints.get(endpointId);
287
+ // If we are building updates on current cluster or endpoint has pending changes, delay event emission
288
+ if (
289
+ (currentUpdates && (currentUpdates.endpointId === endpointId || currentUpdates.clusterId === clusterId)) ||
290
+ (endpoint !== undefined && this.#pendingChanges?.has(endpoint))
291
+ ) {
292
+ this.#delayedClusterEvents.push(occurrence);
293
+ } else {
294
+ this.#eventEmitter(occurrence);
295
+ }
296
+ }
297
+
276
298
  /**
277
299
  * Obtain the {@link ClusterType} for an {@link EndpointNumber} and {@link ClusterId}.
278
300
  */
@@ -308,13 +330,27 @@ export class ClientStructure {
308
330
  }
309
331
 
310
332
  if (cluster.behavior && AttributeList.id in attrs.values) {
311
- if (!isDeepEqual(cluster.attributes, attrs.values[AttributeList.id])) {
333
+ const attributeList = attrs.values[AttributeList.id];
334
+ if (
335
+ Array.isArray(attributeList) &&
336
+ !isDeepEqual(
337
+ cluster.attributes,
338
+ attributeList.sort((a, b) => a - b),
339
+ )
340
+ ) {
312
341
  cluster.behavior = undefined;
313
342
  }
314
343
  }
315
344
 
316
345
  if (cluster.behavior && AcceptedCommandList.id in attrs.values) {
317
- if (!isDeepEqual(cluster.attributes, attrs.values[AttributeList.id])) {
346
+ const acceptedCommands = attrs.values[AcceptedCommandList.id];
347
+ if (
348
+ Array.isArray(acceptedCommands) &&
349
+ !isDeepEqual(
350
+ cluster.commands,
351
+ acceptedCommands.sort((a, b) => a - b),
352
+ )
353
+ ) {
318
354
  cluster.behavior = undefined;
319
355
  }
320
356
  }
@@ -353,11 +389,15 @@ export class ClientStructure {
353
389
  }
354
390
 
355
391
  if (Array.isArray(attributeList)) {
356
- cluster.attributes = attributeList.filter(attr => typeof attr === "number") as AttributeId[];
392
+ cluster.attributes = (attributeList.filter(attr => typeof attr === "number") as AttributeId[]).sort(
393
+ (a, b) => a - b,
394
+ );
357
395
  }
358
396
 
359
397
  if (Array.isArray(commandList)) {
360
- cluster.commands = commandList.filter(cmd => typeof cmd === "number") as CommandId[];
398
+ cluster.commands = (commandList.filter(cmd => typeof cmd === "number") as CommandId[]).sort(
399
+ (a, b) => a - b,
400
+ );
361
401
  }
362
402
  }
363
403
 
@@ -611,7 +651,7 @@ export class ClientStructure {
611
651
  logger.error("Error clearing cluster storage:", e);
612
652
  }
613
653
 
614
- this.#pendingEvents.push({
654
+ this.#pendingStructureEvents.push({
615
655
  kind: "cluster",
616
656
  endpoint: structure,
617
657
  cluster,
@@ -632,7 +672,7 @@ export class ClientStructure {
632
672
  cluster.behavior = pendingBehavior;
633
673
  delete cluster.pendingBehavior;
634
674
 
635
- this.#pendingEvents.push({
675
+ this.#pendingStructureEvents.push({
636
676
  kind: "cluster",
637
677
  subkind,
638
678
  endpoint: structure,
@@ -651,7 +691,7 @@ export class ClientStructure {
651
691
  if (pendingOwner) {
652
692
  endpoint.owner = pendingOwner.endpoint;
653
693
  structure.pendingOwner = undefined;
654
- this.#pendingEvents.push({ kind: "endpoint", endpoint: structure });
694
+ this.#pendingStructureEvents.push({ kind: "endpoint", endpoint: structure });
655
695
  }
656
696
 
657
697
  // Handle behavior installation
@@ -672,7 +712,7 @@ export class ClientStructure {
672
712
  // We emit cluster events during the endpoint event so only add cluster event manually if the endpoint is
673
713
  // already installed
674
714
  if (!pendingOwner) {
675
- this.#pendingEvents.push({
715
+ this.#pendingStructureEvents.push({
676
716
  kind: "cluster",
677
717
  subkind: "add",
678
718
  endpoint: structure,
@@ -700,10 +740,12 @@ export class ClientStructure {
700
740
  * We do this after all structural updates are complete so that listeners can expect composed parts and dependent
701
741
  * behaviors to be installed.
702
742
  */
703
- #emitPendingEvents() {
704
- const events = this.#pendingEvents;
705
- this.#pendingEvents = [];
706
- for (const event of events) {
743
+ #emitPendingStructureEvents() {
744
+ const structureEvents = this.#pendingStructureEvents;
745
+ const clusterEvents = this.#delayedClusterEvents;
746
+ this.#delayedClusterEvents = [];
747
+ this.#pendingStructureEvents = [];
748
+ for (const event of structureEvents) {
707
749
  switch (event.kind) {
708
750
  case "endpoint": {
709
751
  const {
@@ -747,6 +789,9 @@ export class ClientStructure {
747
789
  }
748
790
  }
749
791
  }
792
+ for (const occurrence of clusterEvents) {
793
+ this.#eventEmitter(occurrence);
794
+ }
750
795
  }
751
796
  }
752
797
 
@@ -29,6 +29,7 @@ import {
29
29
  Seconds,
30
30
  Time,
31
31
  Timestamp,
32
+ UninitializedDependencyError,
32
33
  } from "#general";
33
34
  import { ClientGroup } from "#node/ClientGroup.js";
34
35
  import { InteractionServer } from "#node/server/InteractionServer.js";
@@ -69,6 +70,10 @@ export class Peers extends EndpointContainer<ClientNode> {
69
70
  this.deleted.on(this.#manageExpiration.bind(this));
70
71
 
71
72
  this.clusterInstalled(BasicInformationClient).on(this.#instrumentBasicInformation.bind(this));
73
+
74
+ const lifecycle = owner.lifecycle;
75
+ lifecycle.online.on(this.#nodeOnline.bind(this));
76
+ lifecycle.offline.on(this.#nodeOffline.bind(this));
72
77
  }
73
78
 
74
79
  /**
@@ -77,7 +82,14 @@ export class Peers extends EndpointContainer<ClientNode> {
77
82
  initialize() {
78
83
  const factory = this.owner.env.get(ClientNodeFactory);
79
84
 
80
- const clientStores = this.owner.env.get(ServerNodeStore).clientStores;
85
+ const clientStores = this.owner.env.maybeGet(ServerNodeStore)?.clientStores;
86
+ if (clientStores === undefined) {
87
+ throw new UninitializedDependencyError(
88
+ "Peers",
89
+ "are not available because ServerNode initialization is incomplete",
90
+ );
91
+ }
92
+
81
93
  // Group nodes have an in-memory only store, so all nodes restored here are ClientNode
82
94
  for (const id of clientStores.knownIds) {
83
95
  this.add(
@@ -89,6 +101,21 @@ export class Peers extends EndpointContainer<ClientNode> {
89
101
  }
90
102
  }
91
103
 
104
+ async #nodeOnline() {
105
+ // TODO start all peers on node startup in a non blocking way respecting queuing for thread and such
106
+ /*for (const peer of this) {
107
+ await peer.start();
108
+ }*/
109
+ this.#manageExpiration();
110
+ }
111
+
112
+ async #nodeOffline() {
113
+ this.#cancelExpiration();
114
+ for (const peer of this) {
115
+ await peer.cancel();
116
+ }
117
+ }
118
+
92
119
  /**
93
120
  * Find a specific commissionable node.
94
121
  */
@@ -330,13 +357,13 @@ export class Peers extends EndpointContainer<ClientNode> {
330
357
 
331
358
  #onLeave(node: ClientNode, fabricIndex: FabricIndex) {
332
359
  this.#mutex.run(async () => {
333
- const { fabrics: peerFabrics } = node.maybeStateOf(OperationalCredentialsClient);
334
- const peerFabric = peerFabrics.find(fabric => fabric.fabricIndex === fabricIndex);
360
+ const { fabrics: peerFabrics } = node.maybeStateOf(OperationalCredentialsClient) ?? {};
361
+ const peerFabric = peerFabrics?.find(fabric => fabric.fabricIndex === fabricIndex);
335
362
  if (!peerFabric) {
336
363
  return;
337
364
  }
338
365
 
339
- const peerAddress = node.maybeStateOf(CommissioningClient).peerAddress;
366
+ const peerAddress = node.maybeStateOf(CommissioningClient)?.peerAddress;
340
367
  if (!peerAddress) {
341
368
  return;
342
369
  }