@project-chip/matter.js 0.15.4 → 0.15.5

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 (42) hide show
  1. package/dist/cjs/CommissioningController.d.ts +1 -1
  2. package/dist/cjs/CommissioningController.d.ts.map +1 -1
  3. package/dist/cjs/CommissioningController.js +9 -18
  4. package/dist/cjs/CommissioningController.js.map +1 -1
  5. package/dist/cjs/MatterController.d.ts.map +1 -1
  6. package/dist/cjs/MatterController.js +2 -2
  7. package/dist/cjs/MatterController.js.map +1 -1
  8. package/dist/cjs/device/CachedClientNodeStore.d.ts +4 -3
  9. package/dist/cjs/device/CachedClientNodeStore.d.ts.map +1 -1
  10. package/dist/cjs/device/CachedClientNodeStore.js +19 -2
  11. package/dist/cjs/device/CachedClientNodeStore.js.map +1 -1
  12. package/dist/cjs/device/Endpoint.d.ts +1 -1
  13. package/dist/cjs/device/Endpoint.d.ts.map +1 -1
  14. package/dist/cjs/device/Endpoint.js.map +1 -1
  15. package/dist/cjs/device/PairedNode.d.ts +52 -28
  16. package/dist/cjs/device/PairedNode.d.ts.map +1 -1
  17. package/dist/cjs/device/PairedNode.js +196 -77
  18. package/dist/cjs/device/PairedNode.js.map +1 -1
  19. package/dist/esm/CommissioningController.d.ts +1 -1
  20. package/dist/esm/CommissioningController.d.ts.map +1 -1
  21. package/dist/esm/CommissioningController.js +9 -18
  22. package/dist/esm/CommissioningController.js.map +1 -1
  23. package/dist/esm/MatterController.d.ts.map +1 -1
  24. package/dist/esm/MatterController.js +2 -3
  25. package/dist/esm/MatterController.js.map +1 -1
  26. package/dist/esm/device/CachedClientNodeStore.d.ts +4 -3
  27. package/dist/esm/device/CachedClientNodeStore.d.ts.map +1 -1
  28. package/dist/esm/device/CachedClientNodeStore.js +19 -2
  29. package/dist/esm/device/CachedClientNodeStore.js.map +1 -1
  30. package/dist/esm/device/Endpoint.d.ts +1 -1
  31. package/dist/esm/device/Endpoint.d.ts.map +1 -1
  32. package/dist/esm/device/Endpoint.js.map +1 -1
  33. package/dist/esm/device/PairedNode.d.ts +52 -28
  34. package/dist/esm/device/PairedNode.d.ts.map +1 -1
  35. package/dist/esm/device/PairedNode.js +197 -78
  36. package/dist/esm/device/PairedNode.js.map +1 -1
  37. package/package.json +8 -8
  38. package/src/CommissioningController.ts +10 -18
  39. package/src/MatterController.ts +3 -4
  40. package/src/device/CachedClientNodeStore.ts +24 -4
  41. package/src/device/Endpoint.ts +1 -1
  42. package/src/device/PairedNode.ts +292 -89
@@ -40,7 +40,6 @@ import {
40
40
  ControllerCommissioner,
41
41
  DecodedAttributeReportValue,
42
42
  DEFAULT_ADMIN_VENDOR_ID,
43
- DEFAULT_FABRIC_ID,
44
43
  DeviceAdvertiser,
45
44
  DiscoveryAndCommissioningOptions,
46
45
  DiscoveryData,
@@ -112,13 +111,15 @@ export class MatterController {
112
111
  rootFabric?: Fabric;
113
112
  crypto?: Crypto;
114
113
  }): Promise<MatterController> {
114
+ const crypto = options.crypto ?? Environment.default.get(Crypto);
115
+
115
116
  const {
116
117
  controllerStore,
117
118
  scanners,
118
119
  netInterfaces,
119
120
  sessionClosedCallback,
120
121
  adminVendorId,
121
- adminFabricId = FabricId(DEFAULT_FABRIC_ID),
122
+ adminFabricId = FabricId(crypto.randomBigInt(8)),
122
123
  adminFabricIndex = FabricIndex(DEFAULT_FABRIC_INDEX),
123
124
  caseAuthenticatedTags,
124
125
  adminFabricLabel,
@@ -127,8 +128,6 @@ export class MatterController {
127
128
  rootFabric,
128
129
  } = options;
129
130
 
130
- const crypto = options.crypto ?? Environment.default.get(Crypto);
131
-
132
131
  const ca = rootCertificateAuthority ?? (await CertificateAuthority.create(crypto, controllerStore.caStorage));
133
132
  const fabricStorage = controllerStore.fabricStorage;
134
133
 
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  import type { StorageContext } from "#general";
8
- import { Construction, Logger } from "#general";
9
- import { DecodedAttributeReportValue, PeerDataStore, Val } from "#protocol";
8
+ import { Construction, Logger, MaybePromise } from "#general";
9
+ import { DecodedAttributeReportValue, PeerDataStore, ReadScope, Val } from "#protocol";
10
10
  import { AttributeId, ClusterId, EndpointNumber, EventNumber } from "#types";
11
11
  import { ClientEndpointStore } from "./ClientEndpointStore.js";
12
12
 
@@ -132,7 +132,12 @@ export class CachedClientNodeStore extends PeerDataStore {
132
132
  });
133
133
  }
134
134
 
135
- async persistAttributes(attributes: DecodedAttributeReportValue<any>[]) {
135
+ async persistAttributes(attributes: DecodedAttributeReportValue<any>[], scope: ReadScope) {
136
+ // We only store values that are filtered by out fabric, else we create a mixture of data
137
+ if (!scope.isFabricFiltered) {
138
+ return;
139
+ }
140
+
136
141
  const endpointDataMap = new Map<EndpointNumber, Record<ClusterId, Val.Struct>>();
137
142
  for (const {
138
143
  path: { endpointId, clusterId, attributeId, attributeName },
@@ -156,7 +161,9 @@ export class CachedClientNodeStore extends PeerDataStore {
156
161
  }
157
162
 
158
163
  clusterData[attributeId.toString()] = { value, attributeName };
159
- clusterData[VERSION_KEY] = version;
164
+ if (scope.isWildcard(endpointId, clusterId)) {
165
+ clusterData[VERSION_KEY] = version;
166
+ }
160
167
  }
161
168
  for (const [endpointId, endpointData] of endpointDataMap) {
162
169
  const store = this.#storeForEndpoint(endpointId);
@@ -193,4 +200,17 @@ export class CachedClientNodeStore extends PeerDataStore {
193
200
  }
194
201
  return versions;
195
202
  }
203
+
204
+ cleanupAttributeData(endpointId: EndpointNumber, clusterIds?: ClusterId[]): MaybePromise<void> {
205
+ const store = this.#storeForEndpoint(endpointId);
206
+ if (clusterIds === undefined) {
207
+ this.#endpointStores.delete(endpointId);
208
+ return store.erase();
209
+ }
210
+ for (const clusterId of store.get.keys()) {
211
+ if (!clusterIds.includes(ClusterId(parseInt(clusterId)))) {
212
+ store.get.delete(clusterId);
213
+ }
214
+ }
215
+ }
196
216
  }
@@ -239,7 +239,7 @@ export class Endpoint {
239
239
  return this.childEndpoints;
240
240
  }
241
241
 
242
- protected removeChildEndpoint(endpoint: Endpoint): void {
242
+ removeChildEndpoint(endpoint: Endpoint): void {
243
243
  const index = this.childEndpoints.indexOf(endpoint);
244
244
  if (index === -1) {
245
245
  throw new ImplementationError(`Provided endpoint for deletion does not exist as child endpoint.`);
@@ -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,
@@ -31,6 +31,7 @@ import {
31
31
  NodeDiscoveryType,
32
32
  NodeSession,
33
33
  PaseClient,
34
+ StructuredReadAttributeData,
34
35
  UnknownNodeError,
35
36
  structureReadAttributeDataToClusterObject,
36
37
  } from "#protocol";
@@ -211,6 +212,22 @@ interface SubscriptionHandlerCallbacks {
211
212
  subscriptionAlive: () => void;
212
213
  }
213
214
 
215
+ type DescriptorData = AttributeClientValues<typeof Descriptor.Complete.attributes>;
216
+
217
+ /**
218
+ * Tooling function to check if a list of numbers is the same as another list of numbers.
219
+ * it uses Sets to prevent duplicate entries and ordering to cause issues if they ever happen.
220
+ */
221
+ function areNumberListsSame(list1: number[], list2: number[]) {
222
+ const set1 = new Set(list1);
223
+ const set2 = new Set(list2);
224
+ if (set1.size !== set2.size) return false;
225
+ for (const entry of set1.values()) {
226
+ if (!set2.has(entry)) return false;
227
+ }
228
+ return true;
229
+ }
230
+
214
231
  /**
215
232
  * Class to represents one node that is paired/commissioned with the matter.js Controller. Instances are returned by
216
233
  * the CommissioningController on commissioning or when connecting.
@@ -260,38 +277,24 @@ export class PairedNode {
260
277
  #currentSubscriptionIntervalS?: number;
261
278
  #crypto: Crypto;
262
279
 
263
- readonly events = {
264
- /**
265
- * Emitted when the node is initialized from local data. These data usually are stale, but you can still already
266
- * use the node to interact with the device. If no local data are available this event will be emitted together
267
- * with the initializedFromRemote event.
268
- */
269
- initialized: AsyncObservable<[details: DeviceInformationData]>(),
280
+ /**
281
+ * Endpoint structure change information that are checked when updating structure
282
+ * - null means that the endpoint itself changed, so will be regenerated completely any case
283
+ * - array of ClusterIds means that only these clusters changed and will be updated
284
+ */
285
+ #registeredEndpointStructureChanges = new Map<EndpointNumber, ClusterId[] | null>();
270
286
 
271
- /**
272
- * Emitted when the node is fully initialized from remote and all attributes and events are subscribed.
273
- * This event can also be awaited if code needs to be blocked until the node is fully initialized.
274
- */
287
+ readonly events: PairedNode.Events = {
288
+ initialized: AsyncObservable<[details: DeviceInformationData]>(),
275
289
  initializedFromRemote: AsyncObservable<[details: DeviceInformationData]>(),
276
-
277
- /** Emitted when the state of the node changes. */
278
290
  stateChanged: Observable<[nodeState: NodeStates]>(),
279
-
280
- /**
281
- * Emitted when an attribute value changes. If the oldValue is undefined then no former value was known.
282
- */
283
291
  attributeChanged: Observable<[data: DecodedAttributeReportValue<any>, oldValue: any]>(),
284
-
285
- /** Emitted when an event is triggered. */
286
292
  eventTriggered: Observable<[DecodedEventReportValue<any>]>(),
287
-
288
- /** Emitted when the structure of the node changes (Endpoints got added or also removed). */
289
293
  structureChanged: Observable<[void]>(),
290
-
291
- /** Emitted when the node is decommissioned. */
294
+ nodeEndpointAdded: Observable<[EndpointNumber]>(),
295
+ nodeEndpointRemoved: Observable<[EndpointNumber]>(),
296
+ nodeEndpointChanged: Observable<[EndpointNumber]>(),
292
297
  decommissioned: Observable<[void]>(),
293
-
294
- /** Emitted when a subscription alive trigger is received (max interval trigger or any data update) */
295
298
  connectionAlive: Observable<[void]>(),
296
299
  };
297
300
 
@@ -633,7 +636,7 @@ export class PairedNode {
633
636
  return;
634
637
  }
635
638
 
636
- await this.#initializeEndpointStructure(storedAttributeData);
639
+ await this.#initializeEndpointStructure(storedAttributeData, false);
637
640
 
638
641
  // Inform interested parties that the node is initialized
639
642
  await this.events.initialized.emit(this.#nodeDetails.toStorageData());
@@ -678,7 +681,7 @@ export class PairedNode {
678
681
  if (attributeReports === undefined) {
679
682
  throw new InternalError("No attribute reports received when subscribing to all values!");
680
683
  }
681
- await this.#initializeEndpointStructure(attributeReports, anyInitializationDone);
684
+ await this.#initializeEndpointStructure(attributeReports);
682
685
 
683
686
  this.#remoteInitializationInProgress = false; // We are done, rest is bonus and should not block reconnections
684
687
 
@@ -691,7 +694,7 @@ export class PairedNode {
691
694
  this.#currentSubscriptionIntervalS = maxInterval;
692
695
  } else {
693
696
  const allClusterAttributes = await this.readAllAttributes();
694
- await this.#initializeEndpointStructure(allClusterAttributes, anyInitializationDone);
697
+ await this.#initializeEndpointStructure(allClusterAttributes);
695
698
  this.#remoteInitializationInProgress = false; // We are done, rest is bonus and should not block reconnections
696
699
  }
697
700
  if (!this.#remoteInitializationDone) {
@@ -845,6 +848,16 @@ export class PairedNode {
845
848
  this.#reconnectDelayTimer = undefined;
846
849
  this.#setConnectionState(NodeStates.Connected);
847
850
  }
851
+
852
+ if (
853
+ this.#remoteInitializationDone &&
854
+ this.#registeredEndpointStructureChanges.size > 0 &&
855
+ !this.#updateEndpointStructureTimer.isRunning
856
+ ) {
857
+ logger.info(`Node ${this.nodeId}: Endpoint structure needs to be updated ...`);
858
+ this.#updateEndpointStructureTimer.stop().start();
859
+ }
860
+
848
861
  this.events.connectionAlive.emit();
849
862
  },
850
863
  };
@@ -854,12 +867,11 @@ export class PairedNode {
854
867
 
855
868
  // We first update all values by doing a read all on the device
856
869
  // We do not enrich existing data because we just want to store updated data
857
- const attributeData = await this.#interactionClient.getAllAttributes({
870
+ await this.#interactionClient.getAllAttributes({
858
871
  dataVersionFilters: this.#interactionClient.getCachedClusterDataVersions(),
859
872
  executeQueued: !!threadConnected, // We queue subscriptions for thread devices
873
+ attributeChangeListener: subscriptionHandler.attributeListener,
860
874
  });
861
- await this.#interactionClient.processAttributeUpdates(attributeData, subscriptionHandler.attributeListener);
862
- attributeData.length = 0; // Clear the array to save memory
863
875
 
864
876
  // If we subscribe anything we use these data to create the endpoint structure, so we do not need to fetch again
865
877
  const initialSubscriptionData = await this.#interactionClient.subscribeAllAttributesAndEvents({
@@ -892,35 +904,35 @@ export class PairedNode {
892
904
  }
893
905
 
894
906
  #checkAttributesForNeededStructureUpdate(
895
- _endpointId: EndpointNumber,
907
+ endpointId: EndpointNumber,
896
908
  clusterId: ClusterId,
897
909
  attributeId: AttributeId,
898
910
  ) {
899
911
  // Any change in the Descriptor Cluster partsList attribute requires a reinitialization of the endpoint structure
900
- let structureUpdateNeeded = false;
901
- if (clusterId === DescriptorCluster.id) {
902
- switch (attributeId) {
903
- case DescriptorCluster.attributes.partsList.id:
904
- case DescriptorCluster.attributes.serverList.id:
905
- case DescriptorCluster.attributes.deviceTypeList.id:
906
- structureUpdateNeeded = true;
907
- break;
908
- }
909
- }
910
- if (!structureUpdateNeeded) {
912
+ if (clusterId === Descriptor.Complete.id) {
911
913
  switch (attributeId) {
912
- case FeatureMap.id:
913
- case AttributeList.id:
914
- case AcceptedCommandList.id:
915
- case ClusterRevision.id:
916
- structureUpdateNeeded = true;
917
- break;
914
+ case Descriptor.Complete.attributes.partsList.id:
915
+ case Descriptor.Complete.attributes.serverList.id:
916
+ case Descriptor.Complete.attributes.clientList.id:
917
+ case Descriptor.Complete.attributes.deviceTypeList.id:
918
+ this.#registeredEndpointStructureChanges.set(endpointId, null); // full endpoint update needed
919
+ return;
918
920
  }
919
921
  }
920
-
921
- if (structureUpdateNeeded) {
922
- logger.info(`Node ${this.nodeId}: Endpoint structure needs to be updated ...`);
923
- this.#updateEndpointStructureTimer.stop().start();
922
+ switch (attributeId) {
923
+ case FeatureMap.id:
924
+ case AttributeList.id:
925
+ case AcceptedCommandList.id:
926
+ case ClusterRevision.id:
927
+ let knownForUpdate = this.#registeredEndpointStructureChanges.get(endpointId);
928
+ if (knownForUpdate !== null) {
929
+ knownForUpdate = knownForUpdate ?? [];
930
+ if (!knownForUpdate.includes(clusterId)) {
931
+ knownForUpdate.push(clusterId);
932
+ this.#registeredEndpointStructureChanges.set(endpointId, knownForUpdate);
933
+ }
934
+ }
935
+ break;
924
936
  }
925
937
  }
926
938
 
@@ -956,72 +968,206 @@ export class PairedNode {
956
968
  }
957
969
 
958
970
  async #updateEndpointStructure() {
959
- const allClusterAttributes = await this.readAllAttributes();
971
+ const allClusterAttributes = this.#interactionClient.getAllCachedClusterData();
960
972
  await this.#initializeEndpointStructure(allClusterAttributes, true);
961
- this.#options.stateInformationCallback?.(this.nodeId, NodeStateInformation.StructureChanged);
962
- this.events.structureChanged.emit();
973
+ }
974
+
975
+ /**
976
+ * Traverse the structure data and collect all data for the given endpointId.
977
+ * Return true if data for the endpoint was found, otherwise false.
978
+ * If data was found it is added to the collectedData map.
979
+ */
980
+ collectDescriptorData(
981
+ structure: StructuredReadAttributeData,
982
+ endpointId: EndpointNumber,
983
+ collectedData: Map<EndpointNumber, DescriptorData>,
984
+ ) {
985
+ if (collectedData.has(endpointId)) {
986
+ return;
987
+ }
988
+ const endpointData = structure[endpointId];
989
+ const descriptorData = endpointData?.[Descriptor.Complete.id] as DescriptorData | undefined;
990
+ if (endpointData === undefined || descriptorData === undefined) {
991
+ logger.info(`Descriptor data for endpoint ${endpointId} not found in structure! Ignoring endpoint ...`);
992
+ return;
993
+ }
994
+ collectedData.set(endpointId, descriptorData);
995
+ if (descriptorData.partsList.length) {
996
+ for (const partEndpointId of descriptorData.partsList) {
997
+ this.collectDescriptorData(structure, partEndpointId, collectedData);
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ #hasEndpointChanged(device: Endpoint, descriptorData: DescriptorData) {
1003
+ // Check if the device types (ignoring revision for now), or cluster server or cluster clients differ
1004
+ return !(
1005
+ areNumberListsSame(
1006
+ device.getDeviceTypes().map(({ code }) => code),
1007
+ descriptorData.deviceTypeList.map(({ deviceType }) => deviceType),
1008
+ ) &&
1009
+ // Check if the cluster clients are the same - they map to the serverList attribute
1010
+ areNumberListsSame(
1011
+ device.getAllClusterClients().map(({ id }) => id),
1012
+ descriptorData.serverList,
1013
+ ) &&
1014
+ // Check if the cluster servers are the same - they map to the clientList attribute
1015
+ areNumberListsSame(
1016
+ device.getAllClusterServers().map(({ id }) => id),
1017
+ descriptorData.clientList,
1018
+ )
1019
+ );
963
1020
  }
964
1021
 
965
1022
  /** Reads all data from the device and create a device object structure out of it. */
966
1023
  async #initializeEndpointStructure(
967
1024
  allClusterAttributes: DecodedAttributeReportValue<any>[],
968
- updateStructure = false,
1025
+ updateStructure = this.#localInitializationDone || this.#remoteInitializationDone,
969
1026
  ) {
1027
+ if (this.#updateEndpointStructureTimer.isRunning) {
1028
+ this.#updateEndpointStructureTimer.stop();
1029
+ }
1030
+ const eventsToEmit = new Map<EndpointNumber, keyof PairedNode.NodeStructureEvents>();
1031
+ const structureUpdateDetails = this.#registeredEndpointStructureChanges;
1032
+ this.#registeredEndpointStructureChanges = new Map();
1033
+
970
1034
  const allData = structureReadAttributeDataToClusterObject(allClusterAttributes);
971
1035
 
1036
+ // Collect the descriptor data for all endpoints referenced in the structure
1037
+ const descriptors = new Map<EndpointNumber, DescriptorData>();
1038
+ this.collectDescriptorData(allData, EndpointNumber(0), descriptors);
1039
+
972
1040
  if (updateStructure) {
973
1041
  // Find out what we need to remove or retain
974
1042
  const endpointsToRemove = new Set<EndpointNumber>(this.#endpoints.keys());
975
- for (const [endpointId] of Object.entries(allData)) {
976
- const endpointIdNumber = EndpointNumber(parseInt(endpointId));
977
- if (this.#endpoints.has(endpointIdNumber)) {
978
- logger.debug(`Node ${this.nodeId}: Retaining device`, endpointId);
979
- endpointsToRemove.delete(endpointIdNumber);
1043
+ for (const endpointId of descriptors.keys()) {
1044
+ const device = this.#endpoints.get(endpointId);
1045
+ if (device !== undefined) {
1046
+ // Check if there are any changes to the device that require a re-creation
1047
+ // When structureUpdateDetails from subscription updates state changes we do a deep validation
1048
+ // to prevent ordering changes to cause unnecessary device re-creations
1049
+ const hasChanged = structureUpdateDetails.has(endpointId);
1050
+ if (!hasChanged || !this.#hasEndpointChanged(device, descriptors.get(endpointId)!)) {
1051
+ logger.debug(
1052
+ `Node ${this.nodeId}: Retaining endpoint`,
1053
+ endpointId,
1054
+ hasChanged ? "(with only structure changes)" : "(unchanged)",
1055
+ );
1056
+ endpointsToRemove.delete(endpointId);
1057
+ if (hasChanged) {
1058
+ eventsToEmit.set(endpointId, "nodeEndpointChanged");
1059
+ }
1060
+ } else {
1061
+ logger.debug(`Node ${this.nodeId}: Recreating endpoint`, endpointId);
1062
+ eventsToEmit.set(endpointId, "nodeEndpointChanged");
1063
+ }
980
1064
  }
981
1065
  }
982
1066
  // And remove all endpoints no longer in the structure
983
- for (const endpointId of endpointsToRemove.values()) {
984
- logger.debug(`Node ${this.nodeId}: Removing device`, endpointId);
985
- this.#endpoints.get(endpointId)?.removeFromStructure();
986
- this.#endpoints.delete(endpointId);
1067
+ for (const endpoint of endpointsToRemove.values()) {
1068
+ const endpointId = EndpointNumber(endpoint);
1069
+ const device = this.#endpoints.get(endpointId);
1070
+ if (device !== undefined) {
1071
+ if (eventsToEmit.get(endpointId) !== "nodeEndpointChanged") {
1072
+ logger.debug(`Node ${this.nodeId}: Removing endpoint`, endpointId);
1073
+ eventsToEmit.set(endpointId, "nodeEndpointRemoved");
1074
+ }
1075
+ device.removeFromStructure();
1076
+ this.#endpoints.delete(endpointId);
1077
+ }
987
1078
  }
988
1079
  } else {
989
1080
  this.#endpoints.clear();
990
1081
  }
991
1082
 
992
- const partLists = new Map<EndpointNumber, EndpointNumber[]>();
993
- for (const [endpointId, clusters] of Object.entries(allData)) {
994
- const endpointIdNumber = EndpointNumber(parseInt(endpointId));
995
- const descriptorData = clusters[DescriptorCluster.id] as AttributeClientValues<
996
- typeof DescriptorCluster.attributes
997
- >;
1083
+ for (const endpointId of descriptors.keys()) {
1084
+ const clusters = allData[endpointId];
998
1085
 
999
- partLists.set(endpointIdNumber, descriptorData.partsList);
1000
-
1001
- if (this.#endpoints.has(endpointIdNumber)) {
1086
+ if (this.#endpoints.has(endpointId)) {
1002
1087
  // Endpoint exists already, so mo need to create device instance again
1003
1088
  continue;
1004
1089
  }
1005
1090
 
1006
- logger.debug(`Node ${this.nodeId}: Creating device`, endpointId, Diagnostic.json(clusters));
1007
- this.#endpoints.set(
1008
- endpointIdNumber,
1009
- this.#createDevice(endpointIdNumber, clusters, this.#interactionClient),
1091
+ const isRecreation = eventsToEmit.get(endpointId) === "nodeEndpointChanged";
1092
+ logger.debug(
1093
+ `Node ${this.nodeId}: ${isRecreation ? "Recreating" : "Creating"} endpoint`,
1094
+ endpointId,
1095
+ Diagnostic.json(clusters),
1010
1096
  );
1097
+ this.#endpoints.set(endpointId, this.#createDevice(endpointId, clusters, this.#interactionClient));
1098
+ if (!isRecreation) {
1099
+ eventsToEmit.set(endpointId, "nodeEndpointAdded");
1100
+ }
1101
+ }
1102
+
1103
+ // Remove all children that are not in the partsList anymore
1104
+ for (const [endpointId, { partsList }] of descriptors.entries()) {
1105
+ const endpoint = this.#endpoints.get(endpointId);
1106
+ if (endpoint === undefined) {
1107
+ // Should not happen or endpoint was invalid and that's why not created, then we ignore it
1108
+ continue;
1109
+ }
1110
+ endpoint.getChildEndpoints().forEach(child => {
1111
+ if (child.number !== undefined && !partsList.includes(child.number)) {
1112
+ // Remove this child because it is no longer in the partsList
1113
+ endpoint.removeChildEndpoint(child);
1114
+ if (!eventsToEmit.has(endpointId)) {
1115
+ eventsToEmit.set(endpointId, "nodeEndpointChanged");
1116
+ }
1117
+ }
1118
+ });
1011
1119
  }
1012
1120
 
1013
- this.#structureEndpoints(partLists);
1121
+ this.#structureEndpoints(descriptors);
1122
+
1123
+ if (updateStructure && eventsToEmit.size) {
1124
+ for (const [endpointId, eventName] of eventsToEmit.entries()) {
1125
+ // Cleanup storage data for removed or updated endpoints
1126
+ if (eventName !== "nodeEndpointAdded") {
1127
+ // For removed or changed endpoints we need to cleanup the stored data
1128
+ const clusterServers = descriptors.get(endpointId)?.serverList;
1129
+ await this.#interactionClient.cleanupAttributeData(
1130
+ endpointId,
1131
+ eventName === "nodeEndpointRemoved" ? undefined : clusterServers,
1132
+ );
1133
+ }
1134
+ }
1135
+
1136
+ const emitChangeEvents = () => {
1137
+ for (const [endpointId, eventName] of eventsToEmit.entries()) {
1138
+ logger.debug(`Node ${this.nodeId}: Emitting event ${eventName} for endpoint ${endpointId}`);
1139
+ this.events[eventName].emit(endpointId);
1140
+ }
1141
+ this.#options.stateInformationCallback?.(this.nodeId, NodeStateInformation.StructureChanged);
1142
+ this.events.structureChanged.emit();
1143
+ };
1144
+
1145
+ if (this.#connectionState === NodeStates.Connected) {
1146
+ // If we are connected we can emit the events right away
1147
+ emitChangeEvents();
1148
+ } else {
1149
+ // If we are not connected we need to wait until we are connected again and emit these changes afterwards
1150
+ this.events.stateChanged.once(State => {
1151
+ if (State === NodeStates.Connected) {
1152
+ emitChangeEvents();
1153
+ }
1154
+ });
1155
+ }
1156
+ }
1014
1157
  }
1015
1158
 
1016
- /** Bring the endpoints in a structure based on their partsList attribute. */
1017
- #structureEndpoints(partLists: Map<EndpointNumber, EndpointNumber[]>) {
1018
- logger.debug(
1019
- `Node ${this.nodeId}: Endpoints from PartsLists`,
1020
- Diagnostic.json(Array.from(partLists.entries())),
1159
+ /**
1160
+ * Bring the endpoints in a structure based on their partsList attribute. This method only adds endpoints into the
1161
+ * right place as children, Cleanup is not happening here
1162
+ */
1163
+ #structureEndpoints(descriptors: Map<EndpointNumber, DescriptorData>) {
1164
+ const partLists = Array.from(descriptors.entries()).map(
1165
+ ([ep, { partsList }]) => [ep, partsList] as [EndpointNumber, EndpointNumber[]], // else Typescript gets confused
1021
1166
  );
1167
+ logger.debug(`Node ${this.nodeId}: Endpoints from PartsLists`, Diagnostic.json(partLists));
1022
1168
 
1023
1169
  const endpointUsages: { [key: EndpointNumber]: EndpointNumber[] } = {};
1024
- Array.from(partLists.entries()).forEach(([parent, partsList]) =>
1170
+ partLists.forEach(([parent, partsList]) =>
1025
1171
  partsList.forEach(endPoint => {
1026
1172
  if (endPoint === parent) {
1027
1173
  // 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
@@ -1051,14 +1197,19 @@ export class PairedNode {
1051
1197
  const childEndpointId = EndpointNumber(parseInt(childId));
1052
1198
  const childEndpoint = this.#endpoints.get(childEndpointId);
1053
1199
  const parentEndpoint = this.#endpoints.get(usages[0]);
1200
+ const existingChildEndpoint = parentEndpoint?.getChildEndpoint(childEndpointId);
1054
1201
  if (childEndpoint === undefined || parentEndpoint === undefined) {
1055
1202
  logger.warn(
1056
1203
  `Node ${this.nodeId}: Endpoint ${usages[0]} not found in the data received from the device!`,
1057
1204
  );
1058
- } else if (parentEndpoint.getChildEndpoint(childEndpointId) === undefined) {
1205
+ } else if (existingChildEndpoint !== childEndpoint) {
1059
1206
  logger.debug(
1060
1207
  `Node ${this.nodeId}: Endpoint structure: Child: ${childEndpointId} -> Parent: ${parentEndpoint.number}`,
1061
1208
  );
1209
+ if (existingChildEndpoint !== undefined) {
1210
+ // Child endpoint changed, so we need to remove the old one first
1211
+ parentEndpoint.removeChildEndpoint(existingChildEndpoint);
1212
+ }
1062
1213
 
1063
1214
  parentEndpoint.addChildEndpoint(childEndpoint);
1064
1215
  }
@@ -1087,7 +1238,7 @@ export class PairedNode {
1087
1238
  data: { [key: ClusterId]: { [key: string]: any } },
1088
1239
  interactionClient: InteractionClient,
1089
1240
  ) {
1090
- const descriptorData = data[DescriptorCluster.id] as AttributeClientValues<typeof DescriptorCluster.attributes>;
1241
+ const descriptorData = data[Descriptor.Complete.id] as DescriptorData;
1091
1242
 
1092
1243
  const deviceTypes = descriptorData.deviceTypeList.flatMap(({ deviceType, revision }) => {
1093
1244
  const deviceTypeDefinition = getDeviceTypeDefinitionFromModelByCode(deviceType);
@@ -1416,3 +1567,55 @@ export class PairedNode {
1416
1567
  });
1417
1568
  }
1418
1569
  }
1570
+
1571
+ export namespace PairedNode {
1572
+ export interface NodeStructureEvents {
1573
+ /** Emitted when endpoints are added. */
1574
+ nodeEndpointAdded: Observable<[EndpointNumber]>;
1575
+
1576
+ /** Emitted when endpoints are removed. */
1577
+ nodeEndpointRemoved: Observable<[EndpointNumber]>;
1578
+
1579
+ /** Emitted when endpoints are updated (e.g. device type changed, structure changed). */
1580
+ nodeEndpointChanged: Observable<[EndpointNumber]>;
1581
+ }
1582
+
1583
+ export interface Events extends NodeStructureEvents {
1584
+ /**
1585
+ * Emitted when the node is initialized from local data. These data usually are stale, but you can still already
1586
+ * use the node to interact with the device. If no local data are available this event will be emitted together
1587
+ * with the initializedFromRemote event.
1588
+ */
1589
+ initialized: AsyncObservable<[details: DeviceInformationData]>;
1590
+
1591
+ /**
1592
+ * Emitted when the node is fully initialized from remote and all attributes and events are subscribed.
1593
+ * This event can also be awaited if code needs to be blocked until the node is fully initialized.
1594
+ */
1595
+ initializedFromRemote: AsyncObservable<[details: DeviceInformationData]>;
1596
+
1597
+ /** Emitted when the state of the node changes. */
1598
+ stateChanged: Observable<[nodeState: NodeStates]>;
1599
+
1600
+ /**
1601
+ * Emitted when an attribute value changes. If the oldValue is undefined then no former value was known.
1602
+ */
1603
+ attributeChanged: Observable<[data: DecodedAttributeReportValue<any>, oldValue: any]>;
1604
+
1605
+ /** Emitted when an event is triggered. */
1606
+ eventTriggered: Observable<[DecodedEventReportValue<any>]>;
1607
+
1608
+ /**
1609
+ * Emitted when all node structure changes were applied (Endpoints got added or also removed).
1610
+ * You can alternatively use the nodeEndpointAdded, nodeEndpointRemoved and nodeEndpointChanged events to react on specific changes.
1611
+ * This event is emitted after all nodeEndpointAdded, nodeEndpointRemoved and nodeEndpointChanged events are emitted.
1612
+ */
1613
+ structureChanged: Observable<[void]>;
1614
+
1615
+ /** Emitted when the node is decommissioned. */
1616
+ decommissioned: Observable<[void]>;
1617
+
1618
+ /** Emitted when a subscription alive trigger is received (max interval trigger or any data update) */
1619
+ connectionAlive: Observable<[void]>;
1620
+ }
1621
+ }