@project-chip/matter.js 0.15.4 → 0.15.6
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 -1
- package/dist/cjs/CommissioningController.d.ts.map +1 -1
- package/dist/cjs/CommissioningController.js +26 -35
- package/dist/cjs/CommissioningController.js.map +1 -1
- package/dist/cjs/MatterController.d.ts.map +1 -1
- package/dist/cjs/MatterController.js +2 -2
- package/dist/cjs/MatterController.js.map +1 -1
- package/dist/cjs/device/CachedClientNodeStore.d.ts +4 -3
- 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 +56 -28
- package/dist/cjs/device/PairedNode.d.ts.map +1 -1
- package/dist/cjs/device/PairedNode.js +217 -78
- package/dist/cjs/device/PairedNode.js.map +1 -1
- package/dist/esm/CommissioningController.d.ts +2 -1
- package/dist/esm/CommissioningController.d.ts.map +1 -1
- package/dist/esm/CommissioningController.js +26 -35
- package/dist/esm/CommissioningController.js.map +1 -1
- package/dist/esm/MatterController.d.ts.map +1 -1
- package/dist/esm/MatterController.js +2 -3
- package/dist/esm/MatterController.js.map +1 -1
- package/dist/esm/device/CachedClientNodeStore.d.ts +4 -3
- 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 +56 -28
- package/dist/esm/device/PairedNode.d.ts.map +1 -1
- package/dist/esm/device/PairedNode.js +218 -79
- package/dist/esm/device/PairedNode.js.map +1 -1
- package/package.json +8 -8
- package/src/CommissioningController.ts +31 -38
- package/src/MatterController.ts +3 -4
- package/src/device/CachedClientNodeStore.ts +24 -4
- package/src/device/Endpoint.ts +1 -1
- package/src/device/PairedNode.ts +322 -94
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,
|
|
@@ -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.
|
|
@@ -259,39 +276,27 @@ export class PairedNode {
|
|
|
259
276
|
readonly #reconnectFunc: (discoveryType?: NodeDiscoveryType, noForcedConnection?: boolean) => Promise<void>;
|
|
260
277
|
#currentSubscriptionIntervalS?: number;
|
|
261
278
|
#crypto: Crypto;
|
|
279
|
+
#deviceInformationUpdateNeeded = false;
|
|
262
280
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
initialized: AsyncObservable<[details: DeviceInformationData]>(),
|
|
281
|
+
/**
|
|
282
|
+
* Endpoint structure change information that are checked when updating structure
|
|
283
|
+
* - null means that the endpoint itself changed, so will be regenerated completely any case
|
|
284
|
+
* - array of ClusterIds means that only these clusters changed and will be updated
|
|
285
|
+
*/
|
|
286
|
+
#registeredEndpointStructureChanges = new Map<EndpointNumber, ClusterId[] | null>();
|
|
270
287
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
* This event can also be awaited if code needs to be blocked until the node is fully initialized.
|
|
274
|
-
*/
|
|
288
|
+
readonly events: PairedNode.Events = {
|
|
289
|
+
initialized: AsyncObservable<[details: DeviceInformationData]>(),
|
|
275
290
|
initializedFromRemote: AsyncObservable<[details: DeviceInformationData]>(),
|
|
276
|
-
|
|
277
|
-
/** Emitted when the state of the node changes. */
|
|
291
|
+
deviceInformationChanged: AsyncObservable<[details: DeviceInformationData]>(),
|
|
278
292
|
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
293
|
attributeChanged: Observable<[data: DecodedAttributeReportValue<any>, oldValue: any]>(),
|
|
284
|
-
|
|
285
|
-
/** Emitted when an event is triggered. */
|
|
286
294
|
eventTriggered: Observable<[DecodedEventReportValue<any>]>(),
|
|
287
|
-
|
|
288
|
-
/** Emitted when the structure of the node changes (Endpoints got added or also removed). */
|
|
289
295
|
structureChanged: Observable<[void]>(),
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
nodeEndpointAdded: Observable<[EndpointNumber]>(),
|
|
297
|
+
nodeEndpointRemoved: Observable<[EndpointNumber]>(),
|
|
298
|
+
nodeEndpointChanged: Observable<[EndpointNumber]>(),
|
|
292
299
|
decommissioned: Observable<[void]>(),
|
|
293
|
-
|
|
294
|
-
/** Emitted when a subscription alive trigger is received (max interval trigger or any data update) */
|
|
295
300
|
connectionAlive: Observable<[void]>(),
|
|
296
301
|
};
|
|
297
302
|
|
|
@@ -633,7 +638,7 @@ export class PairedNode {
|
|
|
633
638
|
return;
|
|
634
639
|
}
|
|
635
640
|
|
|
636
|
-
await this.#initializeEndpointStructure(storedAttributeData);
|
|
641
|
+
await this.#initializeEndpointStructure(storedAttributeData, false);
|
|
637
642
|
|
|
638
643
|
// Inform interested parties that the node is initialized
|
|
639
644
|
await this.events.initialized.emit(this.#nodeDetails.toStorageData());
|
|
@@ -678,7 +683,7 @@ export class PairedNode {
|
|
|
678
683
|
if (attributeReports === undefined) {
|
|
679
684
|
throw new InternalError("No attribute reports received when subscribing to all values!");
|
|
680
685
|
}
|
|
681
|
-
await this.#initializeEndpointStructure(attributeReports
|
|
686
|
+
await this.#initializeEndpointStructure(attributeReports);
|
|
682
687
|
|
|
683
688
|
this.#remoteInitializationInProgress = false; // We are done, rest is bonus and should not block reconnections
|
|
684
689
|
|
|
@@ -691,7 +696,7 @@ export class PairedNode {
|
|
|
691
696
|
this.#currentSubscriptionIntervalS = maxInterval;
|
|
692
697
|
} else {
|
|
693
698
|
const allClusterAttributes = await this.readAllAttributes();
|
|
694
|
-
await this.#initializeEndpointStructure(allClusterAttributes
|
|
699
|
+
await this.#initializeEndpointStructure(allClusterAttributes);
|
|
695
700
|
this.#remoteInitializationInProgress = false; // We are done, rest is bonus and should not block reconnections
|
|
696
701
|
}
|
|
697
702
|
if (!this.#remoteInitializationDone) {
|
|
@@ -786,7 +791,7 @@ export class PairedNode {
|
|
|
786
791
|
asClusterClientInternal(cluster)._triggerAttributeUpdate(attributeId, value);
|
|
787
792
|
attributeChangedCallback?.(data, oldValue);
|
|
788
793
|
|
|
789
|
-
this.#
|
|
794
|
+
this.#checkAttributesForNeededUpdates(endpointId, clusterId, attributeId);
|
|
790
795
|
},
|
|
791
796
|
eventListener: data => {
|
|
792
797
|
if (ignoreInitialTriggers) return;
|
|
@@ -845,6 +850,30 @@ export class PairedNode {
|
|
|
845
850
|
this.#reconnectDelayTimer = undefined;
|
|
846
851
|
this.#setConnectionState(NodeStates.Connected);
|
|
847
852
|
}
|
|
853
|
+
|
|
854
|
+
if (
|
|
855
|
+
this.#remoteInitializationDone &&
|
|
856
|
+
this.#registeredEndpointStructureChanges.size > 0 &&
|
|
857
|
+
!this.#updateEndpointStructureTimer.isRunning
|
|
858
|
+
) {
|
|
859
|
+
logger.info(`Node ${this.nodeId}: Endpoint structure needs to be updated ...`);
|
|
860
|
+
this.#updateEndpointStructureTimer.stop().start();
|
|
861
|
+
} else if (this.#deviceInformationUpdateNeeded) {
|
|
862
|
+
const rootEndpoint = this.getRootEndpoint();
|
|
863
|
+
if (rootEndpoint !== undefined) {
|
|
864
|
+
this.#nodeDetails
|
|
865
|
+
.enhanceDeviceDetailsFromCache(rootEndpoint)
|
|
866
|
+
.then(() => this.events.deviceInformationChanged.emit(this.#nodeDetails.toStorageData()))
|
|
867
|
+
.catch(error =>
|
|
868
|
+
logger.warn(
|
|
869
|
+
`Node ${this.nodeId}: Error updating device information from root endpoint`,
|
|
870
|
+
error,
|
|
871
|
+
),
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
this.#deviceInformationUpdateNeeded = false;
|
|
876
|
+
|
|
848
877
|
this.events.connectionAlive.emit();
|
|
849
878
|
},
|
|
850
879
|
};
|
|
@@ -854,12 +883,11 @@ export class PairedNode {
|
|
|
854
883
|
|
|
855
884
|
// We first update all values by doing a read all on the device
|
|
856
885
|
// We do not enrich existing data because we just want to store updated data
|
|
857
|
-
|
|
886
|
+
await this.#interactionClient.getAllAttributes({
|
|
858
887
|
dataVersionFilters: this.#interactionClient.getCachedClusterDataVersions(),
|
|
859
888
|
executeQueued: !!threadConnected, // We queue subscriptions for thread devices
|
|
889
|
+
attributeChangeListener: subscriptionHandler.attributeListener,
|
|
860
890
|
});
|
|
861
|
-
await this.#interactionClient.processAttributeUpdates(attributeData, subscriptionHandler.attributeListener);
|
|
862
|
-
attributeData.length = 0; // Clear the array to save memory
|
|
863
891
|
|
|
864
892
|
// If we subscribe anything we use these data to create the endpoint structure, so we do not need to fetch again
|
|
865
893
|
const initialSubscriptionData = await this.#interactionClient.subscribeAllAttributesAndEvents({
|
|
@@ -891,36 +919,34 @@ export class PairedNode {
|
|
|
891
919
|
});
|
|
892
920
|
}
|
|
893
921
|
|
|
894
|
-
#
|
|
895
|
-
_endpointId: EndpointNumber,
|
|
896
|
-
clusterId: ClusterId,
|
|
897
|
-
attributeId: AttributeId,
|
|
898
|
-
) {
|
|
922
|
+
#checkAttributesForNeededUpdates(endpointId: EndpointNumber, clusterId: ClusterId, attributeId: AttributeId) {
|
|
899
923
|
// Any change in the Descriptor Cluster partsList attribute requires a reinitialization of the endpoint structure
|
|
900
|
-
|
|
901
|
-
if (clusterId === DescriptorCluster.id) {
|
|
924
|
+
if (clusterId === Descriptor.Complete.id) {
|
|
902
925
|
switch (attributeId) {
|
|
903
|
-
case
|
|
904
|
-
case
|
|
905
|
-
case
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
if (!structureUpdateNeeded) {
|
|
911
|
-
switch (attributeId) {
|
|
912
|
-
case FeatureMap.id:
|
|
913
|
-
case AttributeList.id:
|
|
914
|
-
case AcceptedCommandList.id:
|
|
915
|
-
case ClusterRevision.id:
|
|
916
|
-
structureUpdateNeeded = true;
|
|
917
|
-
break;
|
|
926
|
+
case Descriptor.Complete.attributes.partsList.id:
|
|
927
|
+
case Descriptor.Complete.attributes.serverList.id:
|
|
928
|
+
case Descriptor.Complete.attributes.clientList.id:
|
|
929
|
+
case Descriptor.Complete.attributes.deviceTypeList.id:
|
|
930
|
+
this.#registeredEndpointStructureChanges.set(endpointId, null); // full endpoint update needed
|
|
931
|
+
return;
|
|
918
932
|
}
|
|
933
|
+
} else if (clusterId === BasicInformation.Cluster.id) {
|
|
934
|
+
this.#deviceInformationUpdateNeeded = true;
|
|
919
935
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
936
|
+
switch (attributeId) {
|
|
937
|
+
case FeatureMap.id:
|
|
938
|
+
case AttributeList.id:
|
|
939
|
+
case AcceptedCommandList.id:
|
|
940
|
+
case ClusterRevision.id:
|
|
941
|
+
let knownForUpdate = this.#registeredEndpointStructureChanges.get(endpointId);
|
|
942
|
+
if (knownForUpdate !== null) {
|
|
943
|
+
knownForUpdate = knownForUpdate ?? [];
|
|
944
|
+
if (!knownForUpdate.includes(clusterId)) {
|
|
945
|
+
knownForUpdate.push(clusterId);
|
|
946
|
+
this.#registeredEndpointStructureChanges.set(endpointId, knownForUpdate);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
break;
|
|
924
950
|
}
|
|
925
951
|
}
|
|
926
952
|
|
|
@@ -956,72 +982,212 @@ export class PairedNode {
|
|
|
956
982
|
}
|
|
957
983
|
|
|
958
984
|
async #updateEndpointStructure() {
|
|
959
|
-
const allClusterAttributes =
|
|
985
|
+
const allClusterAttributes = this.#interactionClient.getAllCachedClusterData();
|
|
960
986
|
await this.#initializeEndpointStructure(allClusterAttributes, true);
|
|
961
|
-
|
|
962
|
-
this.
|
|
987
|
+
|
|
988
|
+
const rootEndpoint = this.getRootEndpoint();
|
|
989
|
+
if (rootEndpoint !== undefined) {
|
|
990
|
+
await this.#nodeDetails.enhanceDeviceDetailsFromCache(rootEndpoint);
|
|
991
|
+
await this.events.deviceInformationChanged.emit(this.#nodeDetails.toStorageData());
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Traverse the structure data and collect all data for the given endpointId.
|
|
997
|
+
* Return true if data for the endpoint was found, otherwise false.
|
|
998
|
+
* If data was found it is added to the collectedData map.
|
|
999
|
+
*/
|
|
1000
|
+
collectDescriptorData(
|
|
1001
|
+
structure: StructuredReadAttributeData,
|
|
1002
|
+
endpointId: EndpointNumber,
|
|
1003
|
+
collectedData: Map<EndpointNumber, DescriptorData>,
|
|
1004
|
+
) {
|
|
1005
|
+
if (collectedData.has(endpointId)) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const endpointData = structure[endpointId];
|
|
1009
|
+
const descriptorData = endpointData?.[Descriptor.Complete.id] as DescriptorData | undefined;
|
|
1010
|
+
if (endpointData === undefined || descriptorData === undefined) {
|
|
1011
|
+
logger.info(`Descriptor data for endpoint ${endpointId} not found in structure! Ignoring endpoint ...`);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
collectedData.set(endpointId, descriptorData);
|
|
1015
|
+
if (descriptorData.partsList.length) {
|
|
1016
|
+
for (const partEndpointId of descriptorData.partsList) {
|
|
1017
|
+
this.collectDescriptorData(structure, partEndpointId, collectedData);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
#hasEndpointChanged(device: Endpoint, descriptorData: DescriptorData) {
|
|
1023
|
+
// Check if the device types (ignoring revision for now), or cluster server or cluster clients differ
|
|
1024
|
+
return !(
|
|
1025
|
+
areNumberListsSame(
|
|
1026
|
+
device.getDeviceTypes().map(({ code }) => code),
|
|
1027
|
+
descriptorData.deviceTypeList.map(({ deviceType }) => deviceType),
|
|
1028
|
+
) &&
|
|
1029
|
+
// Check if the cluster clients are the same - they map to the serverList attribute
|
|
1030
|
+
areNumberListsSame(
|
|
1031
|
+
device.getAllClusterClients().map(({ id }) => id),
|
|
1032
|
+
descriptorData.serverList,
|
|
1033
|
+
) &&
|
|
1034
|
+
// Check if the cluster servers are the same - they map to the clientList attribute
|
|
1035
|
+
areNumberListsSame(
|
|
1036
|
+
device.getAllClusterServers().map(({ id }) => id),
|
|
1037
|
+
descriptorData.clientList,
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
963
1040
|
}
|
|
964
1041
|
|
|
965
1042
|
/** Reads all data from the device and create a device object structure out of it. */
|
|
966
1043
|
async #initializeEndpointStructure(
|
|
967
1044
|
allClusterAttributes: DecodedAttributeReportValue<any>[],
|
|
968
|
-
updateStructure =
|
|
1045
|
+
updateStructure = this.#localInitializationDone || this.#remoteInitializationDone,
|
|
969
1046
|
) {
|
|
1047
|
+
if (this.#updateEndpointStructureTimer.isRunning) {
|
|
1048
|
+
this.#updateEndpointStructureTimer.stop();
|
|
1049
|
+
}
|
|
1050
|
+
const eventsToEmit = new Map<EndpointNumber, keyof PairedNode.NodeStructureEvents>();
|
|
1051
|
+
const structureUpdateDetails = this.#registeredEndpointStructureChanges;
|
|
1052
|
+
this.#registeredEndpointStructureChanges = new Map();
|
|
1053
|
+
|
|
970
1054
|
const allData = structureReadAttributeDataToClusterObject(allClusterAttributes);
|
|
971
1055
|
|
|
1056
|
+
// Collect the descriptor data for all endpoints referenced in the structure
|
|
1057
|
+
const descriptors = new Map<EndpointNumber, DescriptorData>();
|
|
1058
|
+
this.collectDescriptorData(allData, EndpointNumber(0), descriptors);
|
|
1059
|
+
|
|
972
1060
|
if (updateStructure) {
|
|
973
1061
|
// Find out what we need to remove or retain
|
|
974
1062
|
const endpointsToRemove = new Set<EndpointNumber>(this.#endpoints.keys());
|
|
975
|
-
for (const
|
|
976
|
-
const
|
|
977
|
-
if (
|
|
978
|
-
|
|
979
|
-
|
|
1063
|
+
for (const endpointId of descriptors.keys()) {
|
|
1064
|
+
const device = this.#endpoints.get(endpointId);
|
|
1065
|
+
if (device !== undefined) {
|
|
1066
|
+
// Check if there are any changes to the device that require a re-creation
|
|
1067
|
+
// When structureUpdateDetails from subscription updates state changes we do a deep validation
|
|
1068
|
+
// to prevent ordering changes to cause unnecessary device re-creations
|
|
1069
|
+
const hasChanged = structureUpdateDetails.has(endpointId);
|
|
1070
|
+
if (!hasChanged || !this.#hasEndpointChanged(device, descriptors.get(endpointId)!)) {
|
|
1071
|
+
logger.debug(
|
|
1072
|
+
`Node ${this.nodeId}: Retaining endpoint`,
|
|
1073
|
+
endpointId,
|
|
1074
|
+
hasChanged ? "(with only structure changes)" : "(unchanged)",
|
|
1075
|
+
);
|
|
1076
|
+
endpointsToRemove.delete(endpointId);
|
|
1077
|
+
if (hasChanged) {
|
|
1078
|
+
eventsToEmit.set(endpointId, "nodeEndpointChanged");
|
|
1079
|
+
}
|
|
1080
|
+
} else {
|
|
1081
|
+
logger.debug(`Node ${this.nodeId}: Recreating endpoint`, endpointId);
|
|
1082
|
+
eventsToEmit.set(endpointId, "nodeEndpointChanged");
|
|
1083
|
+
}
|
|
980
1084
|
}
|
|
981
1085
|
}
|
|
982
1086
|
// And remove all endpoints no longer in the structure
|
|
983
|
-
for (const
|
|
984
|
-
|
|
985
|
-
this.#endpoints.get(endpointId)
|
|
986
|
-
|
|
1087
|
+
for (const endpoint of endpointsToRemove.values()) {
|
|
1088
|
+
const endpointId = EndpointNumber(endpoint);
|
|
1089
|
+
const device = this.#endpoints.get(endpointId);
|
|
1090
|
+
if (device !== undefined) {
|
|
1091
|
+
if (eventsToEmit.get(endpointId) !== "nodeEndpointChanged") {
|
|
1092
|
+
logger.debug(`Node ${this.nodeId}: Removing endpoint`, endpointId);
|
|
1093
|
+
eventsToEmit.set(endpointId, "nodeEndpointRemoved");
|
|
1094
|
+
}
|
|
1095
|
+
device.removeFromStructure();
|
|
1096
|
+
this.#endpoints.delete(endpointId);
|
|
1097
|
+
}
|
|
987
1098
|
}
|
|
988
1099
|
} else {
|
|
989
1100
|
this.#endpoints.clear();
|
|
990
1101
|
}
|
|
991
1102
|
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
const endpointIdNumber = EndpointNumber(parseInt(endpointId));
|
|
995
|
-
const descriptorData = clusters[DescriptorCluster.id] as AttributeClientValues<
|
|
996
|
-
typeof DescriptorCluster.attributes
|
|
997
|
-
>;
|
|
1103
|
+
for (const endpointId of descriptors.keys()) {
|
|
1104
|
+
const clusters = allData[endpointId];
|
|
998
1105
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
if (this.#endpoints.has(endpointIdNumber)) {
|
|
1106
|
+
if (this.#endpoints.has(endpointId)) {
|
|
1002
1107
|
// Endpoint exists already, so mo need to create device instance again
|
|
1003
1108
|
continue;
|
|
1004
1109
|
}
|
|
1005
1110
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1111
|
+
const isRecreation = eventsToEmit.get(endpointId) === "nodeEndpointChanged";
|
|
1112
|
+
logger.debug(
|
|
1113
|
+
`Node ${this.nodeId}: ${isRecreation ? "Recreating" : "Creating"} endpoint`,
|
|
1114
|
+
endpointId,
|
|
1115
|
+
Diagnostic.json(clusters),
|
|
1010
1116
|
);
|
|
1117
|
+
this.#endpoints.set(endpointId, this.#createDevice(endpointId, clusters, this.#interactionClient));
|
|
1118
|
+
if (!isRecreation) {
|
|
1119
|
+
eventsToEmit.set(endpointId, "nodeEndpointAdded");
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Remove all children that are not in the partsList anymore
|
|
1124
|
+
for (const [endpointId, { partsList }] of descriptors.entries()) {
|
|
1125
|
+
const endpoint = this.#endpoints.get(endpointId);
|
|
1126
|
+
if (endpoint === undefined) {
|
|
1127
|
+
// Should not happen or endpoint was invalid and that's why not created, then we ignore it
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
endpoint.getChildEndpoints().forEach(child => {
|
|
1131
|
+
if (child.number !== undefined && !partsList.includes(child.number)) {
|
|
1132
|
+
// Remove this child because it is no longer in the partsList
|
|
1133
|
+
endpoint.removeChildEndpoint(child);
|
|
1134
|
+
if (!eventsToEmit.has(endpointId)) {
|
|
1135
|
+
eventsToEmit.set(endpointId, "nodeEndpointChanged");
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1011
1139
|
}
|
|
1012
1140
|
|
|
1013
|
-
this.#structureEndpoints(
|
|
1141
|
+
this.#structureEndpoints(descriptors);
|
|
1142
|
+
|
|
1143
|
+
if (updateStructure && eventsToEmit.size) {
|
|
1144
|
+
for (const [endpointId, eventName] of eventsToEmit.entries()) {
|
|
1145
|
+
// Cleanup storage data for removed or updated endpoints
|
|
1146
|
+
if (eventName !== "nodeEndpointAdded") {
|
|
1147
|
+
// For removed or changed endpoints we need to cleanup the stored data
|
|
1148
|
+
const clusterServers = descriptors.get(endpointId)?.serverList;
|
|
1149
|
+
await this.#interactionClient.cleanupAttributeData(
|
|
1150
|
+
endpointId,
|
|
1151
|
+
eventName === "nodeEndpointRemoved" ? undefined : clusterServers,
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const emitChangeEvents = () => {
|
|
1157
|
+
for (const [endpointId, eventName] of eventsToEmit.entries()) {
|
|
1158
|
+
logger.debug(`Node ${this.nodeId}: Emitting event ${eventName} for endpoint ${endpointId}`);
|
|
1159
|
+
this.events[eventName].emit(endpointId);
|
|
1160
|
+
}
|
|
1161
|
+
this.#options.stateInformationCallback?.(this.nodeId, NodeStateInformation.StructureChanged);
|
|
1162
|
+
this.events.structureChanged.emit();
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
if (this.#connectionState === NodeStates.Connected) {
|
|
1166
|
+
// If we are connected we can emit the events right away
|
|
1167
|
+
emitChangeEvents();
|
|
1168
|
+
} else {
|
|
1169
|
+
// If we are not connected we need to wait until we are connected again and emit these changes afterwards
|
|
1170
|
+
this.events.stateChanged.once(State => {
|
|
1171
|
+
if (State === NodeStates.Connected) {
|
|
1172
|
+
emitChangeEvents();
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1014
1177
|
}
|
|
1015
1178
|
|
|
1016
|
-
/**
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1179
|
+
/**
|
|
1180
|
+
* Bring the endpoints in a structure based on their partsList attribute. This method only adds endpoints into the
|
|
1181
|
+
* right place as children, Cleanup is not happening here
|
|
1182
|
+
*/
|
|
1183
|
+
#structureEndpoints(descriptors: Map<EndpointNumber, DescriptorData>) {
|
|
1184
|
+
const partLists = Array.from(descriptors.entries()).map(
|
|
1185
|
+
([ep, { partsList }]) => [ep, partsList] as [EndpointNumber, EndpointNumber[]], // else Typescript gets confused
|
|
1021
1186
|
);
|
|
1187
|
+
logger.debug(`Node ${this.nodeId}: Endpoints from PartsLists`, Diagnostic.json(partLists));
|
|
1022
1188
|
|
|
1023
1189
|
const endpointUsages: { [key: EndpointNumber]: EndpointNumber[] } = {};
|
|
1024
|
-
|
|
1190
|
+
partLists.forEach(([parent, partsList]) =>
|
|
1025
1191
|
partsList.forEach(endPoint => {
|
|
1026
1192
|
if (endPoint === parent) {
|
|
1027
1193
|
// 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 +1217,19 @@ export class PairedNode {
|
|
|
1051
1217
|
const childEndpointId = EndpointNumber(parseInt(childId));
|
|
1052
1218
|
const childEndpoint = this.#endpoints.get(childEndpointId);
|
|
1053
1219
|
const parentEndpoint = this.#endpoints.get(usages[0]);
|
|
1220
|
+
const existingChildEndpoint = parentEndpoint?.getChildEndpoint(childEndpointId);
|
|
1054
1221
|
if (childEndpoint === undefined || parentEndpoint === undefined) {
|
|
1055
1222
|
logger.warn(
|
|
1056
1223
|
`Node ${this.nodeId}: Endpoint ${usages[0]} not found in the data received from the device!`,
|
|
1057
1224
|
);
|
|
1058
|
-
} else if (
|
|
1225
|
+
} else if (existingChildEndpoint !== childEndpoint) {
|
|
1059
1226
|
logger.debug(
|
|
1060
1227
|
`Node ${this.nodeId}: Endpoint structure: Child: ${childEndpointId} -> Parent: ${parentEndpoint.number}`,
|
|
1061
1228
|
);
|
|
1229
|
+
if (existingChildEndpoint !== undefined) {
|
|
1230
|
+
// Child endpoint changed, so we need to remove the old one first
|
|
1231
|
+
parentEndpoint.removeChildEndpoint(existingChildEndpoint);
|
|
1232
|
+
}
|
|
1062
1233
|
|
|
1063
1234
|
parentEndpoint.addChildEndpoint(childEndpoint);
|
|
1064
1235
|
}
|
|
@@ -1087,7 +1258,7 @@ export class PairedNode {
|
|
|
1087
1258
|
data: { [key: ClusterId]: { [key: string]: any } },
|
|
1088
1259
|
interactionClient: InteractionClient,
|
|
1089
1260
|
) {
|
|
1090
|
-
const descriptorData = data[
|
|
1261
|
+
const descriptorData = data[Descriptor.Complete.id] as DescriptorData;
|
|
1091
1262
|
|
|
1092
1263
|
const deviceTypes = descriptorData.deviceTypeList.flatMap(({ deviceType, revision }) => {
|
|
1093
1264
|
const deviceTypeDefinition = getDeviceTypeDefinitionFromModelByCode(deviceType);
|
|
@@ -1416,3 +1587,60 @@ export class PairedNode {
|
|
|
1416
1587
|
});
|
|
1417
1588
|
}
|
|
1418
1589
|
}
|
|
1590
|
+
|
|
1591
|
+
export namespace PairedNode {
|
|
1592
|
+
export interface NodeStructureEvents {
|
|
1593
|
+
/** Emitted when endpoints are added. */
|
|
1594
|
+
nodeEndpointAdded: Observable<[EndpointNumber]>;
|
|
1595
|
+
|
|
1596
|
+
/** Emitted when endpoints are removed. */
|
|
1597
|
+
nodeEndpointRemoved: Observable<[EndpointNumber]>;
|
|
1598
|
+
|
|
1599
|
+
/** Emitted when endpoints are updated (e.g. device type changed, structure changed). */
|
|
1600
|
+
nodeEndpointChanged: Observable<[EndpointNumber]>;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
export interface Events extends NodeStructureEvents {
|
|
1604
|
+
/**
|
|
1605
|
+
* Emitted when the node is initialized from local data. These data usually are stale, but you can still already
|
|
1606
|
+
* use the node to interact with the device. If no local data are available this event will be emitted together
|
|
1607
|
+
* with the initializedFromRemote event.
|
|
1608
|
+
*/
|
|
1609
|
+
initialized: AsyncObservable<[details: DeviceInformationData]>;
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Emitted when the node is fully initialized from remote and all attributes and events are subscribed.
|
|
1613
|
+
* This event can also be awaited if code needs to be blocked until the node is fully initialized.
|
|
1614
|
+
*/
|
|
1615
|
+
initializedFromRemote: AsyncObservable<[details: DeviceInformationData]>;
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* Emitted when the device information changes.
|
|
1619
|
+
*/
|
|
1620
|
+
deviceInformationChanged: AsyncObservable<[details: DeviceInformationData]>;
|
|
1621
|
+
|
|
1622
|
+
/** Emitted when the state of the node changes. */
|
|
1623
|
+
stateChanged: Observable<[nodeState: NodeStates]>;
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Emitted when an attribute value changes. If the oldValue is undefined then no former value was known.
|
|
1627
|
+
*/
|
|
1628
|
+
attributeChanged: Observable<[data: DecodedAttributeReportValue<any>, oldValue: any]>;
|
|
1629
|
+
|
|
1630
|
+
/** Emitted when an event is triggered. */
|
|
1631
|
+
eventTriggered: Observable<[DecodedEventReportValue<any>]>;
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Emitted when all node structure changes were applied (Endpoints got added or also removed).
|
|
1635
|
+
* You can alternatively use the nodeEndpointAdded, nodeEndpointRemoved and nodeEndpointChanged events to react on specific changes.
|
|
1636
|
+
* This event is emitted after all nodeEndpointAdded, nodeEndpointRemoved and nodeEndpointChanged events are emitted.
|
|
1637
|
+
*/
|
|
1638
|
+
structureChanged: Observable<[void]>;
|
|
1639
|
+
|
|
1640
|
+
/** Emitted when the node is decommissioned. */
|
|
1641
|
+
decommissioned: Observable<[void]>;
|
|
1642
|
+
|
|
1643
|
+
/** Emitted when a subscription alive trigger is received (max interval trigger or any data update) */
|
|
1644
|
+
connectionAlive: Observable<[void]>;
|
|
1645
|
+
}
|
|
1646
|
+
}
|