@project-chip/matter.js 0.13.1-alpha.0-20250520-d699cd56d → 0.14.0-alpha.0-20250524-51a7e1721

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 (105) hide show
  1. package/dist/cjs/CommissioningController.d.ts.map +1 -1
  2. package/dist/cjs/CommissioningController.js +18 -5
  3. package/dist/cjs/CommissioningController.js.map +1 -1
  4. package/dist/cjs/cluster/export.d.ts +1 -2
  5. package/dist/cjs/cluster/export.d.ts.map +1 -1
  6. package/dist/cjs/cluster/export.js +1 -2
  7. package/dist/cjs/cluster/export.js.map +1 -1
  8. package/dist/cjs/cluster/server/AttributeServer.d.ts +298 -0
  9. package/dist/cjs/cluster/server/AttributeServer.d.ts.map +1 -0
  10. package/dist/cjs/cluster/server/AttributeServer.js +652 -0
  11. package/dist/cjs/cluster/server/AttributeServer.js.map +6 -0
  12. package/dist/cjs/cluster/server/ClusterDatasource.d.ts +15 -0
  13. package/dist/cjs/cluster/server/ClusterDatasource.d.ts.map +1 -0
  14. package/dist/cjs/cluster/server/ClusterDatasource.js +22 -0
  15. package/dist/cjs/cluster/server/ClusterDatasource.js.map +6 -0
  16. package/dist/cjs/cluster/server/ClusterServer.d.ts +2 -2
  17. package/dist/cjs/cluster/server/ClusterServer.d.ts.map +1 -1
  18. package/dist/cjs/cluster/server/ClusterServer.js +6 -4
  19. package/dist/cjs/cluster/server/ClusterServer.js.map +1 -1
  20. package/dist/cjs/cluster/server/ClusterServerTypes.d.ts +4 -1
  21. package/dist/cjs/cluster/server/ClusterServerTypes.d.ts.map +1 -1
  22. package/dist/cjs/cluster/server/ClusterServerTypes.js.map +1 -1
  23. package/dist/cjs/cluster/server/CommandServer.d.ts +33 -0
  24. package/dist/cjs/cluster/server/CommandServer.d.ts.map +1 -0
  25. package/dist/cjs/cluster/server/CommandServer.js +76 -0
  26. package/dist/cjs/cluster/server/CommandServer.js.map +6 -0
  27. package/dist/cjs/cluster/server/EventServer.d.ts +39 -0
  28. package/dist/cjs/cluster/server/EventServer.d.ts.map +1 -0
  29. package/dist/cjs/cluster/server/EventServer.js +152 -0
  30. package/dist/cjs/cluster/server/EventServer.js.map +6 -0
  31. package/dist/cjs/cluster/server/index.d.ts +11 -0
  32. package/dist/cjs/cluster/server/index.d.ts.map +1 -0
  33. package/dist/cjs/cluster/server/index.js +28 -0
  34. package/dist/cjs/cluster/server/index.js.map +6 -0
  35. package/dist/cjs/device/Endpoint.d.ts +9 -5
  36. package/dist/cjs/device/Endpoint.d.ts.map +1 -1
  37. package/dist/cjs/device/Endpoint.js +115 -29
  38. package/dist/cjs/device/Endpoint.js.map +1 -1
  39. package/dist/cjs/device/PairedNode.d.ts +5 -4
  40. package/dist/cjs/device/PairedNode.d.ts.map +1 -1
  41. package/dist/cjs/device/PairedNode.js +26 -3
  42. package/dist/cjs/device/PairedNode.js.map +1 -1
  43. package/dist/cjs/device/export.d.ts +0 -1
  44. package/dist/cjs/device/export.d.ts.map +1 -1
  45. package/dist/cjs/device/export.js +0 -8
  46. package/dist/cjs/device/export.js.map +1 -1
  47. package/dist/esm/CommissioningController.d.ts.map +1 -1
  48. package/dist/esm/CommissioningController.js +18 -5
  49. package/dist/esm/CommissioningController.js.map +1 -1
  50. package/dist/esm/cluster/export.d.ts +1 -2
  51. package/dist/esm/cluster/export.d.ts.map +1 -1
  52. package/dist/esm/cluster/export.js +1 -2
  53. package/dist/esm/cluster/export.js.map +1 -1
  54. package/dist/esm/cluster/server/AttributeServer.d.ts +298 -0
  55. package/dist/esm/cluster/server/AttributeServer.d.ts.map +1 -0
  56. package/dist/esm/cluster/server/AttributeServer.js +636 -0
  57. package/dist/esm/cluster/server/AttributeServer.js.map +6 -0
  58. package/dist/esm/cluster/server/ClusterDatasource.d.ts +15 -0
  59. package/dist/esm/cluster/server/ClusterDatasource.d.ts.map +1 -0
  60. package/dist/esm/cluster/server/ClusterDatasource.js +6 -0
  61. package/dist/esm/cluster/server/ClusterDatasource.js.map +6 -0
  62. package/dist/esm/cluster/server/ClusterServer.d.ts +2 -2
  63. package/dist/esm/cluster/server/ClusterServer.d.ts.map +1 -1
  64. package/dist/esm/cluster/server/ClusterServer.js +3 -5
  65. package/dist/esm/cluster/server/ClusterServer.js.map +1 -1
  66. package/dist/esm/cluster/server/ClusterServerTypes.d.ts +4 -1
  67. package/dist/esm/cluster/server/ClusterServerTypes.d.ts.map +1 -1
  68. package/dist/esm/cluster/server/ClusterServerTypes.js.map +1 -1
  69. package/dist/esm/cluster/server/CommandServer.d.ts +33 -0
  70. package/dist/esm/cluster/server/CommandServer.d.ts.map +1 -0
  71. package/dist/esm/cluster/server/CommandServer.js +56 -0
  72. package/dist/esm/cluster/server/CommandServer.js.map +6 -0
  73. package/dist/esm/cluster/server/EventServer.d.ts +39 -0
  74. package/dist/esm/cluster/server/EventServer.d.ts.map +1 -0
  75. package/dist/esm/cluster/server/EventServer.js +141 -0
  76. package/dist/esm/cluster/server/EventServer.js.map +6 -0
  77. package/dist/esm/cluster/server/index.d.ts +11 -0
  78. package/dist/esm/cluster/server/index.d.ts.map +1 -0
  79. package/dist/esm/cluster/server/index.js +11 -0
  80. package/dist/esm/cluster/server/index.js.map +6 -0
  81. package/dist/esm/device/Endpoint.d.ts +9 -5
  82. package/dist/esm/device/Endpoint.d.ts.map +1 -1
  83. package/dist/esm/device/Endpoint.js +116 -31
  84. package/dist/esm/device/Endpoint.js.map +1 -1
  85. package/dist/esm/device/PairedNode.d.ts +5 -4
  86. package/dist/esm/device/PairedNode.d.ts.map +1 -1
  87. package/dist/esm/device/PairedNode.js +26 -4
  88. package/dist/esm/device/PairedNode.js.map +1 -1
  89. package/dist/esm/device/export.d.ts +0 -1
  90. package/dist/esm/device/export.d.ts.map +1 -1
  91. package/dist/esm/device/export.js +0 -4
  92. package/dist/esm/device/export.js.map +1 -1
  93. package/package.json +8 -8
  94. package/src/CommissioningController.ts +24 -6
  95. package/src/cluster/export.ts +1 -2
  96. package/src/cluster/server/AttributeServer.ts +867 -0
  97. package/src/cluster/server/ClusterDatasource.ts +16 -0
  98. package/src/cluster/server/ClusterServer.ts +6 -9
  99. package/src/cluster/server/ClusterServerTypes.ts +4 -11
  100. package/src/cluster/server/CommandServer.ts +87 -0
  101. package/src/cluster/server/EventServer.ts +198 -0
  102. package/src/cluster/server/index.ts +11 -0
  103. package/src/device/Endpoint.ts +135 -39
  104. package/src/device/PairedNode.ts +30 -7
  105. package/src/device/export.ts +0 -3
@@ -0,0 +1,867 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Matter.js Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { Endpoint } from "#device/Endpoint.js";
8
+ import { Diagnostic, ImplementationError, InternalError, Logger, MatterError, camelize, isDeepEqual } from "#general";
9
+ import { AccessLevel, AttributeModel, ClusterModel, DatatypeModel, FabricIndex, MatterModel } from "#model";
10
+ import { Fabric, Message, NoAssociatedFabricError, SecureSession, Session, assertSecureSession } from "#protocol";
11
+ import {
12
+ Attribute,
13
+ AttributeId,
14
+ Attributes,
15
+ BitSchema,
16
+ Cluster,
17
+ Commands,
18
+ Events,
19
+ StatusCode,
20
+ StatusResponseError,
21
+ TlvSchema,
22
+ TypeFromPartialBitSchema,
23
+ ValidationError,
24
+ } from "#types";
25
+ import { ClusterDatasource } from "./ClusterDatasource.js";
26
+
27
+ const logger = Logger.get("AttributeServer");
28
+
29
+ const FabricIndexName = camelize(FabricIndex.name);
30
+
31
+ /**
32
+ * Thrown when an operation cannot complete because fabric information is
33
+ * unavailable.
34
+ */
35
+ export class FabricScopeError extends MatterError {}
36
+
37
+ export type AnyAttributeServer<T = any> = AttributeServer<T> | FabricScopedAttributeServer<T> | FixedAttributeServer<T>;
38
+
39
+ type DelayedChangeData = {
40
+ oldValue: any;
41
+ newValue: any;
42
+ changed: boolean;
43
+ };
44
+
45
+ /**
46
+ * Factory function to create an attribute server.
47
+ */
48
+ export function createAttributeServer<
49
+ T,
50
+ F extends BitSchema,
51
+ SF extends TypeFromPartialBitSchema<F>,
52
+ A extends Attributes,
53
+ C extends Commands,
54
+ E extends Events,
55
+ >(
56
+ clusterDef: Cluster<F, SF, A, C, E>,
57
+ attributeDef: Attribute<T, F>,
58
+ attributeName: string,
59
+ initValue: T,
60
+ datasource: ClusterDatasource,
61
+ getter?: (session?: Session, endpoint?: Endpoint, isFabricFiltered?: boolean, message?: Message) => T,
62
+ setter?: (value: T, session?: Session, endpoint?: Endpoint, message?: Message) => boolean,
63
+ validator?: (value: T, session?: Session, endpoint?: Endpoint) => void,
64
+ ) {
65
+ const {
66
+ id,
67
+ schema,
68
+ writable,
69
+ fabricScoped,
70
+ fixed,
71
+ omitChanges,
72
+ timed,
73
+ default: defaultValue,
74
+ readAcl,
75
+ writeAcl,
76
+ } = attributeDef;
77
+
78
+ if (fixed) {
79
+ return new FixedAttributeServer(
80
+ id,
81
+ attributeName,
82
+ readAcl,
83
+ writeAcl,
84
+ schema,
85
+ writable,
86
+ false,
87
+ timed,
88
+ initValue,
89
+ defaultValue,
90
+ datasource,
91
+ getter,
92
+ );
93
+ }
94
+
95
+ if (fabricScoped) {
96
+ return new FabricScopedAttributeServer(
97
+ id,
98
+ attributeName,
99
+ readAcl,
100
+ writeAcl,
101
+ schema,
102
+ writable,
103
+ !omitChanges,
104
+ timed,
105
+ initValue,
106
+ defaultValue,
107
+ clusterDef,
108
+ datasource,
109
+ getter,
110
+ setter,
111
+ validator,
112
+ );
113
+ }
114
+
115
+ return new AttributeServer(
116
+ id,
117
+ attributeName,
118
+ readAcl,
119
+ writeAcl,
120
+ schema,
121
+ writable,
122
+ !omitChanges,
123
+ timed,
124
+ initValue,
125
+ defaultValue,
126
+ datasource,
127
+ getter,
128
+ setter,
129
+ validator,
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Base class for all attribute servers.
135
+ */
136
+ export abstract class BaseAttributeServer<T> {
137
+ /**
138
+ * The value is undefined when getter/setter are used. But we still handle the version number here.
139
+ */
140
+ protected value: T | undefined = undefined;
141
+ protected endpoint?: Endpoint;
142
+ readonly defaultValue: T;
143
+ #readAcl: AccessLevel | undefined;
144
+ #writeAcl: AccessLevel | undefined;
145
+
146
+ constructor(
147
+ readonly id: AttributeId,
148
+ readonly name: string,
149
+ readAcl: AccessLevel | undefined,
150
+ writeAcl: AccessLevel | undefined,
151
+ readonly schema: TlvSchema<T>,
152
+ readonly isWritable: boolean,
153
+ readonly isSubscribable: boolean,
154
+ readonly requiresTimedInteraction: boolean,
155
+ initValue: T,
156
+ defaultValue: T | undefined,
157
+ ) {
158
+ this.#readAcl = readAcl;
159
+ this.#writeAcl = writeAcl;
160
+ try {
161
+ this.validateWithSchema(initValue);
162
+ this.value = initValue;
163
+ } catch (error) {
164
+ logger.warn(
165
+ `Attribute value to initialize for ${name} has an invalid value ${Diagnostic.json(
166
+ initValue,
167
+ )}. Restore to default ${Diagnostic.json(defaultValue)}`,
168
+ );
169
+ if (defaultValue === undefined) {
170
+ throw new ImplementationError(`Attribute value to initialize for ${name} cannot be undefined.`);
171
+ }
172
+ this.validateWithSchema(defaultValue);
173
+ this.value = defaultValue;
174
+ }
175
+ this.defaultValue = this.value;
176
+ }
177
+
178
+ get hasFabricSensitiveData() {
179
+ return false;
180
+ }
181
+
182
+ validateWithSchema(value: T) {
183
+ try {
184
+ this.schema.validate(value);
185
+ } catch (e) {
186
+ ValidationError.accept(e);
187
+
188
+ // Handle potential error cases where a custom validator is used.
189
+ e.message = `Validation error for attribute "${this.name}"${e.fieldName !== undefined ? ` in field ${e.fieldName}` : ""}: ${e.message}`;
190
+ throw e;
191
+ }
192
+ }
193
+
194
+ assignToEndpoint(endpoint: Endpoint) {
195
+ this.endpoint = endpoint;
196
+ }
197
+
198
+ /**
199
+ * Initialize the value of the attribute, used when a persisted value is set initially or when values needs to be
200
+ * adjusted before the Device gets announced. Do not use this method to change values when the device is in use!
201
+ */
202
+ abstract init(value: T | undefined): void;
203
+
204
+ get writeAcl() {
205
+ return this.#writeAcl ?? AccessLevel.Operate; // ???
206
+ }
207
+
208
+ get readAcl() {
209
+ return this.#readAcl ?? AccessLevel.View; // ???
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Attribute server class that handled fixed attribute values that never change and is the base class for other
215
+ * Attribute server types.
216
+ */
217
+ export class FixedAttributeServer<T> extends BaseAttributeServer<T> {
218
+ readonly isFixed: boolean = true;
219
+ protected readonly getter: (
220
+ session?: Session,
221
+ endpoint?: Endpoint,
222
+ isFabricFiltered?: boolean,
223
+ message?: Message,
224
+ ) => T;
225
+
226
+ constructor(
227
+ id: AttributeId,
228
+ name: string,
229
+ readAcl: AccessLevel | undefined,
230
+ writeAcl: AccessLevel | undefined,
231
+ schema: TlvSchema<T>,
232
+ isWritable: boolean,
233
+ isSubscribable: boolean,
234
+ requiresTimedInteraction: boolean,
235
+ initValue: T,
236
+ defaultValue: T | undefined,
237
+ protected readonly datasource: ClusterDatasource,
238
+
239
+ /**
240
+ * Optional getter function to handle special requirements or the data are stored in different places.
241
+ *
242
+ * @param session the session that is requesting the value (if any)
243
+ * @param endpoint the endpoint the cluster server of this attribute is assigned to
244
+ * @param isFabricFiltered whether the read request is fabric scoped or not
245
+ * @param message the wire message that initiated the request (if any)
246
+ */
247
+ getter?: (session?: Session, endpoint?: Endpoint, isFabricFiltered?: boolean, message?: Message) => T,
248
+ ) {
249
+ super(
250
+ id,
251
+ name,
252
+ readAcl,
253
+ writeAcl,
254
+ schema,
255
+ isWritable,
256
+ isSubscribable,
257
+ requiresTimedInteraction,
258
+ initValue,
259
+ defaultValue,
260
+ ); // Fixed attributes do not change, so are not subscribable
261
+
262
+ if (getter === undefined) {
263
+ this.getter = () => {
264
+ if (this.value === undefined) {
265
+ // Should not happen
266
+ throw new InternalError(`Attribute value for attribute "${name}" is not initialized.`);
267
+ }
268
+ return this.value;
269
+ };
270
+ } else {
271
+ this.getter = getter;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Get the value of the attribute. This method is used by the Interaction model to read the value of the attribute
277
+ * and includes the ACL check. It should not be used locally in the code!
278
+ *
279
+ * If a getter is defined the value is determined by that getter method.
280
+ */
281
+ get(session: Session, isFabricFiltered: boolean, message?: Message): T {
282
+ // TODO: check ACL
283
+
284
+ return this.getter(session, this.endpoint, isFabricFiltered, message);
285
+ }
286
+
287
+ /**
288
+ * Get the value of the attribute including the version number. This method is used by the Interaction model to read
289
+ * the value of the attribute and includes the ACL check. It should not be used locally in the code!
290
+ *
291
+ * If a getter is defined the value is determined by that getter method. The version number is always 0 for fixed
292
+ * attributes.
293
+ */
294
+ getWithVersion(session: Session, isFabricFiltered: boolean, message?: Message) {
295
+ return { version: this.datasource.version, value: this.get(session, isFabricFiltered, message) };
296
+ }
297
+
298
+ /**
299
+ * Get the value of the attribute locally. This method should be used locally in the code and does not include the
300
+ * ACL check.
301
+ * If a getter is defined the value is determined by that getter method.
302
+ */
303
+ getLocal(): T {
304
+ return this.getter(undefined, this.endpoint);
305
+ }
306
+
307
+ /**
308
+ * Initialize the value of the attribute, used when a persisted value is set initially or when values needs to be
309
+ * adjusted before the Device gets announced. Do not use this method to change values when the device is in use!
310
+ * If a getter or setter is defined the value must be undefined The version number must also be undefined.
311
+ */
312
+ init(value: T | undefined) {
313
+ if (value === undefined) {
314
+ throw new InternalError(`Cannot initialize fixed attribute "${this.name}" with undefined value.`);
315
+ }
316
+ this.validateWithSchema(value);
317
+ this.value = value;
318
+ }
319
+
320
+ /**
321
+ * Add an internal listener that is called when the value of the attribute changes. The listener is called with the
322
+ * new value and the version number.
323
+ */
324
+ addValueChangeListener(_listener: (value: T, version: number) => void) {
325
+ /** Fixed attributes do not change. */
326
+ }
327
+
328
+ /**
329
+ * Remove an internal listener.
330
+ */
331
+ removeValueChangeListener(_listener: (value: T, version: number) => void) {
332
+ /** Fixed attributes do not change. */
333
+ }
334
+
335
+ /**
336
+ * Add an external listener that is called when the value of the attribute changes. The listener is called with the
337
+ * new value and the old value.
338
+ */
339
+ addValueSetListener(_listener: (newValue: T, oldValue: T) => void) {
340
+ /** Fixed attributes do not change. */
341
+ }
342
+
343
+ /**
344
+ * Add an external listener that is called when the value of the attribute changes. The listener is called with the
345
+ * new value and the old value. This method is a convenient alias for addValueSetListener.
346
+ */
347
+ subscribe(_listener: (newValue: T, oldValue: T) => void) {
348
+ /** Fixed attributes do not change. */
349
+ }
350
+
351
+ /**
352
+ * Remove an external listener.
353
+ */
354
+ removeValueSetListener(_listener: (newValue: T, oldValue: T) => void) {
355
+ /** Fixed attributes do not change. */
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Attribute server for normal attributes that can be read and written.
361
+ */
362
+ export class AttributeServer<T> extends FixedAttributeServer<T> {
363
+ override readonly isFixed = false;
364
+ protected readonly valueChangeListeners = new Array<(value: T, version: number) => void>();
365
+ protected readonly valueSetListeners = new Array<(newValue: T, oldValue: T) => void>();
366
+ protected readonly setter: (value: T, session?: Session, endpoint?: Endpoint, message?: Message) => boolean;
367
+ protected readonly validator: (value: T, session?: Session, endpoint?: Endpoint) => void;
368
+ protected delayedChangeData?: DelayedChangeData = undefined;
369
+
370
+ constructor(
371
+ id: AttributeId,
372
+ name: string,
373
+ readAcl: AccessLevel | undefined,
374
+ writeAcl: AccessLevel | undefined,
375
+ schema: TlvSchema<T>,
376
+ isWritable: boolean,
377
+ isSubscribable: boolean,
378
+ requiresTimedInteraction: boolean,
379
+ initValue: T,
380
+ defaultValue: T | undefined,
381
+ datasource: ClusterDatasource,
382
+ getter?: (session?: Session, endpoint?: Endpoint, isFabricFiltered?: boolean, message?: Message) => T,
383
+
384
+ /**
385
+ * Optional setter function to handle special requirements or the data are stored in different places. If a
386
+ * setter method is used for a writable attribute, the getter method must be implemented as well. The method
387
+ * needs to return if the stored value has changed or not.
388
+ *
389
+ * @param value the value to be set.
390
+ * @param session the session that is requesting the value (if any).
391
+ * @param endpoint the endpoint the cluster server of this attribute is assigned to.
392
+ * @returns true if the value has changed, false otherwise.
393
+ */
394
+ setter?: (value: T, session?: Session, endpoint?: Endpoint, message?: Message) => boolean,
395
+
396
+ /**
397
+ * Optional Validator function to handle special requirements for verification of stored data. The method should
398
+ * throw an error if the value is not valid. If a StatusResponseError is thrown this one is also returned to the
399
+ * client.
400
+ *
401
+ * If a setter is used then no validator should be used as the setter should handle the validation itself!
402
+ *
403
+ * @param value the value to be set.
404
+ * @param session the session that is requesting the value (if any).
405
+ * @param endpoint the endpoint the cluster server of this attribute is assigned to.
406
+ */
407
+ validator?: (value: T, session?: Session, endpoint?: Endpoint) => void,
408
+ ) {
409
+ if (
410
+ isWritable &&
411
+ (getter === undefined || setter === undefined) &&
412
+ !(getter === undefined && setter === undefined)
413
+ ) {
414
+ throw new ImplementationError(
415
+ `Getter and setter must be implemented together for writeable attribute "${name}".`,
416
+ );
417
+ }
418
+
419
+ super(
420
+ id,
421
+ name,
422
+ readAcl,
423
+ writeAcl,
424
+ schema,
425
+ isWritable,
426
+ isSubscribable,
427
+ requiresTimedInteraction,
428
+ initValue,
429
+ defaultValue,
430
+ datasource,
431
+ getter,
432
+ );
433
+
434
+ if (setter === undefined) {
435
+ this.setter = value => {
436
+ const oldValue = this.value;
437
+ this.value = value;
438
+ return !isDeepEqual(value, oldValue);
439
+ };
440
+ } else {
441
+ this.setter = setter;
442
+ }
443
+
444
+ this.validator = (value, session, endpoint) => {
445
+ this.validateWithSchema(value);
446
+ if (validator !== undefined) {
447
+ validator(value, session, endpoint);
448
+ }
449
+ };
450
+ }
451
+
452
+ /**
453
+ * Initialize the value of the attribute, used when a persisted value is set initially or when values needs to be
454
+ * adjusted before the Device gets announced. Do not use this method to change values when the device is in use!
455
+ */
456
+ override init(value: T | undefined) {
457
+ if (value === undefined) {
458
+ value = this.getter(undefined, this.endpoint);
459
+ }
460
+ if (value === undefined) {
461
+ throw new InternalError(`Cannot initialize attribute "${this.name}" with undefined value.`);
462
+ }
463
+ this.validator(value, undefined, this.endpoint);
464
+ this.value = value;
465
+ }
466
+
467
+ /**
468
+ * Set the value of the attribute. This method is used by the Interaction model to write the value of the attribute
469
+ * and includes the ACL check. It should not be used locally in the code!
470
+ *
471
+ * If a setter is defined this setter method is called to store the value.
472
+ *
473
+ * Listeners are called when the value changes (internal listeners) or in any case (external listeners).
474
+ */
475
+ set(value: T, session: Session, message?: Message, delayChangeEvents = false) {
476
+ if (!this.isWritable) {
477
+ throw new StatusResponseError(`Attribute "${this.name}" is not writable.`, StatusCode.UnsupportedWrite);
478
+ }
479
+
480
+ this.setRemote(value, session, message, delayChangeEvents);
481
+ }
482
+
483
+ /**
484
+ * Method that contains the logic to set a value "from remote" (e.g. from a client).
485
+ */
486
+ protected setRemote(value: T, session: Session, message?: Message, delayChangeEvents = false) {
487
+ this.processSet(value, session, message, delayChangeEvents);
488
+ this.value = value;
489
+ }
490
+
491
+ /**
492
+ * Set the value of the attribute locally. This method should be used locally in the code and does not include the
493
+ * ACL check.
494
+ *
495
+ * If a setter is defined this setter method is called to validate and store the value.
496
+ *
497
+ * Else if a validator is defined the value is validated before it is stored.
498
+ *
499
+ * Listeners are called when the value changes (internal listeners) or in any case (external listeners).
500
+ */
501
+ setLocal(value: T) {
502
+ this.processSet(value, undefined);
503
+ this.value = value;
504
+ }
505
+
506
+ /**
507
+ * Helper Method to process the set of a value in a generic way. This method is used internally.
508
+ */
509
+ protected processSet(value: T, session?: Session, message?: Message, delayChangeEvents = false) {
510
+ this.validator(value, session, this.endpoint);
511
+ const oldValue = this.getter(session, this.endpoint, undefined, message);
512
+ const valueChanged = this.setter(value, session, this.endpoint, message);
513
+ if (delayChangeEvents) {
514
+ this.delayedChangeData = {
515
+ oldValue: this.delayedChangeData?.oldValue ?? oldValue, // We keep the oldest value
516
+ newValue: value,
517
+ changed: !!this.delayedChangeData?.changed || valueChanged, // We combine the changed flag
518
+ };
519
+ logger.info(`Delay change for attribute "${this.name}" with value ${Diagnostic.json(value)}`);
520
+ } else {
521
+ this.handleVersionAndTriggerListeners(value, oldValue, valueChanged);
522
+ }
523
+ }
524
+
525
+ triggerDelayedChangeEvents() {
526
+ if (this.delayedChangeData !== undefined) {
527
+ const { oldValue, newValue, changed } = this.delayedChangeData;
528
+ this.delayedChangeData = undefined;
529
+ logger.info(`Trigger delayed change for attribute "${this.name}" with value ${Diagnostic.json(newValue)}`);
530
+ this.handleVersionAndTriggerListeners(newValue, oldValue, changed);
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Helper Method to handle needed version increases and trigger the relevant listeners. This method is used
536
+ * internally.
537
+ */
538
+ protected handleVersionAndTriggerListeners(value: T, oldValue: T | undefined, considerVersionChanged: boolean) {
539
+ if (considerVersionChanged) {
540
+ const version = this.datasource.increaseVersion();
541
+ this.valueChangeListeners.forEach(listener => listener(value, version));
542
+ }
543
+ if (oldValue !== undefined) {
544
+ this.valueSetListeners.forEach(listener => listener(value, oldValue));
545
+ }
546
+ }
547
+
548
+ /**
549
+ * When the value is handled by getter or setter methods and is changed by other processes this method can be used
550
+ * to notify the attribute server that the value has changed. This will increase the version number and trigger the
551
+ * listeners.
552
+ *
553
+ * ACL checks needs to be performed before calling this method.
554
+ */
555
+ updated(session: SecureSession) {
556
+ const oldValue = this.value ?? this.defaultValue;
557
+ try {
558
+ this.value = this.get(session, false);
559
+ } catch (e) {
560
+ NoAssociatedFabricError.accept(e);
561
+
562
+ // Handle potential error cases where the session does not have a fabric assigned.
563
+ if (this.value === undefined) {
564
+ this.value = this.defaultValue;
565
+ }
566
+ }
567
+ this.handleVersionAndTriggerListeners(this.value, oldValue, true);
568
+ }
569
+
570
+ /**
571
+ * When the value is handled by getter or setter methods and is changed by other processes and no session from the
572
+ * originating process is known this method can be used to notify the attribute server that the value has changed.
573
+ * This will increase the version number and trigger the listeners.
574
+ *
575
+ * ACL checks needs to be performed before calling this method.
576
+ */
577
+ updatedLocal() {
578
+ const oldValue = this.value ?? this.defaultValue;
579
+ this.value = this.getLocal();
580
+ this.handleVersionAndTriggerListeners(this.value, oldValue, true);
581
+ }
582
+
583
+ /**
584
+ * Add an internal listener that is called when the value of the attribute changes. The listener is called with the
585
+ * new value and the version number.
586
+ */
587
+ override addValueChangeListener(listener: (value: T, version: number) => void) {
588
+ this.valueChangeListeners.push(listener);
589
+ }
590
+
591
+ /**
592
+ * Remove an internal listener.
593
+ */
594
+ override removeValueChangeListener(listener: (value: T, version: number) => void) {
595
+ const entryIndex = this.valueChangeListeners.indexOf(listener);
596
+ if (entryIndex !== -1) {
597
+ this.valueChangeListeners.splice(entryIndex, 1);
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Add an external listener that is called when the value of the attribute changes. The listener is called with the
603
+ * new value and the old value.
604
+ */
605
+ override addValueSetListener(listener: (newValue: T, oldValue: T) => void) {
606
+ this.valueSetListeners.push(listener);
607
+ }
608
+
609
+ /**
610
+ * Add an external listener that is called when the value of the attribute changes. The listener is called with the
611
+ * new value and the old value. This method is a convenient alias for addValueSetListener.
612
+ */
613
+ override subscribe(listener: (newValue: T, oldValue: T) => void) {
614
+ this.addValueSetListener(listener);
615
+ }
616
+
617
+ /**
618
+ * Remove an external listener.
619
+ */
620
+ override removeValueSetListener(listener: (newValue: T, oldValue: T) => void) {
621
+ const entryIndex = this.valueSetListeners.indexOf(listener);
622
+ if (entryIndex !== -1) {
623
+ this.valueSetListeners.splice(entryIndex, 1);
624
+ }
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Attribute server which is getting and setting the value for a defined fabric. The values are automatically persisted
630
+ * on fabric level if no custom getter or setter is defined.
631
+ */
632
+ export class FabricScopedAttributeServer<T> extends AttributeServer<T> {
633
+ private readonly fabricSensitiveElementsToRemove = new Array<string>();
634
+
635
+ constructor(
636
+ id: AttributeId,
637
+ name: string,
638
+ readAcl: AccessLevel | undefined,
639
+ writeAcl: AccessLevel | undefined,
640
+ schema: TlvSchema<T>,
641
+ isWritable: boolean,
642
+ isSubscribable: boolean,
643
+ requiresTimedInteraction: boolean,
644
+ initValue: T,
645
+ defaultValue: T | undefined,
646
+ readonly cluster: Cluster<any, any, any, any, any>,
647
+ datasource: ClusterDatasource,
648
+ getter?: (session?: Session, endpoint?: Endpoint, isFabricFiltered?: boolean) => T,
649
+ setter?: (value: T, session?: Session, endpoint?: Endpoint, message?: Message) => boolean,
650
+ validator?: (value: T, session?: Session, endpoint?: Endpoint) => void,
651
+ ) {
652
+ if (
653
+ isWritable &&
654
+ (getter === undefined || setter === undefined) &&
655
+ !(getter === undefined && setter === undefined)
656
+ ) {
657
+ throw new ImplementationError(
658
+ `Getter and setter must be implemented together for writeable fabric scoped attribute "${name}".`,
659
+ );
660
+ }
661
+
662
+ if (getter === undefined) {
663
+ getter = (session, _endpoint, isFabricFiltered) => {
664
+ if (session === undefined)
665
+ throw new FabricScopeError(`Session is required for fabric scoped attribute ${name}`);
666
+
667
+ if (isFabricFiltered === true) {
668
+ assertSecureSession(session);
669
+ return this.getLocalForFabric(session.associatedFabric);
670
+ } else {
671
+ const values = new Array<any>();
672
+ for (const fabric of datasource.fabrics) {
673
+ const value = this.getLocalForFabric(fabric);
674
+ if (!Array.isArray(value)) {
675
+ throw new FabricScopeError(
676
+ `Fabric scoped attribute "${name}" can only be read for all fabrics if they are arrays.`,
677
+ );
678
+ }
679
+ values.push(...value);
680
+ }
681
+ return values as T;
682
+ }
683
+ };
684
+ }
685
+
686
+ if (setter === undefined) {
687
+ setter = () => {
688
+ throw new ImplementationError("Legacy FabricScopedAttributeServer data set is not supported anymore.");
689
+ };
690
+ }
691
+
692
+ super(
693
+ id,
694
+ name,
695
+ readAcl,
696
+ writeAcl,
697
+ schema,
698
+ isWritable,
699
+ isSubscribable,
700
+ requiresTimedInteraction,
701
+ initValue,
702
+ defaultValue,
703
+ datasource,
704
+ getter,
705
+ setter,
706
+ validator,
707
+ );
708
+
709
+ this.#determineSensitiveFieldsToRemove();
710
+ }
711
+
712
+ #determineSensitiveFieldsToRemove() {
713
+ const clusterFromModel = MatterModel.standard.get(ClusterModel, this.cluster.id);
714
+ if (clusterFromModel === undefined) {
715
+ logger.debug(`${this.cluster.name}: Cluster for Fabric scoped element not found in Model, ignore`);
716
+ return;
717
+ }
718
+ const attributeFromModel = clusterFromModel.get(AttributeModel, this.id);
719
+ if (attributeFromModel === undefined) {
720
+ logger.debug(
721
+ `${this.cluster.name}.${this.id}: Attribute for Fabric scoped element not found in Model, ignore`,
722
+ );
723
+ return;
724
+ }
725
+ if (!attributeFromModel.fabricScoped) {
726
+ logger.debug(`${this.cluster.name}.${this.id}: Attribute is not Fabric scoped in model, ignore`);
727
+ return;
728
+ }
729
+ if (attributeFromModel.children.length !== 1) {
730
+ logger.debug(`${this.cluster.name}.${this.id}: Attribute has not exactly one child, ignore`);
731
+ return;
732
+ }
733
+ const type = attributeFromModel.children[0].type;
734
+ if (type === undefined) {
735
+ logger.debug(`${this.cluster.name}.${this.id}: Attribute field has no type, ignore`);
736
+ return;
737
+ }
738
+ const dataType = clusterFromModel.get(DatatypeModel, type);
739
+ if (dataType === undefined) {
740
+ logger.debug(`${this.cluster.name}.${this.id}: DataType ${type} not found in model, ignore`);
741
+ return;
742
+ }
743
+ dataType.children
744
+ .filter(field => field.fabricSensitive)
745
+ .forEach(field => this.fabricSensitiveElementsToRemove.push(camelize(field.name)));
746
+ }
747
+
748
+ override get hasFabricSensitiveData() {
749
+ return this.fabricSensitiveElementsToRemove.length > 0;
750
+ }
751
+
752
+ /**
753
+ * Sanitize the value of the attribute by removing fabric sensitive fields that do not belong to the
754
+ * associated fabric
755
+ */
756
+ sanitizeFabricSensitiveFields(value: T, associatedFabric?: Fabric) {
757
+ if (this.fabricSensitiveElementsToRemove.length && Array.isArray(value)) {
758
+ // Get the associated Fabric Index or uses -1 when no Fabric is associated because this value will
759
+ // never be in the struct
760
+ const associatedFabricIndex = associatedFabric?.fabricIndex ?? -1;
761
+ return value.map(data => {
762
+ if (data[FabricIndexName] !== associatedFabricIndex) {
763
+ const result = { ...data };
764
+ this.fabricSensitiveElementsToRemove.forEach(fieldName => delete result[fieldName]);
765
+ return result;
766
+ }
767
+ return data;
768
+ });
769
+ }
770
+ return value;
771
+ }
772
+
773
+ /**
774
+ * Initialize the attribute with a value. Because the value is stored on fabric level this method only initializes
775
+ * the version number.
776
+ */
777
+ override init(value: T | undefined) {
778
+ if (value !== undefined) {
779
+ throw new InternalError(`Cannot initialize fabric scoped attribute "${this.name}" with a value.`);
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Fabric scoped enhancement of set to allow setting special fabricindex locally.
785
+ */
786
+ override set(value: T, session: Session, message: Message, delayChangeEvents = false, preserveFabricIndex = false) {
787
+ if (!this.isWritable) {
788
+ throw new StatusResponseError(`Attribute "${this.name}" is not writable.`, StatusCode.UnsupportedWrite);
789
+ }
790
+
791
+ this.setRemote(value, session, message, delayChangeEvents, preserveFabricIndex);
792
+ }
793
+
794
+ /**
795
+ * Method that contains the logic to set a value "from remote" (e.g. from a client). For Fabric scoped attributes
796
+ * we need to inject the fabric index into the value.
797
+ */
798
+ protected override setRemote(
799
+ value: T,
800
+ session: Session,
801
+ message: Message,
802
+ delayChangeEvents = false,
803
+ preserveFabricIndex = false,
804
+ ) {
805
+ // Inject fabric index into structures in general if undefined, if set it will be used
806
+ value = this.schema.injectField(
807
+ value,
808
+ <number>FabricIndex.id,
809
+ session.associatedFabric.fabricIndex,
810
+ () => !preserveFabricIndex, // No one should send any index and if we simply SHALL ignore it, but internally we might need it
811
+ );
812
+ logger.info(
813
+ `Set remote value for fabric scoped attribute "${this.name}" to ${Diagnostic.json(value)} (delayed=${delayChangeEvents})`,
814
+ );
815
+
816
+ super.setRemote(value, session, message, delayChangeEvents);
817
+ }
818
+
819
+ /**
820
+ * Set Local is not allowed for fabric scoped attributes. Use setLocalForFabric instead.
821
+ */
822
+ override setLocal(_value: T) {
823
+ throw new FabricScopeError(
824
+ `Fabric scoped attribute "${this.name}" can only be set locally by providing a Fabric. Use setLocalForFabric instead.`,
825
+ );
826
+ }
827
+
828
+ /**
829
+ * Set the value of the attribute locally for a fabric. This method should be used locally in the code and does not
830
+ * include the ACL check.
831
+ * If a setter is defined this method cannot be used!
832
+ * If a validator is defined the value is validated before it is stored.
833
+ * Listeners are called when the value changes (internal listeners) or in any case (external listeners).
834
+ */
835
+ setLocalForFabric(_value: T, _fabric: Fabric) {
836
+ throw new ImplementationError("Legacy FabricScopedAttributeServer data write is not supported anymore.");
837
+ }
838
+
839
+ /**
840
+ * When the value is handled by getter or setter methods and is changed by other processes and no session from the
841
+ * originating process is known this method can be used to notify the attribute server that the value has changed.
842
+ * This will increase the version number and trigger the listeners.
843
+ * ACL checks needs to be performed before calling this method.
844
+ */
845
+ updatedLocalForFabric(fabric: Fabric) {
846
+ const oldValue = this.value ?? this.defaultValue;
847
+ try {
848
+ this.value = this.getLocalForFabric(fabric);
849
+ } catch (e) {
850
+ FabricScopeError.accept(e);
851
+
852
+ if (this.value === undefined) {
853
+ this.value = this.defaultValue;
854
+ }
855
+ }
856
+ this.handleVersionAndTriggerListeners(this.value, oldValue, true);
857
+ }
858
+
859
+ /**
860
+ * Get the value of the attribute locally for a special Fabric. This method should be used locally in the code and
861
+ * does not include the ACL check.
862
+ * If a getter is defined this method returns an error and the value should be retrieved directly internally.
863
+ */
864
+ getLocalForFabric(_fabric: Fabric): T {
865
+ throw new ImplementationError("Legacy FabricScopedAttributeServer data read is not supported anymore.");
866
+ }
867
+ }