@project-chip/matter.js 0.16.0-alpha.0-20250930-05e6cc3f8 → 0.16.0-alpha.0-20251003-dc6d5523d

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 (49) hide show
  1. package/dist/cjs/CommissioningController.d.ts +2 -2
  2. package/dist/cjs/CommissioningController.d.ts.map +1 -1
  3. package/dist/cjs/CommissioningController.js +2 -2
  4. package/dist/cjs/CommissioningController.js.map +1 -1
  5. package/dist/cjs/MatterController.d.ts +6 -6
  6. package/dist/cjs/MatterController.d.ts.map +1 -1
  7. package/dist/cjs/MatterController.js +12 -12
  8. package/dist/cjs/MatterController.js.map +1 -1
  9. package/dist/cjs/PaseCommissioner.js +1 -1
  10. package/dist/cjs/PaseCommissioner.js.map +1 -1
  11. package/dist/cjs/device/CachedClientNodeStore.d.ts +5 -4
  12. package/dist/cjs/device/CachedClientNodeStore.d.ts.map +1 -1
  13. package/dist/cjs/device/CachedClientNodeStore.js +19 -2
  14. package/dist/cjs/device/CachedClientNodeStore.js.map +1 -1
  15. package/dist/cjs/device/Endpoint.d.ts +1 -1
  16. package/dist/cjs/device/Endpoint.d.ts.map +1 -1
  17. package/dist/cjs/device/Endpoint.js.map +1 -1
  18. package/dist/cjs/device/PairedNode.d.ts +52 -28
  19. package/dist/cjs/device/PairedNode.d.ts.map +1 -1
  20. package/dist/cjs/device/PairedNode.js +191 -76
  21. package/dist/cjs/device/PairedNode.js.map +1 -1
  22. package/dist/esm/CommissioningController.d.ts +2 -2
  23. package/dist/esm/CommissioningController.d.ts.map +1 -1
  24. package/dist/esm/CommissioningController.js +3 -3
  25. package/dist/esm/CommissioningController.js.map +1 -1
  26. package/dist/esm/MatterController.d.ts +6 -6
  27. package/dist/esm/MatterController.d.ts.map +1 -1
  28. package/dist/esm/MatterController.js +13 -13
  29. package/dist/esm/MatterController.js.map +1 -1
  30. package/dist/esm/PaseCommissioner.js +1 -1
  31. package/dist/esm/PaseCommissioner.js.map +1 -1
  32. package/dist/esm/device/CachedClientNodeStore.d.ts +5 -4
  33. package/dist/esm/device/CachedClientNodeStore.d.ts.map +1 -1
  34. package/dist/esm/device/CachedClientNodeStore.js +19 -2
  35. package/dist/esm/device/CachedClientNodeStore.js.map +1 -1
  36. package/dist/esm/device/Endpoint.d.ts +1 -1
  37. package/dist/esm/device/Endpoint.d.ts.map +1 -1
  38. package/dist/esm/device/Endpoint.js.map +1 -1
  39. package/dist/esm/device/PairedNode.d.ts +52 -28
  40. package/dist/esm/device/PairedNode.d.ts.map +1 -1
  41. package/dist/esm/device/PairedNode.js +192 -77
  42. package/dist/esm/device/PairedNode.js.map +1 -1
  43. package/package.json +8 -8
  44. package/src/CommissioningController.ts +3 -3
  45. package/src/MatterController.ts +18 -18
  46. package/src/PaseCommissioner.ts +1 -1
  47. package/src/device/CachedClientNodeStore.ts +24 -4
  48. package/src/device/Endpoint.ts +1 -1
  49. package/src/device/PairedNode.ts +286 -88
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { AdministratorCommissioning, BasicInformation, DescriptorCluster, OperationalCredentials } from "#clusters";
7
+ import { AdministratorCommissioning, BasicInformation, Descriptor, OperationalCredentials } from "#clusters";
8
8
  import {
9
9
  AsyncObservable,
10
10
  AtLeastOne,
@@ -36,6 +36,7 @@ import {
36
36
  NodeDiscoveryType,
37
37
  NodeSession,
38
38
  PaseClient,
39
+ StructuredReadAttributeData,
39
40
  UnknownNodeError,
40
41
  structureReadAttributeDataToClusterObject,
41
42
  } from "#protocol";
@@ -232,6 +233,22 @@ interface SubscriptionHandlerCallbacks {
232
233
  subscriptionAlive: () => void;
233
234
  }
234
235
 
236
+ type DescriptorData = AttributeClientValues<typeof Descriptor.Complete.attributes>;
237
+
238
+ /**
239
+ * Tooling function to check if a list of numbers is the same as another list of numbers.
240
+ * it uses Sets to prevent duplicate entries and ordering to cause issues if they ever happen.
241
+ */
242
+ function areNumberListsSame(list1: number[], list2: number[]) {
243
+ const set1 = new Set(list1);
244
+ const set2 = new Set(list2);
245
+ if (set1.size !== set2.size) return false;
246
+ for (const entry of set1.values()) {
247
+ if (!set2.has(entry)) return false;
248
+ }
249
+ return true;
250
+ }
251
+
235
252
  /**
236
253
  * Class to represents one node that is paired/commissioned with the matter.js Controller. Instances are returned by
237
254
  * the CommissioningController on commissioning or when connecting.
@@ -278,38 +295,24 @@ export class PairedNode {
278
295
  #currentSubscriptionIntervalS?: number;
279
296
  #crypto: Crypto;
280
297
 
281
- readonly events = {
282
- /**
283
- * Emitted when the node is initialized from local data. These data usually are stale, but you can still already
284
- * use the node to interact with the device. If no local data are available this event will be emitted together
285
- * with the initializedFromRemote event.
286
- */
287
- initialized: AsyncObservable<[details: DeviceInformationData]>(),
298
+ /**
299
+ * Endpoint structure change information that are checked when updating structure
300
+ * - null means that the endpoint itself changed, so will be regenerated completely any case
301
+ * - array of ClusterIds means that only these clusters changed and will be updated
302
+ */
303
+ #registeredEndpointStructureChanges = new Map<EndpointNumber, ClusterId[] | null>();
288
304
 
289
- /**
290
- * Emitted when the node is fully initialized from remote and all attributes and events are subscribed.
291
- * This event can also be awaited if code needs to be blocked until the node is fully initialized.
292
- */
305
+ readonly events: PairedNode.Events = {
306
+ initialized: AsyncObservable<[details: DeviceInformationData]>(),
293
307
  initializedFromRemote: AsyncObservable<[details: DeviceInformationData]>(),
294
-
295
- /** Emitted when the state of the node changes. */
296
308
  stateChanged: Observable<[nodeState: NodeStates]>(),
297
-
298
- /**
299
- * Emitted when an attribute value changes. If the oldValue is undefined then no former value was known.
300
- */
301
309
  attributeChanged: Observable<[data: DecodedAttributeReportValue<any>, oldValue: any]>(),
302
-
303
- /** Emitted when an event is triggered. */
304
310
  eventTriggered: Observable<[DecodedEventReportValue<any>]>(),
305
-
306
- /** Emitted when the structure of the node changes (Endpoints got added or also removed). */
307
311
  structureChanged: Observable<[void]>(),
308
-
309
- /** Emitted when the node is decommissioned. */
312
+ nodeEndpointAdded: Observable<[EndpointNumber]>(),
313
+ nodeEndpointRemoved: Observable<[EndpointNumber]>(),
314
+ nodeEndpointChanged: Observable<[EndpointNumber]>(),
310
315
  decommissioned: Observable<[void]>(),
311
-
312
- /** Emitted when a subscription alive trigger is received (max interval trigger or any data update) */
313
316
  connectionAlive: Observable<[void]>(),
314
317
  };
315
318
 
@@ -651,7 +654,7 @@ export class PairedNode {
651
654
  return;
652
655
  }
653
656
 
654
- await this.#initializeEndpointStructure(storedAttributeData);
657
+ await this.#initializeEndpointStructure(storedAttributeData, false);
655
658
 
656
659
  // Inform interested parties that the node is initialized
657
660
  await this.events.initialized.emit(this.#nodeDetails.toStorageData());
@@ -696,7 +699,7 @@ export class PairedNode {
696
699
  if (attributeReports === undefined) {
697
700
  throw new InternalError("No attribute reports received when subscribing to all values!");
698
701
  }
699
- await this.#initializeEndpointStructure(attributeReports, anyInitializationDone);
702
+ await this.#initializeEndpointStructure(attributeReports);
700
703
 
701
704
  this.#remoteInitializationInProgress = false; // We are done, rest is bonus and should not block reconnections
702
705
 
@@ -709,7 +712,7 @@ export class PairedNode {
709
712
  this.#currentSubscriptionIntervalS = maxInterval;
710
713
  } else {
711
714
  const allClusterAttributes = await this.readAllAttributes();
712
- await this.#initializeEndpointStructure(allClusterAttributes, anyInitializationDone);
715
+ await this.#initializeEndpointStructure(allClusterAttributes);
713
716
  this.#remoteInitializationInProgress = false; // We are done, rest is bonus and should not block reconnections
714
717
  }
715
718
  if (!this.#remoteInitializationDone) {
@@ -863,6 +866,16 @@ export class PairedNode {
863
866
  this.#reconnectDelayTimer = undefined;
864
867
  this.#setConnectionState(NodeStates.Connected);
865
868
  }
869
+
870
+ if (
871
+ this.#remoteInitializationDone &&
872
+ this.#registeredEndpointStructureChanges.size > 0 &&
873
+ !this.#updateEndpointStructureTimer.isRunning
874
+ ) {
875
+ logger.info(`Node ${this.nodeId}: Endpoint structure needs to be updated ...`);
876
+ this.#updateEndpointStructureTimer.stop().start();
877
+ }
878
+
866
879
  this.events.connectionAlive.emit();
867
880
  },
868
881
  };
@@ -872,12 +885,11 @@ export class PairedNode {
872
885
 
873
886
  // We first update all values by doing a read all on the device
874
887
  // We do not enrich existing data because we just want to store updated data
875
- const attributeData = await this.#interactionClient.getAllAttributes({
888
+ await this.#interactionClient.getAllAttributes({
876
889
  dataVersionFilters: this.#interactionClient.getCachedClusterDataVersions(),
877
890
  executeQueued: !!threadConnected, // We queue subscriptions for thread devices
891
+ attributeChangeListener: subscriptionHandler.attributeListener,
878
892
  });
879
- await this.#interactionClient.processAttributeUpdates(attributeData, subscriptionHandler.attributeListener);
880
- attributeData.length = 0; // Clear the array to save memory
881
893
 
882
894
  // If we subscribe anything we use these data to create the endpoint structure, so we do not need to fetch again
883
895
  const initialSubscriptionData = await this.#interactionClient.subscribeAllAttributesAndEvents({
@@ -910,35 +922,35 @@ export class PairedNode {
910
922
  }
911
923
 
912
924
  #checkAttributesForNeededStructureUpdate(
913
- _endpointId: EndpointNumber,
925
+ endpointId: EndpointNumber,
914
926
  clusterId: ClusterId,
915
927
  attributeId: AttributeId,
916
928
  ) {
917
929
  // Any change in the Descriptor Cluster partsList attribute requires a reinitialization of the endpoint structure
918
- let structureUpdateNeeded = false;
919
- if (clusterId === DescriptorCluster.id) {
920
- switch (attributeId) {
921
- case DescriptorCluster.attributes.partsList.id:
922
- case DescriptorCluster.attributes.serverList.id:
923
- case DescriptorCluster.attributes.deviceTypeList.id:
924
- structureUpdateNeeded = true;
925
- break;
926
- }
927
- }
928
- if (!structureUpdateNeeded) {
930
+ if (clusterId === Descriptor.Complete.id) {
929
931
  switch (attributeId) {
930
- case FeatureMap.id:
931
- case AttributeList.id:
932
- case AcceptedCommandList.id:
933
- case ClusterRevision.id:
934
- structureUpdateNeeded = true;
935
- break;
932
+ case Descriptor.Complete.attributes.partsList.id:
933
+ case Descriptor.Complete.attributes.serverList.id:
934
+ case Descriptor.Complete.attributes.clientList.id:
935
+ case Descriptor.Complete.attributes.deviceTypeList.id:
936
+ this.#registeredEndpointStructureChanges.set(endpointId, null); // full endpoint update needed
937
+ return;
936
938
  }
937
939
  }
938
-
939
- if (structureUpdateNeeded) {
940
- logger.info(`Node ${this.nodeId}: Endpoint structure needs to be updated ...`);
941
- this.#updateEndpointStructureTimer.stop().start();
940
+ switch (attributeId) {
941
+ case FeatureMap.id:
942
+ case AttributeList.id:
943
+ case AcceptedCommandList.id:
944
+ case ClusterRevision.id:
945
+ let knownForUpdate = this.#registeredEndpointStructureChanges.get(endpointId);
946
+ if (knownForUpdate !== null) {
947
+ knownForUpdate = knownForUpdate ?? [];
948
+ if (!knownForUpdate.includes(clusterId)) {
949
+ knownForUpdate.push(clusterId);
950
+ this.#registeredEndpointStructureChanges.set(endpointId, knownForUpdate);
951
+ }
952
+ }
953
+ break;
942
954
  }
943
955
  }
944
956
 
@@ -974,72 +986,206 @@ export class PairedNode {
974
986
  }
975
987
 
976
988
  async #updateEndpointStructure() {
977
- const allClusterAttributes = await this.readAllAttributes();
989
+ const allClusterAttributes = this.#interactionClient.getAllCachedClusterData();
978
990
  await this.#initializeEndpointStructure(allClusterAttributes, true);
979
- this.#options.stateInformationCallback?.(this.nodeId, NodeStateInformation.StructureChanged);
980
- this.events.structureChanged.emit();
991
+ }
992
+
993
+ /**
994
+ * Traverse the structure data and collect all data for the given endpointId.
995
+ * Return true if data for the endpoint was found, otherwise false.
996
+ * If data was found it is added to the collectedData map.
997
+ */
998
+ collectDescriptorData(
999
+ structure: StructuredReadAttributeData,
1000
+ endpointId: EndpointNumber,
1001
+ collectedData: Map<EndpointNumber, DescriptorData>,
1002
+ ) {
1003
+ if (collectedData.has(endpointId)) {
1004
+ return;
1005
+ }
1006
+ const endpointData = structure[endpointId];
1007
+ const descriptorData = endpointData?.[Descriptor.Complete.id] as DescriptorData | undefined;
1008
+ if (endpointData === undefined || descriptorData === undefined) {
1009
+ logger.info(`Descriptor data for endpoint ${endpointId} not found in structure! Ignoring endpoint ...`);
1010
+ return;
1011
+ }
1012
+ collectedData.set(endpointId, descriptorData);
1013
+ if (descriptorData.partsList.length) {
1014
+ for (const partEndpointId of descriptorData.partsList) {
1015
+ this.collectDescriptorData(structure, partEndpointId, collectedData);
1016
+ }
1017
+ }
1018
+ }
1019
+
1020
+ #hasEndpointChanged(device: Endpoint, descriptorData: DescriptorData) {
1021
+ // Check if the device types (ignoring revision for now), or cluster server or cluster clients differ
1022
+ return !(
1023
+ areNumberListsSame(
1024
+ device.getDeviceTypes().map(({ code }) => code),
1025
+ descriptorData.deviceTypeList.map(({ deviceType }) => deviceType),
1026
+ ) &&
1027
+ // Check if the cluster clients are the same - they map to the serverList attribute
1028
+ areNumberListsSame(
1029
+ device.getAllClusterClients().map(({ id }) => id),
1030
+ descriptorData.serverList,
1031
+ ) &&
1032
+ // Check if the cluster servers are the same - they map to the clientList attribute
1033
+ areNumberListsSame(
1034
+ device.getAllClusterServers().map(({ id }) => id),
1035
+ descriptorData.clientList,
1036
+ )
1037
+ );
981
1038
  }
982
1039
 
983
1040
  /** Reads all data from the device and create a device object structure out of it. */
984
1041
  async #initializeEndpointStructure(
985
1042
  allClusterAttributes: DecodedAttributeReportValue<any>[],
986
- updateStructure = false,
1043
+ updateStructure = this.#localInitializationDone || this.#remoteInitializationDone,
987
1044
  ) {
1045
+ if (this.#updateEndpointStructureTimer.isRunning) {
1046
+ this.#updateEndpointStructureTimer.stop();
1047
+ }
1048
+ const eventsToEmit = new Map<EndpointNumber, keyof PairedNode.NodeStructureEvents>();
1049
+ const structureUpdateDetails = this.#registeredEndpointStructureChanges;
1050
+ this.#registeredEndpointStructureChanges = new Map();
1051
+
988
1052
  const allData = structureReadAttributeDataToClusterObject(allClusterAttributes);
989
1053
 
1054
+ // Collect the descriptor data for all endpoints referenced in the structure
1055
+ const descriptors = new Map<EndpointNumber, DescriptorData>();
1056
+ this.collectDescriptorData(allData, EndpointNumber(0), descriptors);
1057
+
990
1058
  if (updateStructure) {
991
1059
  // Find out what we need to remove or retain
992
1060
  const endpointsToRemove = new Set<number>(this.#endpoints.keys());
993
- for (const [endpointId] of Object.entries(allData)) {
994
- const endpointIdNumber = EndpointNumber(parseInt(endpointId));
995
- if (this.#endpoints.has(endpointIdNumber)) {
996
- logger.debug(`Node ${this.nodeId}: Retaining device`, endpointId);
997
- endpointsToRemove.delete(endpointIdNumber);
1061
+ for (const endpointId of descriptors.keys()) {
1062
+ const device = this.#endpoints.get(endpointId);
1063
+ if (device !== undefined) {
1064
+ // Check if there are any changes to the device that require a re-creation
1065
+ // When structureUpdateDetails from subscription updates state changes we do a deep validation
1066
+ // to prevent ordering changes to cause unnecessary device re-creations
1067
+ const hasChanged = structureUpdateDetails.has(endpointId);
1068
+ if (!hasChanged || !this.#hasEndpointChanged(device, descriptors.get(endpointId)!)) {
1069
+ logger.debug(
1070
+ `Node ${this.nodeId}: Retaining endpoint`,
1071
+ endpointId,
1072
+ hasChanged ? "(with only structure changes)" : "(unchanged)",
1073
+ );
1074
+ endpointsToRemove.delete(endpointId);
1075
+ if (hasChanged) {
1076
+ eventsToEmit.set(endpointId, "nodeEndpointChanged");
1077
+ }
1078
+ } else {
1079
+ logger.debug(`Node ${this.nodeId}: Recreating endpoint`, endpointId);
1080
+ eventsToEmit.set(endpointId, "nodeEndpointChanged");
1081
+ }
998
1082
  }
999
1083
  }
1000
1084
  // And remove all endpoints no longer in the structure
1001
- for (const endpointId of endpointsToRemove.values()) {
1002
- logger.debug(`Node ${this.nodeId}: Removing device`, endpointId);
1003
- this.#endpoints.get(endpointId)?.removeFromStructure();
1004
- this.#endpoints.delete(endpointId);
1085
+ for (const endpoint of endpointsToRemove.values()) {
1086
+ const endpointId = EndpointNumber(endpoint);
1087
+ const device = this.#endpoints.get(endpointId);
1088
+ if (device !== undefined) {
1089
+ if (eventsToEmit.get(endpointId) !== "nodeEndpointChanged") {
1090
+ logger.debug(`Node ${this.nodeId}: Removing endpoint`, endpointId);
1091
+ eventsToEmit.set(endpointId, "nodeEndpointRemoved");
1092
+ }
1093
+ device.removeFromStructure();
1094
+ this.#endpoints.delete(endpointId);
1095
+ }
1005
1096
  }
1006
1097
  } else {
1007
1098
  this.#endpoints.clear();
1008
1099
  }
1009
1100
 
1010
- const partLists = new Map<EndpointNumber, EndpointNumber[]>();
1011
- for (const [endpointId, clusters] of Object.entries(allData)) {
1012
- const endpointIdNumber = EndpointNumber(parseInt(endpointId));
1013
- const descriptorData = clusters[DescriptorCluster.id] as AttributeClientValues<
1014
- typeof DescriptorCluster.attributes
1015
- >;
1101
+ for (const endpointId of descriptors.keys()) {
1102
+ const clusters = allData[endpointId];
1016
1103
 
1017
- partLists.set(endpointIdNumber, descriptorData.partsList);
1018
-
1019
- if (this.#endpoints.has(endpointIdNumber)) {
1104
+ if (this.#endpoints.has(endpointId)) {
1020
1105
  // Endpoint exists already, so mo need to create device instance again
1021
1106
  continue;
1022
1107
  }
1023
1108
 
1024
- logger.debug(`Node ${this.nodeId}: Creating device`, endpointId, Diagnostic.json(clusters));
1025
- this.#endpoints.set(
1026
- endpointIdNumber,
1027
- this.#createDevice(endpointIdNumber, clusters, this.#interactionClient),
1109
+ const isRecreation = eventsToEmit.get(endpointId) === "nodeEndpointChanged";
1110
+ logger.debug(
1111
+ `Node ${this.nodeId}: ${isRecreation ? "Recreating" : "Creating"} endpoint`,
1112
+ endpointId,
1113
+ Diagnostic.json(clusters),
1028
1114
  );
1115
+ this.#endpoints.set(endpointId, this.#createDevice(endpointId, clusters, this.#interactionClient));
1116
+ if (!isRecreation) {
1117
+ eventsToEmit.set(endpointId, "nodeEndpointAdded");
1118
+ }
1119
+ }
1120
+
1121
+ // Remove all children that are not in the partsList anymore
1122
+ for (const [endpointId, { partsList }] of descriptors.entries()) {
1123
+ const endpoint = this.#endpoints.get(endpointId);
1124
+ if (endpoint === undefined) {
1125
+ // Should not happen or endpoint was invalid and that's why not created, then we ignore it
1126
+ continue;
1127
+ }
1128
+ endpoint.getChildEndpoints().forEach(child => {
1129
+ if (child.number !== undefined && !partsList.includes(child.number)) {
1130
+ // Remove this child because it is no longer in the partsList
1131
+ endpoint.removeChildEndpoint(child);
1132
+ if (!eventsToEmit.has(endpointId)) {
1133
+ eventsToEmit.set(endpointId, "nodeEndpointChanged");
1134
+ }
1135
+ }
1136
+ });
1029
1137
  }
1030
1138
 
1031
- this.#structureEndpoints(partLists);
1139
+ this.#structureEndpoints(descriptors);
1140
+
1141
+ if (updateStructure && eventsToEmit.size) {
1142
+ for (const [endpointId, eventName] of eventsToEmit.entries()) {
1143
+ // Cleanup storage data for removed or updated endpoints
1144
+ if (eventName !== "nodeEndpointAdded") {
1145
+ // For removed or changed endpoints we need to cleanup the stored data
1146
+ const clusterServers = descriptors.get(endpointId)?.serverList;
1147
+ await this.#interactionClient.cleanupAttributeData(
1148
+ endpointId,
1149
+ eventName === "nodeEndpointRemoved" ? undefined : clusterServers,
1150
+ );
1151
+ }
1152
+ }
1153
+
1154
+ const emitChangeEvents = () => {
1155
+ for (const [endpointId, eventName] of eventsToEmit.entries()) {
1156
+ logger.debug(`Node ${this.nodeId}: Emitting event ${eventName} for endpoint ${endpointId}`);
1157
+ this.events[eventName].emit(endpointId);
1158
+ }
1159
+ this.#options.stateInformationCallback?.(this.nodeId, NodeStateInformation.StructureChanged);
1160
+ this.events.structureChanged.emit();
1161
+ };
1162
+
1163
+ if (this.#connectionState === NodeStates.Connected) {
1164
+ // If we are connected we can emit the events right away
1165
+ emitChangeEvents();
1166
+ } else {
1167
+ // If we are not connected we need to wait until we are connected again and emit these changes afterwards
1168
+ this.events.stateChanged.once(State => {
1169
+ if (State === NodeStates.Connected) {
1170
+ emitChangeEvents();
1171
+ }
1172
+ });
1173
+ }
1174
+ }
1032
1175
  }
1033
1176
 
1034
- /** Bring the endpoints in a structure based on their partsList attribute. */
1035
- #structureEndpoints(partLists: Map<EndpointNumber, EndpointNumber[]>) {
1036
- logger.debug(
1037
- `Node ${this.nodeId}: Endpoints from PartsLists`,
1038
- Diagnostic.json(Array.from(partLists.entries())),
1177
+ /**
1178
+ * Bring the endpoints in a structure based on their partsList attribute. This method only adds endpoints into the
1179
+ * right place as children, Cleanup is not happening here
1180
+ */
1181
+ #structureEndpoints(descriptors: Map<EndpointNumber, DescriptorData>) {
1182
+ const partLists = Array.from(descriptors.entries()).map(
1183
+ ([ep, { partsList }]) => [ep, partsList] as [EndpointNumber, EndpointNumber[]], // else Typescript gets confused
1039
1184
  );
1185
+ logger.debug(`Node ${this.nodeId}: Endpoints from PartsLists`, Diagnostic.json(partLists));
1040
1186
 
1041
1187
  const endpointUsages: { [key: EndpointNumber]: EndpointNumber[] } = {};
1042
- Array.from(partLists.entries()).forEach(([parent, partsList]) =>
1188
+ partLists.forEach(([parent, partsList]) =>
1043
1189
  partsList.forEach(endPoint => {
1044
1190
  if (endPoint === parent) {
1045
1191
  // There could be more cases of invalid and cycling structures that never should happen ... so lets not over optimize to try to find all of them right now
@@ -1105,7 +1251,7 @@ export class PairedNode {
1105
1251
  data: { [key: ClusterId]: { [key: string]: any } },
1106
1252
  interactionClient: InteractionClient,
1107
1253
  ) {
1108
- const descriptorData = data[DescriptorCluster.id] as AttributeClientValues<typeof DescriptorCluster.attributes>;
1254
+ const descriptorData = data[Descriptor.Complete.id] as DescriptorData;
1109
1255
 
1110
1256
  const deviceTypes = descriptorData.deviceTypeList.flatMap(({ deviceType, revision }) => {
1111
1257
  const deviceTypeDefinition = getDeviceTypeDefinitionFromModelByCode(deviceType);
@@ -1479,3 +1625,55 @@ export class PairedNode {
1479
1625
  return root.commandsOf(type);
1480
1626
  }
1481
1627
  }
1628
+
1629
+ export namespace PairedNode {
1630
+ export interface NodeStructureEvents {
1631
+ /** Emitted when endpoints are added. */
1632
+ nodeEndpointAdded: Observable<[EndpointNumber]>;
1633
+
1634
+ /** Emitted when endpoints are removed. */
1635
+ nodeEndpointRemoved: Observable<[EndpointNumber]>;
1636
+
1637
+ /** Emitted when endpoints are updated (e.g. device type changed, structure changed). */
1638
+ nodeEndpointChanged: Observable<[EndpointNumber]>;
1639
+ }
1640
+
1641
+ export interface Events extends NodeStructureEvents {
1642
+ /**
1643
+ * Emitted when the node is initialized from local data. These data usually are stale, but you can still already
1644
+ * use the node to interact with the device. If no local data are available this event will be emitted together
1645
+ * with the initializedFromRemote event.
1646
+ */
1647
+ initialized: AsyncObservable<[details: DeviceInformationData]>;
1648
+
1649
+ /**
1650
+ * Emitted when the node is fully initialized from remote and all attributes and events are subscribed.
1651
+ * This event can also be awaited if code needs to be blocked until the node is fully initialized.
1652
+ */
1653
+ initializedFromRemote: AsyncObservable<[details: DeviceInformationData]>;
1654
+
1655
+ /** Emitted when the state of the node changes. */
1656
+ stateChanged: Observable<[nodeState: NodeStates]>;
1657
+
1658
+ /**
1659
+ * Emitted when an attribute value changes. If the oldValue is undefined then no former value was known.
1660
+ */
1661
+ attributeChanged: Observable<[data: DecodedAttributeReportValue<any>, oldValue: any]>;
1662
+
1663
+ /** Emitted when an event is triggered. */
1664
+ eventTriggered: Observable<[DecodedEventReportValue<any>]>;
1665
+
1666
+ /**
1667
+ * Emitted when all node structure changes were applied (Endpoints got added or also removed).
1668
+ * You can alternatively use the nodeEndpointAdded, nodeEndpointRemoved and nodeEndpointChanged events to react on specific changes.
1669
+ * This event is emitted after all nodeEndpointAdded, nodeEndpointRemoved and nodeEndpointChanged events are emitted.
1670
+ */
1671
+ structureChanged: Observable<[void]>;
1672
+
1673
+ /** Emitted when the node is decommissioned. */
1674
+ decommissioned: Observable<[void]>;
1675
+
1676
+ /** Emitted when a subscription alive trigger is received (max interval trigger or any data update) */
1677
+ connectionAlive: Observable<[void]>;
1678
+ }
1679
+ }