@matter/protocol 0.13.1-alpha.0-20250509-28e1567e1 → 0.13.1-alpha.0-20250511-74ef153aa

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 (118) hide show
  1. package/dist/cjs/action/protocols.d.ts +59 -2
  2. package/dist/cjs/action/protocols.d.ts.map +1 -1
  3. package/dist/cjs/action/request/Read.d.ts +2 -2
  4. package/dist/cjs/action/request/Read.d.ts.map +1 -1
  5. package/dist/cjs/action/request/Read.js +4 -4
  6. package/dist/cjs/action/request/Read.js.map +1 -1
  7. package/dist/cjs/action/response/ReadResult.d.ts +6 -2
  8. package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
  9. package/dist/cjs/action/server/AttributeResponse.d.ts +46 -17
  10. package/dist/cjs/action/server/AttributeResponse.d.ts.map +1 -1
  11. package/dist/cjs/action/server/AttributeResponse.js +128 -110
  12. package/dist/cjs/action/server/AttributeResponse.js.map +2 -2
  13. package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts +36 -0
  14. package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts.map +1 -0
  15. package/dist/cjs/action/server/AttributeSubscriptionResponse.js +86 -0
  16. package/dist/cjs/action/server/AttributeSubscriptionResponse.js.map +6 -0
  17. package/dist/cjs/action/server/DataResponse.d.ts +45 -0
  18. package/dist/cjs/action/server/DataResponse.d.ts.map +1 -0
  19. package/dist/cjs/action/server/DataResponse.js +69 -0
  20. package/dist/cjs/action/server/DataResponse.js.map +6 -0
  21. package/dist/cjs/action/server/EventResponse.d.ts +28 -0
  22. package/dist/cjs/action/server/EventResponse.d.ts.map +1 -0
  23. package/dist/cjs/action/server/EventResponse.js +318 -0
  24. package/dist/cjs/action/server/EventResponse.js.map +6 -0
  25. package/dist/cjs/action/server/ServerInteraction.d.ts.map +1 -1
  26. package/dist/cjs/action/server/ServerInteraction.js +15 -2
  27. package/dist/cjs/action/server/ServerInteraction.js.map +1 -1
  28. package/dist/cjs/action/server/index.d.ts +3 -0
  29. package/dist/cjs/action/server/index.d.ts.map +1 -1
  30. package/dist/cjs/action/server/index.js +3 -0
  31. package/dist/cjs/action/server/index.js.map +1 -1
  32. package/dist/cjs/events/OccurrenceManager.d.ts +20 -11
  33. package/dist/cjs/events/OccurrenceManager.d.ts.map +1 -1
  34. package/dist/cjs/events/OccurrenceManager.js +113 -74
  35. package/dist/cjs/events/OccurrenceManager.js.map +1 -1
  36. package/dist/cjs/interaction/InteractionMessenger.d.ts +14 -2
  37. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  38. package/dist/cjs/interaction/InteractionMessenger.js +87 -3
  39. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  40. package/dist/cjs/interaction/index.d.ts +0 -1
  41. package/dist/cjs/interaction/index.d.ts.map +1 -1
  42. package/dist/cjs/interaction/index.js +0 -1
  43. package/dist/cjs/interaction/index.js.map +1 -1
  44. package/dist/cjs/peer/ControllerCommissioningFlow.js +1 -1
  45. package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
  46. package/dist/cjs/protocol/MessageExchange.js +11 -1
  47. package/dist/cjs/protocol/MessageExchange.js.map +1 -1
  48. package/dist/esm/action/protocols.d.ts +59 -2
  49. package/dist/esm/action/protocols.d.ts.map +1 -1
  50. package/dist/esm/action/request/Read.d.ts +2 -2
  51. package/dist/esm/action/request/Read.d.ts.map +1 -1
  52. package/dist/esm/action/request/Read.js +4 -4
  53. package/dist/esm/action/request/Read.js.map +1 -1
  54. package/dist/esm/action/response/ReadResult.d.ts +6 -2
  55. package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
  56. package/dist/esm/action/server/AttributeResponse.d.ts +46 -17
  57. package/dist/esm/action/server/AttributeResponse.d.ts.map +1 -1
  58. package/dist/esm/action/server/AttributeResponse.js +129 -113
  59. package/dist/esm/action/server/AttributeResponse.js.map +1 -1
  60. package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts +36 -0
  61. package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts.map +1 -0
  62. package/dist/esm/action/server/AttributeSubscriptionResponse.js +66 -0
  63. package/dist/esm/action/server/AttributeSubscriptionResponse.js.map +6 -0
  64. package/dist/esm/action/server/DataResponse.d.ts +45 -0
  65. package/dist/esm/action/server/DataResponse.d.ts.map +1 -0
  66. package/dist/esm/action/server/DataResponse.js +49 -0
  67. package/dist/esm/action/server/DataResponse.js.map +6 -0
  68. package/dist/esm/action/server/EventResponse.d.ts +28 -0
  69. package/dist/esm/action/server/EventResponse.d.ts.map +1 -0
  70. package/dist/esm/action/server/EventResponse.js +305 -0
  71. package/dist/esm/action/server/EventResponse.js.map +6 -0
  72. package/dist/esm/action/server/ServerInteraction.d.ts.map +1 -1
  73. package/dist/esm/action/server/ServerInteraction.js +16 -3
  74. package/dist/esm/action/server/ServerInteraction.js.map +1 -1
  75. package/dist/esm/action/server/index.d.ts +3 -0
  76. package/dist/esm/action/server/index.d.ts.map +1 -1
  77. package/dist/esm/action/server/index.js +3 -0
  78. package/dist/esm/action/server/index.js.map +1 -1
  79. package/dist/esm/events/OccurrenceManager.d.ts +20 -11
  80. package/dist/esm/events/OccurrenceManager.d.ts.map +1 -1
  81. package/dist/esm/events/OccurrenceManager.js +117 -80
  82. package/dist/esm/events/OccurrenceManager.js.map +1 -1
  83. package/dist/esm/interaction/InteractionMessenger.d.ts +14 -2
  84. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  85. package/dist/esm/interaction/InteractionMessenger.js +87 -3
  86. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  87. package/dist/esm/interaction/index.d.ts +0 -1
  88. package/dist/esm/interaction/index.d.ts.map +1 -1
  89. package/dist/esm/interaction/index.js +0 -1
  90. package/dist/esm/interaction/index.js.map +1 -1
  91. package/dist/esm/peer/ControllerCommissioningFlow.js +1 -1
  92. package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
  93. package/dist/esm/protocol/MessageExchange.js +11 -1
  94. package/dist/esm/protocol/MessageExchange.js.map +1 -1
  95. package/package.json +6 -6
  96. package/src/action/protocols.ts +68 -2
  97. package/src/action/request/Read.ts +2 -2
  98. package/src/action/response/ReadResult.ts +8 -1
  99. package/src/action/server/AttributeResponse.ts +145 -118
  100. package/src/action/server/AttributeSubscriptionResponse.ts +90 -0
  101. package/src/action/server/DataResponse.ts +70 -0
  102. package/src/action/server/EventResponse.ts +381 -0
  103. package/src/action/server/ServerInteraction.ts +18 -4
  104. package/src/action/server/index.ts +3 -0
  105. package/src/events/OccurrenceManager.ts +126 -100
  106. package/src/interaction/InteractionMessenger.ts +93 -8
  107. package/src/interaction/index.ts +0 -1
  108. package/src/peer/ControllerCommissioningFlow.ts +1 -1
  109. package/src/protocol/MessageExchange.ts +13 -1
  110. package/dist/cjs/interaction/ServerSubscription.d.ts +0 -116
  111. package/dist/cjs/interaction/ServerSubscription.d.ts.map +0 -1
  112. package/dist/cjs/interaction/ServerSubscription.js +0 -778
  113. package/dist/cjs/interaction/ServerSubscription.js.map +0 -6
  114. package/dist/esm/interaction/ServerSubscription.d.ts +0 -116
  115. package/dist/esm/interaction/ServerSubscription.d.ts.map +0 -1
  116. package/dist/esm/interaction/ServerSubscription.js +0 -778
  117. package/dist/esm/interaction/ServerSubscription.js.map +0 -6
  118. package/src/interaction/ServerSubscription.ts +0 -1038
@@ -8,45 +8,35 @@ import { AttributeTypeProtocol, ClusterProtocol, EndpointProtocol, NodeProtocol
8
8
  import { Read } from "#action/request/Read.js";
9
9
  import { ReadResult } from "#action/response/ReadResult.js";
10
10
  import { AccessControl } from "#action/server/AccessControl.js";
11
+ import { DataResponse, FallbackLimits, WildcardPathFlagsCodec } from "#action/server/DataResponse.js";
11
12
  import { Val } from "#action/Val.js";
12
- import { InternalError } from "#general";
13
- import { AccessLevel, AttributeModel, ElementTag } from "#model";
13
+ import { Diagnostic, InternalError, Logger } from "#general";
14
+ import { AttributeModel, DataModelPath, ElementTag } from "#model";
14
15
  import {
15
16
  AttributePath,
16
- BitmapSchema,
17
17
  ClusterId,
18
18
  EndpointNumber,
19
19
  GlobalAttributes,
20
20
  NodeId,
21
21
  Status,
22
22
  StatusResponseError,
23
- WildcardPathFlagsBitmap,
23
+ TlvSchema,
24
24
  } from "#types";
25
- import { DataModelPath } from "@matter/model";
26
- import { TlvSchema } from "@matter/types";
25
+ import { StatusCode } from "@matter/types";
26
+
27
+ const logger = Logger.get("AttributeResponse");
27
28
 
28
29
  export const GlobalAttrIds = new Set(Object.values(GlobalAttributes({})).map(attr => attr.id));
29
- export const WildcardPathFlagsCodec = BitmapSchema(WildcardPathFlagsBitmap);
30
- export const FallbackLimits: AccessControl.Limits = {
31
- fabricScoped: false,
32
- fabricSensitive: false,
33
- readable: true,
34
- readLevel: AccessLevel.View,
35
- timed: false,
36
- writable: true,
37
- writeLevel: AccessLevel.Administer,
38
- };
39
30
 
40
31
  /**
41
32
  * Implements read of attribute data for Matter "read" and "subscribe" interactions.
42
33
  *
43
34
  * TODO - profile; ensure nested functions are properly JITed and/or inlined
44
35
  */
45
- export class AttributeResponse<SessionT extends AccessControl.Session = AccessControl.Session> {
46
- // Configuration
47
- #session: SessionT;
48
- #node: NodeProtocol;
49
- #versions?: Record<EndpointNumber, Record<ClusterId, number>> | undefined;
36
+ export class AttributeResponse<
37
+ SessionT extends AccessControl.Session = AccessControl.Session,
38
+ > extends DataResponse<SessionT> {
39
+ #versions?: Record<EndpointNumber, Record<ClusterId, number>>;
50
40
 
51
41
  // Each input AttributePathIB that does not have an error installs a producer. Producers run after validation and
52
42
  // generate actual attribute data
@@ -63,14 +53,17 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
63
53
  #currentState?: Val.ProtocolStruct;
64
54
  #wildcardPathFlags = 0;
65
55
 
66
- // The node ID may be expensive to retrieve and is invariant so we cache it here
67
- #cachedNodeId?: NodeId;
56
+ // Count how many attribute status (on error) and attribute values (on success) we have emitted
57
+ #statusCount = 0;
58
+ #valueCount = 0;
59
+ #filteredCount = 0;
68
60
 
69
- constructor(node: NodeProtocol, session: SessionT, { dataVersionFilters, attributeRequests }: Read.Attributes) {
70
- this.#node = node;
71
- this.#session = session;
61
+ constructor(node: NodeProtocol, session: SessionT) {
62
+ super(node, session);
63
+ }
72
64
 
73
- const nodeId = session.fabric === undefined ? NodeId.UNSPECIFIED_NODE_ID : this.#nodeId;
65
+ *process({ dataVersionFilters, attributeRequests }: Read.Attributes): Generator<ReadResult.Chunk, void, void> {
66
+ const nodeId = this.session.fabric === undefined ? NodeId.UNSPECIFIED_NODE_ID : this.nodeId;
74
67
 
75
68
  // Index versions
76
69
  if (dataVersionFilters?.length) {
@@ -93,17 +86,12 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
93
86
  // Register paths
94
87
  for (const path of attributeRequests) {
95
88
  if (path.endpointId === undefined || path.clusterId === undefined || path.attributeId === undefined) {
96
- this.#addWildcard(path);
89
+ this.addWildcard(path);
97
90
  } else {
98
- this.#addConcrete(path as ReadResult.ConcreteAttributePath);
91
+ this.addConcrete(path as ReadResult.ConcreteAttributePath);
99
92
  }
100
93
  }
101
- }
102
94
 
103
- /**
104
- * Emits chunks produced by paths added via {@link #addWildcard} and {@link #addConcrete}.
105
- */
106
- *[Symbol.iterator](): Generator<ReadResult.Chunk, void, void> {
107
95
  if (this.#dataProducers) {
108
96
  for (const producer of this.#dataProducers) {
109
97
  yield* producer.apply(this);
@@ -117,18 +105,36 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
117
105
  }
118
106
  }
119
107
 
108
+ /** Guarded accessor for this.#currentEndpoint. This should never be undefined */
109
+ get #guardedCurrentEndpoint() {
110
+ if (this.#currentEndpoint === undefined) {
111
+ throw new InternalError("currentEndpoint is not set. Should never happen");
112
+ }
113
+ return this.#currentEndpoint;
114
+ }
115
+
116
+ /** Guarded accessor for this.#currentCluster. This should never be undefined */
117
+ get #guardedCurrentCluster(): ClusterProtocol {
118
+ if (this.#currentCluster === undefined) {
119
+ throw new InternalError("currentCluster is not set. Should never happen");
120
+ }
121
+ return this.#currentCluster;
122
+ }
123
+
124
+ get counts() {
125
+ return {
126
+ status: this.#statusCount,
127
+ value: this.#valueCount,
128
+ existent: this.#valueCount + this.#filteredCount,
129
+ };
130
+ }
131
+
120
132
  /**
121
133
  * Validate a wildcard path and update internal state.
122
134
  */
123
- #addWildcard(path: AttributePath) {
135
+ protected addWildcard(path: AttributePath) {
124
136
  const { nodeId, endpointId, clusterId, attributeId, wildcardPathFlags } = path;
125
137
 
126
- if (nodeId !== undefined && nodeId !== this.#nodeId) {
127
- return;
128
- }
129
-
130
- const wpf = wildcardPathFlags ? WildcardPathFlagsCodec.encode(wildcardPathFlags) : 0;
131
-
132
138
  if (clusterId === undefined && attributeId !== undefined && !GlobalAttrIds.has(attributeId)) {
133
139
  throw new StatusResponseError(
134
140
  `Illegal read of wildcard cluster with non-global attribute #${attributeId}`,
@@ -136,21 +142,27 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
136
142
  );
137
143
  }
138
144
 
145
+ if (nodeId !== undefined && nodeId !== this.nodeId) {
146
+ return;
147
+ }
148
+
149
+ const wpf = wildcardPathFlags ? WildcardPathFlagsCodec.encode(wildcardPathFlags) : 0;
150
+
139
151
  if (endpointId === undefined) {
140
152
  this.#addProducer(function* (this: AttributeResponse) {
141
153
  this.#wildcardPathFlags = wpf;
142
- for (const endpoint of this.#node) {
143
- yield* this.#readEndpointForWildcard(endpoint, path);
154
+ for (const endpoint of this.node) {
155
+ yield* this.readEndpointForWildcard(endpoint, path);
144
156
  }
145
157
  });
146
158
  return;
147
159
  }
148
160
 
149
- const endpoint = this.#node[endpointId];
161
+ const endpoint = this.node[endpointId];
150
162
  if (endpoint) {
151
163
  this.#addProducer(function (this: AttributeResponse) {
152
164
  this.#wildcardPathFlags = wpf;
153
- return this.#readEndpointForWildcard(endpoint, path);
165
+ return this.readEndpointForWildcard(endpoint, path);
154
166
  });
155
167
  }
156
168
  }
@@ -158,14 +170,16 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
158
170
  /**
159
171
  * Validate a concrete path and update internal state.
160
172
  */
161
- #addConcrete(path: ReadResult.ConcreteAttributePath) {
173
+ protected addConcrete(path: ReadResult.ConcreteAttributePath) {
162
174
  const { nodeId, endpointId, clusterId, attributeId } = path;
163
- if (nodeId !== undefined && this.#nodeId !== nodeId) {
164
- this.#addStatus(path, Status.UnsupportedNode);
175
+
176
+ if (nodeId !== undefined && this.nodeId !== nodeId) {
177
+ this.addStatus(path, Status.UnsupportedNode);
178
+ return;
165
179
  }
166
180
 
167
181
  // Resolve path elements
168
- const endpoint = this.#node[endpointId];
182
+ const endpoint = this.node[endpointId];
169
183
  const cluster = endpoint?.[clusterId];
170
184
  const attribute = cluster?.type.attributes[attributeId];
171
185
  let limits;
@@ -173,7 +187,7 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
173
187
  // We still need to authorize the user for access even though this path doesn't resolve. Spec is not
174
188
  // explicit on what privilege level we should require as normally that information comes from the resolved
175
189
  // attribute. So attempt to resolve via the active model
176
- const modelAttr = this.#node.matter
190
+ const modelAttr = this.node.matter
177
191
  .member(path.clusterId, [ElementTag.Cluster])
178
192
  ?.member(path.attributeId, [ElementTag.Attribute]);
179
193
 
@@ -197,44 +211,45 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
197
211
  endpoint: endpointId,
198
212
  cluster: clusterId,
199
213
  }),
200
- owningFabric: this.#session.fabric,
214
+ owningFabric: this.session.fabric,
201
215
  };
202
- const permission = this.#session.authorityAt(limits.readLevel, location);
216
+ const permission = this.session.authorityAt(limits.readLevel, location);
203
217
  switch (permission) {
204
218
  case AccessControl.Authority.Granted:
205
219
  break;
206
220
 
207
221
  case AccessControl.Authority.Unauthorized:
208
- this.#addStatus(path, Status.UnsupportedAccess);
222
+ this.addStatus(path, Status.UnsupportedAccess);
209
223
  return;
210
224
 
211
225
  case AccessControl.Authority.Restricted:
212
- this.#addStatus(path, Status.AccessRestricted);
226
+ this.addStatus(path, Status.AccessRestricted);
213
227
  return;
214
228
 
215
229
  default:
216
230
  throw new InternalError(`Unsupported authorization state ${permission}`);
217
231
  }
218
232
  if (endpoint === undefined) {
219
- this.#addStatus(path, Status.UnsupportedEndpoint);
233
+ this.addStatus(path, Status.UnsupportedEndpoint);
220
234
  return;
221
235
  }
222
236
  if (cluster === undefined) {
223
- this.#addStatus(path, Status.UnsupportedCluster);
237
+ this.addStatus(path, Status.UnsupportedCluster);
224
238
  return;
225
239
  }
226
- if (attribute === undefined) {
227
- this.#addStatus(path, Status.UnsupportedAttribute);
240
+ if (attribute === undefined || !cluster.type.attributes[attribute.id]) {
241
+ this.addStatus(path, Status.UnsupportedAttribute);
228
242
  return;
229
243
  }
230
244
  if (!limits.readable) {
231
- this.#addStatus(path, Status.UnsupportedRead);
245
+ this.addStatus(path, Status.UnsupportedRead);
232
246
  return;
233
247
  }
234
248
 
235
249
  // Skip if version is unchanged
236
250
  const skipVersion = this.#versions?.[path.endpointId]?.[path.clusterId];
237
251
  if (skipVersion !== undefined && skipVersion === cluster.version) {
252
+ this.#filteredCount++;
238
253
  return;
239
254
  }
240
255
 
@@ -248,21 +263,21 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
248
263
  }
249
264
  this.#currentEndpoint = endpoint;
250
265
  this.#currentCluster = cluster;
251
- this.#currentState = cluster.open(this.#session);
266
+ this.#currentState = cluster.open(this.session);
252
267
  } else if (this.#currentCluster !== cluster) {
253
268
  this.#currentCluster = cluster;
254
- this.#currentState = cluster.open(this.#session);
269
+ this.#currentState = cluster.open(this.session);
255
270
  } else if (this.#currentState === undefined) {
256
- this.#currentState = cluster.open(this.#session);
271
+ this.#currentState = cluster.open(this.session);
257
272
  }
258
273
 
259
- // Perform actual read of one attribute
260
- this.#addValue(
261
- path,
262
- this.#currentState[attributeId],
263
- cluster.version,
264
- this.#currentCluster.type.attributes[attributeId]!.tlv,
274
+ const value = this.#currentState[attributeId];
275
+ const version = cluster.version;
276
+ logger.debug(
277
+ () => `Reading attribute ${this.node.inspectPath(path)}=${Diagnostic.json(value)} (version=${version})`,
265
278
  );
279
+ // Perform actual read of one attribute
280
+ this.#addValue(path, value, version, this.#currentCluster.type.attributes[attributeId]!.tlv);
266
281
  });
267
282
  }
268
283
 
@@ -274,9 +289,9 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
274
289
  *
275
290
  * {@link this.#wildcardPathFlags} to numeric bitmap must be set prior to invocation.
276
291
  *
277
- * TODO - skip endpoints for which subject is unauthorized
292
+ * TODO - skip endpoints for which subject is unauthorized as optimization
278
293
  */
279
- *#readEndpointForWildcard(endpoint: EndpointProtocol, path: AttributePath) {
294
+ protected *readEndpointForWildcard(endpoint: EndpointProtocol, path: AttributePath) {
280
295
  if (endpoint.wildcardPathFlags & this.#wildcardPathFlags) {
281
296
  return;
282
297
  }
@@ -293,12 +308,12 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
293
308
  const { clusterId } = path;
294
309
  if (clusterId === undefined) {
295
310
  for (const cluster of endpoint) {
296
- this.#readClusterForWildcard(cluster, path);
311
+ this.readClusterForWildcard(cluster, path);
297
312
  }
298
313
  } else {
299
314
  const cluster = endpoint[clusterId];
300
315
  if (cluster !== undefined) {
301
- this.#readClusterForWildcard(cluster, path);
316
+ this.readClusterForWildcard(cluster, path);
302
317
  }
303
318
  }
304
319
  }
@@ -310,7 +325,7 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
310
325
  *
311
326
  * TODO - skip clusters for which subject is unauthorized
312
327
  */
313
- #readClusterForWildcard(cluster: ClusterProtocol, path: AttributePath) {
328
+ protected readClusterForWildcard(cluster: ClusterProtocol, path: AttributePath) {
314
329
  if (cluster.type.wildcardPathFlags & this.#wildcardPathFlags) {
315
330
  return;
316
331
  }
@@ -320,20 +335,30 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
320
335
  this.#currentState = undefined;
321
336
  }
322
337
 
323
- const skipVersion = this.#versions?.[this.#currentEndpoint!.id]?.[cluster.type.id];
324
- if (skipVersion !== undefined && skipVersion === cluster.version) {
325
- return;
326
- }
327
-
328
338
  const { attributeId } = path;
339
+ const skipVersion = this.#versions?.[this.#guardedCurrentEndpoint.id]?.[cluster.type.id];
340
+ const filteredByVersion = skipVersion !== undefined && skipVersion === cluster.version;
341
+
329
342
  if (attributeId === undefined) {
343
+ if (filteredByVersion) {
344
+ for (const attribute of cluster.type.attributes) {
345
+ if (attribute.limits.readable) {
346
+ this.#filteredCount++;
347
+ }
348
+ }
349
+ return;
350
+ }
330
351
  for (const attribute of cluster.type.attributes) {
331
- this.#readAttributeForWildcard(attribute, path);
352
+ this.readAttributeForWildcard(attribute, path);
332
353
  }
333
354
  } else {
355
+ if (filteredByVersion) {
356
+ this.#filteredCount++;
357
+ return;
358
+ }
334
359
  const attribute = cluster.type.attributes[attributeId];
335
360
  if (attribute !== undefined) {
336
- this.#readAttributeForWildcard(attribute, path);
361
+ this.readAttributeForWildcard(attribute, path);
337
362
  }
338
363
  }
339
364
  }
@@ -343,37 +368,44 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
343
368
  *
344
369
  * Depends on state initialized by {@link #readClusterForWildcard}.
345
370
  */
346
- #readAttributeForWildcard(attribute: AttributeTypeProtocol, path: AttributePath) {
371
+ protected readAttributeForWildcard(attribute: AttributeTypeProtocol, path: AttributePath) {
372
+ if (!this.#guardedCurrentCluster.type.attributes[attribute.id]) {
373
+ return;
374
+ }
375
+
347
376
  if (attribute.wildcardPathFlags & this.#wildcardPathFlags) {
348
377
  return;
349
378
  }
350
379
 
351
380
  if (
352
381
  !attribute.limits.readable ||
353
- this.#session.authorityAt(attribute.limits.readLevel, this.#currentCluster!.location) !==
382
+ this.session.authorityAt(attribute.limits.readLevel, this.#guardedCurrentCluster.location) !==
354
383
  AccessControl.Authority.Granted
355
384
  ) {
356
385
  return;
357
386
  }
358
387
 
359
388
  if (this.#currentState === undefined) {
360
- this.#currentState = this.#currentCluster!.open(this.#session);
389
+ this.#currentState = this.#guardedCurrentCluster.open(this.session);
361
390
  }
362
391
  const value = this.#currentState[attribute.id];
363
- if (value !== undefined) {
364
- // Only if we have a state value set
365
- this.#addValue(
366
- {
367
- ...path,
368
- endpointId: this.#currentEndpoint?.id as EndpointNumber,
369
- clusterId: this.#currentCluster?.type.id as ClusterId,
370
- attributeId: attribute.id,
371
- },
372
- this.#currentState[attribute.id],
373
- this.#currentCluster!.version,
374
- attribute.tlv,
375
- );
392
+ if (value === undefined) {
393
+ // Should normally never happen
394
+ logger.warn(`Attribute ${this.node.inspectPath(path)} defined and enabled but has no value.`);
395
+ return;
376
396
  }
397
+
398
+ this.#addValue(
399
+ {
400
+ ...path,
401
+ endpointId: this.#guardedCurrentEndpoint.id,
402
+ clusterId: this.#guardedCurrentCluster.type.id,
403
+ attributeId: attribute.id,
404
+ },
405
+ this.#currentState[attribute.id],
406
+ this.#guardedCurrentCluster.version,
407
+ attribute.tlv,
408
+ );
377
409
  }
378
410
 
379
411
  /**
@@ -387,21 +419,30 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
387
419
  }
388
420
  }
389
421
 
422
+ #addReportData(report: ReadResult.Report) {
423
+ if (this.#chunk) {
424
+ this.#chunk.push(report);
425
+ } else {
426
+ this.#chunk = [report];
427
+ }
428
+ }
429
+
390
430
  /**
391
431
  * Add a status value.
392
432
  */
393
- #addStatus(path: ReadResult.ConcreteAttributePath, status: Status) {
433
+ protected addStatus(path: ReadResult.ConcreteAttributePath, status: Status) {
434
+ logger.debug(
435
+ () => `Error reading attribute ${this.node.inspectPath(path)}: Status=${StatusCode[status]}(${status})`,
436
+ );
437
+
394
438
  const report: ReadResult.GlobalAttributeStatus = {
395
439
  kind: "attr-status",
396
440
  path,
397
441
  status,
398
442
  };
399
443
 
400
- if (this.#chunk) {
401
- this.#chunk.push(report);
402
- } else {
403
- this.#chunk = [report];
404
- }
444
+ this.#addReportData(report);
445
+ this.#statusCount++;
405
446
  }
406
447
 
407
448
  /**
@@ -416,21 +457,7 @@ export class AttributeResponse<SessionT extends AccessControl.Session = AccessCo
416
457
  tlv,
417
458
  };
418
459
 
419
- if (this.#chunk) {
420
- this.#chunk.push(report);
421
- } else {
422
- this.#chunk = [report];
423
- }
424
- }
425
-
426
- /**
427
- * The node ID used to filter paths with node ID specified. Unsure if this is ever actually used.
428
- */
429
- get #nodeId() {
430
- if (this.#cachedNodeId === undefined) {
431
- this.#cachedNodeId =
432
- (this.#session.fabric && this.#node.nodeIdFor(this.#session.fabric)) ?? NodeId.UNSPECIFIED_NODE_ID;
433
- }
434
- return this.#cachedNodeId;
460
+ this.#addReportData(report);
461
+ this.#valueCount++;
435
462
  }
436
463
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { AttributeTypeProtocol, ClusterProtocol, EndpointProtocol, NodeProtocol } from "#action/protocols.js";
7
+ import { ReadResult } from "#action/response/ReadResult.js";
8
+ import { InternalError } from "#general";
9
+ import { AttributeId, AttributePath, ClusterId, EndpointNumber } from "#types";
10
+ import { AccessControl } from "./AccessControl.js";
11
+ import { AttributeResponse } from "./AttributeResponse.js";
12
+
13
+ type ClusterFilter = {
14
+ [clusterId: ClusterId]: Set<AttributeId>;
15
+ };
16
+ export type AttributeResponseFilter = {
17
+ [endpointId: EndpointNumber]: ClusterFilter;
18
+ };
19
+
20
+ /**
21
+ * AttributeSubscriptionResponse is a specialized version of AttributeResponse that processes a read/subscribe request
22
+ * with a filter applied to the attributes. Only the attributes that match the filter will be processed.
23
+ */
24
+ export class AttributeSubscriptionResponse<
25
+ SessionT extends AccessControl.Session = AccessControl.Session,
26
+ > extends AttributeResponse<SessionT> {
27
+ #filter: AttributeResponseFilter;
28
+ #currentEndpointFilter?: ClusterFilter;
29
+ #currentClusterFilter?: Set<number>;
30
+
31
+ constructor(node: NodeProtocol, session: SessionT, filter: AttributeResponseFilter) {
32
+ super(node, session);
33
+ this.#filter = filter;
34
+ }
35
+
36
+ get filter() {
37
+ return this.#filter;
38
+ }
39
+
40
+ /** Guarded accessor for this.#currentEndpointFilter. This should never be undefined */
41
+ protected get currentEndpointFilter() {
42
+ if (!this.#currentEndpointFilter) {
43
+ throw new InternalError("currentEndpointFilter is not set. Should never happen");
44
+ }
45
+ return this.#currentEndpointFilter;
46
+ }
47
+
48
+ /** Guarded accessor for this.#currentCLusterFilter. This should never be undefined */
49
+ protected get currentClusterFilter() {
50
+ if (!this.#currentClusterFilter) {
51
+ throw new InternalError("currentClusterFilter is not set. Should never happen");
52
+ }
53
+ return this.#currentClusterFilter;
54
+ }
55
+
56
+ protected override addConcrete(path: ReadResult.ConcreteAttributePath) {
57
+ const { endpointId, clusterId, attributeId } = path;
58
+ if (this.#filter[endpointId]?.[clusterId]?.has(attributeId) === undefined) {
59
+ return;
60
+ }
61
+ super.addConcrete(path);
62
+ }
63
+
64
+ protected override *readEndpointForWildcard(endpoint: EndpointProtocol, path: AttributePath) {
65
+ this.#currentEndpointFilter = this.#filter[endpoint.id];
66
+ if (this.#currentEndpointFilter === undefined) {
67
+ return;
68
+ }
69
+ yield* super.readEndpointForWildcard(endpoint, path);
70
+ }
71
+
72
+ protected override readClusterForWildcard(cluster: ClusterProtocol, path: AttributePath) {
73
+ this.#currentClusterFilter = this.currentEndpointFilter[cluster.type.id];
74
+ if (this.#currentClusterFilter === undefined) {
75
+ return;
76
+ }
77
+ super.readClusterForWildcard(cluster, path);
78
+ }
79
+
80
+ protected override readAttributeForWildcard(attribute: AttributeTypeProtocol, path: AttributePath) {
81
+ if (!this.currentClusterFilter.has(attribute.id)) {
82
+ return;
83
+ }
84
+ super.readAttributeForWildcard(attribute, path);
85
+ }
86
+
87
+ protected override addStatus() {
88
+ // For Filtered responses we suppress all status reports
89
+ }
90
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { NodeProtocol } from "#action/protocols.js";
7
+ import { AccessControl } from "#action/server/AccessControl.js";
8
+ import { AccessLevel } from "#model";
9
+ import { BitmapSchema, NodeId, WildcardPathFlagsBitmap } from "#types";
10
+
11
+ export const WildcardPathFlagsCodec = BitmapSchema(WildcardPathFlagsBitmap);
12
+ export const FallbackLimits: AccessControl.Limits = {
13
+ fabricScoped: false,
14
+ fabricSensitive: false,
15
+ readable: true,
16
+ readLevel: AccessLevel.View,
17
+ timed: false,
18
+ writable: true,
19
+ writeLevel: AccessLevel.Administer,
20
+ };
21
+
22
+ export abstract class DataResponse<SessionT extends AccessControl.Session = AccessControl.Session> {
23
+ // Configuration
24
+ #session: SessionT;
25
+ #node: NodeProtocol;
26
+
27
+ // The node ID may be expensive to retrieve and is invariant so we cache it here
28
+ #cachedNodeId?: NodeId;
29
+
30
+ constructor(node: NodeProtocol, session: SessionT) {
31
+ this.#node = node;
32
+ this.#session = session;
33
+ }
34
+
35
+ protected get node() {
36
+ return this.#node;
37
+ }
38
+
39
+ protected get session() {
40
+ return this.#session;
41
+ }
42
+
43
+ /**
44
+ * The node ID used to filter paths with node ID specified. Unsure if this is ever actually used.
45
+ */
46
+ protected get nodeId() {
47
+ if (this.#cachedNodeId === undefined) {
48
+ this.#cachedNodeId =
49
+ (this.#session.fabric && this.#node.nodeIdFor(this.#session.fabric)) ?? NodeId.UNSPECIFIED_NODE_ID;
50
+ }
51
+ return this.#cachedNodeId;
52
+ }
53
+
54
+ abstract counts: {
55
+ /**
56
+ * Number of existent entries that were processed. Not all must have had data to send.
57
+ */
58
+ existent: number;
59
+
60
+ /**
61
+ * Number of status responses (aka errors we have sent)
62
+ */
63
+ status: number;
64
+
65
+ /**
66
+ * Number of value responses (aka success we have sent)
67
+ */
68
+ value: number;
69
+ };
70
+ }