@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.
- package/dist/cjs/CommissioningController.d.ts +2 -2
- package/dist/cjs/CommissioningController.d.ts.map +1 -1
- package/dist/cjs/CommissioningController.js +2 -2
- package/dist/cjs/CommissioningController.js.map +1 -1
- package/dist/cjs/MatterController.d.ts +6 -6
- package/dist/cjs/MatterController.d.ts.map +1 -1
- package/dist/cjs/MatterController.js +12 -12
- package/dist/cjs/MatterController.js.map +1 -1
- package/dist/cjs/PaseCommissioner.js +1 -1
- package/dist/cjs/PaseCommissioner.js.map +1 -1
- package/dist/cjs/device/CachedClientNodeStore.d.ts +5 -4
- package/dist/cjs/device/CachedClientNodeStore.d.ts.map +1 -1
- package/dist/cjs/device/CachedClientNodeStore.js +19 -2
- package/dist/cjs/device/CachedClientNodeStore.js.map +1 -1
- package/dist/cjs/device/Endpoint.d.ts +1 -1
- package/dist/cjs/device/Endpoint.d.ts.map +1 -1
- package/dist/cjs/device/Endpoint.js.map +1 -1
- package/dist/cjs/device/PairedNode.d.ts +52 -28
- package/dist/cjs/device/PairedNode.d.ts.map +1 -1
- package/dist/cjs/device/PairedNode.js +191 -76
- package/dist/cjs/device/PairedNode.js.map +1 -1
- package/dist/esm/CommissioningController.d.ts +2 -2
- package/dist/esm/CommissioningController.d.ts.map +1 -1
- package/dist/esm/CommissioningController.js +3 -3
- package/dist/esm/CommissioningController.js.map +1 -1
- package/dist/esm/MatterController.d.ts +6 -6
- package/dist/esm/MatterController.d.ts.map +1 -1
- package/dist/esm/MatterController.js +13 -13
- package/dist/esm/MatterController.js.map +1 -1
- package/dist/esm/PaseCommissioner.js +1 -1
- package/dist/esm/PaseCommissioner.js.map +1 -1
- package/dist/esm/device/CachedClientNodeStore.d.ts +5 -4
- package/dist/esm/device/CachedClientNodeStore.d.ts.map +1 -1
- package/dist/esm/device/CachedClientNodeStore.js +19 -2
- package/dist/esm/device/CachedClientNodeStore.js.map +1 -1
- package/dist/esm/device/Endpoint.d.ts +1 -1
- package/dist/esm/device/Endpoint.d.ts.map +1 -1
- package/dist/esm/device/Endpoint.js.map +1 -1
- package/dist/esm/device/PairedNode.d.ts +52 -28
- package/dist/esm/device/PairedNode.d.ts.map +1 -1
- package/dist/esm/device/PairedNode.js +192 -77
- package/dist/esm/device/PairedNode.js.map +1 -1
- package/package.json +8 -8
- package/src/CommissioningController.ts +3 -3
- package/src/MatterController.ts +18 -18
- package/src/PaseCommissioner.ts +1 -1
- package/src/device/CachedClientNodeStore.ts +24 -4
- package/src/device/Endpoint.ts +1 -1
- package/src/device/PairedNode.ts +286 -88
package/src/device/PairedNode.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { AdministratorCommissioning, BasicInformation,
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
931
|
-
case
|
|
932
|
-
case
|
|
933
|
-
case
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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 =
|
|
989
|
+
const allClusterAttributes = this.#interactionClient.getAllCachedClusterData();
|
|
978
990
|
await this.#initializeEndpointStructure(allClusterAttributes, true);
|
|
979
|
-
|
|
980
|
-
|
|
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 =
|
|
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
|
|
994
|
-
const
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
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
|
|
1002
|
-
|
|
1003
|
-
this.#endpoints.get(endpointId)
|
|
1004
|
-
|
|
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
|
|
1011
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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(
|
|
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
|
-
/**
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
+
}
|