@project-chip/matter.js 0.15.3 → 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.
- package/dist/cjs/CommissioningController.d.ts +1 -1
- package/dist/cjs/CommissioningController.d.ts.map +1 -1
- package/dist/cjs/CommissioningController.js +9 -18
- 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 +52 -28
- package/dist/cjs/device/PairedNode.d.ts.map +1 -1
- package/dist/cjs/device/PairedNode.js +197 -78
- package/dist/cjs/device/PairedNode.js.map +1 -1
- package/dist/esm/CommissioningController.d.ts +1 -1
- package/dist/esm/CommissioningController.d.ts.map +1 -1
- package/dist/esm/CommissioningController.js +9 -18
- 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 +52 -28
- package/dist/esm/device/PairedNode.d.ts.map +1 -1
- package/dist/esm/device/PairedNode.js +198 -79
- package/dist/esm/device/PairedNode.js.map +1 -1
- package/package.json +8 -8
- package/src/CommissioningController.ts +10 -18
- 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 +293 -91
package/src/MatterController.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
}
|
package/src/device/Endpoint.ts
CHANGED
|
@@ -239,7 +239,7 @@ export class Endpoint {
|
|
|
239
239
|
return this.childEndpoints;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
|
|
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.`);
|
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.
|
|
@@ -260,38 +277,24 @@ export class PairedNode {
|
|
|
260
277
|
#currentSubscriptionIntervalS?: number;
|
|
261
278
|
#crypto: Crypto;
|
|
262
279
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.addAttributesToCache(attributeData);
|
|
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({
|
|
@@ -871,8 +883,7 @@ export class PairedNode {
|
|
|
871
883
|
enrichCachedAttributeData: true,
|
|
872
884
|
eventFilters: maxKnownEventNumber !== undefined ? [{ eventMin: maxKnownEventNumber + 1n }] : undefined,
|
|
873
885
|
executeQueued: !!threadConnected, // We queue subscriptions for thread devices
|
|
874
|
-
attributeListener:
|
|
875
|
-
subscriptionHandler.attributeListener(data, changed, oldValue),
|
|
886
|
+
attributeListener: subscriptionHandler.attributeListener,
|
|
876
887
|
eventListener: data => subscriptionHandler.eventListener(data),
|
|
877
888
|
updateTimeoutHandler: () => subscriptionHandler.updateTimeoutHandler(),
|
|
878
889
|
updateReceived: () => subscriptionHandler.subscriptionAlive(),
|
|
@@ -893,35 +904,35 @@ export class PairedNode {
|
|
|
893
904
|
}
|
|
894
905
|
|
|
895
906
|
#checkAttributesForNeededStructureUpdate(
|
|
896
|
-
|
|
907
|
+
endpointId: EndpointNumber,
|
|
897
908
|
clusterId: ClusterId,
|
|
898
909
|
attributeId: AttributeId,
|
|
899
910
|
) {
|
|
900
911
|
// Any change in the Descriptor Cluster partsList attribute requires a reinitialization of the endpoint structure
|
|
901
|
-
|
|
902
|
-
if (clusterId === DescriptorCluster.id) {
|
|
903
|
-
switch (attributeId) {
|
|
904
|
-
case DescriptorCluster.attributes.partsList.id:
|
|
905
|
-
case DescriptorCluster.attributes.serverList.id:
|
|
906
|
-
case DescriptorCluster.attributes.deviceTypeList.id:
|
|
907
|
-
structureUpdateNeeded = true;
|
|
908
|
-
break;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
if (!structureUpdateNeeded) {
|
|
912
|
+
if (clusterId === Descriptor.Complete.id) {
|
|
912
913
|
switch (attributeId) {
|
|
913
|
-
case
|
|
914
|
-
case
|
|
915
|
-
case
|
|
916
|
-
case
|
|
917
|
-
|
|
918
|
-
|
|
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;
|
|
919
920
|
}
|
|
920
921
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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;
|
|
925
936
|
}
|
|
926
937
|
}
|
|
927
938
|
|
|
@@ -957,72 +968,206 @@ export class PairedNode {
|
|
|
957
968
|
}
|
|
958
969
|
|
|
959
970
|
async #updateEndpointStructure() {
|
|
960
|
-
const allClusterAttributes =
|
|
971
|
+
const allClusterAttributes = this.#interactionClient.getAllCachedClusterData();
|
|
961
972
|
await this.#initializeEndpointStructure(allClusterAttributes, true);
|
|
962
|
-
|
|
963
|
-
|
|
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
|
+
);
|
|
964
1020
|
}
|
|
965
1021
|
|
|
966
1022
|
/** Reads all data from the device and create a device object structure out of it. */
|
|
967
1023
|
async #initializeEndpointStructure(
|
|
968
1024
|
allClusterAttributes: DecodedAttributeReportValue<any>[],
|
|
969
|
-
updateStructure =
|
|
1025
|
+
updateStructure = this.#localInitializationDone || this.#remoteInitializationDone,
|
|
970
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
|
+
|
|
971
1034
|
const allData = structureReadAttributeDataToClusterObject(allClusterAttributes);
|
|
972
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
|
+
|
|
973
1040
|
if (updateStructure) {
|
|
974
1041
|
// Find out what we need to remove or retain
|
|
975
1042
|
const endpointsToRemove = new Set<EndpointNumber>(this.#endpoints.keys());
|
|
976
|
-
for (const
|
|
977
|
-
const
|
|
978
|
-
if (
|
|
979
|
-
|
|
980
|
-
|
|
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
|
+
}
|
|
981
1064
|
}
|
|
982
1065
|
}
|
|
983
1066
|
// And remove all endpoints no longer in the structure
|
|
984
|
-
for (const
|
|
985
|
-
|
|
986
|
-
this.#endpoints.get(endpointId)
|
|
987
|
-
|
|
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
|
+
}
|
|
988
1078
|
}
|
|
989
1079
|
} else {
|
|
990
1080
|
this.#endpoints.clear();
|
|
991
1081
|
}
|
|
992
1082
|
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
const endpointIdNumber = EndpointNumber(parseInt(endpointId));
|
|
996
|
-
const descriptorData = clusters[DescriptorCluster.id] as AttributeClientValues<
|
|
997
|
-
typeof DescriptorCluster.attributes
|
|
998
|
-
>;
|
|
1083
|
+
for (const endpointId of descriptors.keys()) {
|
|
1084
|
+
const clusters = allData[endpointId];
|
|
999
1085
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
if (this.#endpoints.has(endpointIdNumber)) {
|
|
1086
|
+
if (this.#endpoints.has(endpointId)) {
|
|
1003
1087
|
// Endpoint exists already, so mo need to create device instance again
|
|
1004
1088
|
continue;
|
|
1005
1089
|
}
|
|
1006
1090
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1091
|
+
const isRecreation = eventsToEmit.get(endpointId) === "nodeEndpointChanged";
|
|
1092
|
+
logger.debug(
|
|
1093
|
+
`Node ${this.nodeId}: ${isRecreation ? "Recreating" : "Creating"} endpoint`,
|
|
1094
|
+
endpointId,
|
|
1095
|
+
Diagnostic.json(clusters),
|
|
1011
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
|
+
});
|
|
1012
1119
|
}
|
|
1013
1120
|
|
|
1014
|
-
this.#structureEndpoints(
|
|
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
|
+
}
|
|
1015
1157
|
}
|
|
1016
1158
|
|
|
1017
|
-
/**
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
1022
1166
|
);
|
|
1167
|
+
logger.debug(`Node ${this.nodeId}: Endpoints from PartsLists`, Diagnostic.json(partLists));
|
|
1023
1168
|
|
|
1024
1169
|
const endpointUsages: { [key: EndpointNumber]: EndpointNumber[] } = {};
|
|
1025
|
-
|
|
1170
|
+
partLists.forEach(([parent, partsList]) =>
|
|
1026
1171
|
partsList.forEach(endPoint => {
|
|
1027
1172
|
if (endPoint === parent) {
|
|
1028
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
|
|
@@ -1052,14 +1197,19 @@ export class PairedNode {
|
|
|
1052
1197
|
const childEndpointId = EndpointNumber(parseInt(childId));
|
|
1053
1198
|
const childEndpoint = this.#endpoints.get(childEndpointId);
|
|
1054
1199
|
const parentEndpoint = this.#endpoints.get(usages[0]);
|
|
1200
|
+
const existingChildEndpoint = parentEndpoint?.getChildEndpoint(childEndpointId);
|
|
1055
1201
|
if (childEndpoint === undefined || parentEndpoint === undefined) {
|
|
1056
1202
|
logger.warn(
|
|
1057
1203
|
`Node ${this.nodeId}: Endpoint ${usages[0]} not found in the data received from the device!`,
|
|
1058
1204
|
);
|
|
1059
|
-
} else if (
|
|
1205
|
+
} else if (existingChildEndpoint !== childEndpoint) {
|
|
1060
1206
|
logger.debug(
|
|
1061
1207
|
`Node ${this.nodeId}: Endpoint structure: Child: ${childEndpointId} -> Parent: ${parentEndpoint.number}`,
|
|
1062
1208
|
);
|
|
1209
|
+
if (existingChildEndpoint !== undefined) {
|
|
1210
|
+
// Child endpoint changed, so we need to remove the old one first
|
|
1211
|
+
parentEndpoint.removeChildEndpoint(existingChildEndpoint);
|
|
1212
|
+
}
|
|
1063
1213
|
|
|
1064
1214
|
parentEndpoint.addChildEndpoint(childEndpoint);
|
|
1065
1215
|
}
|
|
@@ -1088,7 +1238,7 @@ export class PairedNode {
|
|
|
1088
1238
|
data: { [key: ClusterId]: { [key: string]: any } },
|
|
1089
1239
|
interactionClient: InteractionClient,
|
|
1090
1240
|
) {
|
|
1091
|
-
const descriptorData = data[
|
|
1241
|
+
const descriptorData = data[Descriptor.Complete.id] as DescriptorData;
|
|
1092
1242
|
|
|
1093
1243
|
const deviceTypes = descriptorData.deviceTypeList.flatMap(({ deviceType, revision }) => {
|
|
1094
1244
|
const deviceTypeDefinition = getDeviceTypeDefinitionFromModelByCode(deviceType);
|
|
@@ -1417,3 +1567,55 @@ export class PairedNode {
|
|
|
1417
1567
|
});
|
|
1418
1568
|
}
|
|
1419
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
|
+
}
|