@matter/protocol 0.13.1-alpha.0-20250509-28e1567e1 → 0.13.1-alpha.0-20250515-a4c61c546
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/action/protocols.d.ts +59 -2
- package/dist/cjs/action/protocols.d.ts.map +1 -1
- package/dist/cjs/action/request/Read.d.ts +2 -2
- package/dist/cjs/action/request/Read.d.ts.map +1 -1
- package/dist/cjs/action/request/Read.js +4 -4
- package/dist/cjs/action/request/Read.js.map +1 -1
- package/dist/cjs/action/response/ReadResult.d.ts +6 -2
- package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeResponse.d.ts +46 -17
- package/dist/cjs/action/server/AttributeResponse.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeResponse.js +128 -110
- package/dist/cjs/action/server/AttributeResponse.js.map +2 -2
- package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts +36 -0
- package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/AttributeSubscriptionResponse.js +86 -0
- package/dist/cjs/action/server/AttributeSubscriptionResponse.js.map +6 -0
- package/dist/cjs/action/server/DataResponse.d.ts +45 -0
- package/dist/cjs/action/server/DataResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/DataResponse.js +69 -0
- package/dist/cjs/action/server/DataResponse.js.map +6 -0
- package/dist/cjs/action/server/EventResponse.d.ts +28 -0
- package/dist/cjs/action/server/EventResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/EventResponse.js +318 -0
- package/dist/cjs/action/server/EventResponse.js.map +6 -0
- package/dist/cjs/action/server/ServerInteraction.d.ts.map +1 -1
- package/dist/cjs/action/server/ServerInteraction.js +15 -2
- package/dist/cjs/action/server/ServerInteraction.js.map +1 -1
- package/dist/cjs/action/server/index.d.ts +3 -0
- package/dist/cjs/action/server/index.d.ts.map +1 -1
- package/dist/cjs/action/server/index.js +3 -0
- package/dist/cjs/action/server/index.js.map +1 -1
- package/dist/cjs/events/OccurrenceManager.d.ts +20 -11
- package/dist/cjs/events/OccurrenceManager.d.ts.map +1 -1
- package/dist/cjs/events/OccurrenceManager.js +113 -74
- package/dist/cjs/events/OccurrenceManager.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts +14 -2
- package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +87 -3
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/cjs/interaction/index.d.ts +0 -1
- package/dist/cjs/interaction/index.d.ts.map +1 -1
- package/dist/cjs/interaction/index.js +0 -1
- package/dist/cjs/interaction/index.js.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.js +1 -1
- package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/cjs/protocol/MessageExchange.js +11 -1
- package/dist/cjs/protocol/MessageExchange.js.map +1 -1
- package/dist/esm/action/protocols.d.ts +59 -2
- package/dist/esm/action/protocols.d.ts.map +1 -1
- package/dist/esm/action/request/Read.d.ts +2 -2
- package/dist/esm/action/request/Read.d.ts.map +1 -1
- package/dist/esm/action/request/Read.js +4 -4
- package/dist/esm/action/request/Read.js.map +1 -1
- package/dist/esm/action/response/ReadResult.d.ts +6 -2
- package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeResponse.d.ts +46 -17
- package/dist/esm/action/server/AttributeResponse.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeResponse.js +129 -113
- package/dist/esm/action/server/AttributeResponse.js.map +1 -1
- package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts +36 -0
- package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts.map +1 -0
- package/dist/esm/action/server/AttributeSubscriptionResponse.js +66 -0
- package/dist/esm/action/server/AttributeSubscriptionResponse.js.map +6 -0
- package/dist/esm/action/server/DataResponse.d.ts +45 -0
- package/dist/esm/action/server/DataResponse.d.ts.map +1 -0
- package/dist/esm/action/server/DataResponse.js +49 -0
- package/dist/esm/action/server/DataResponse.js.map +6 -0
- package/dist/esm/action/server/EventResponse.d.ts +28 -0
- package/dist/esm/action/server/EventResponse.d.ts.map +1 -0
- package/dist/esm/action/server/EventResponse.js +305 -0
- package/dist/esm/action/server/EventResponse.js.map +6 -0
- package/dist/esm/action/server/ServerInteraction.d.ts.map +1 -1
- package/dist/esm/action/server/ServerInteraction.js +16 -3
- package/dist/esm/action/server/ServerInteraction.js.map +1 -1
- package/dist/esm/action/server/index.d.ts +3 -0
- package/dist/esm/action/server/index.d.ts.map +1 -1
- package/dist/esm/action/server/index.js +3 -0
- package/dist/esm/action/server/index.js.map +1 -1
- package/dist/esm/events/OccurrenceManager.d.ts +20 -11
- package/dist/esm/events/OccurrenceManager.d.ts.map +1 -1
- package/dist/esm/events/OccurrenceManager.js +117 -80
- package/dist/esm/events/OccurrenceManager.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts +14 -2
- package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +87 -3
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/interaction/index.d.ts +0 -1
- package/dist/esm/interaction/index.d.ts.map +1 -1
- package/dist/esm/interaction/index.js +0 -1
- package/dist/esm/interaction/index.js.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.js +1 -1
- package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/esm/protocol/MessageExchange.js +11 -1
- package/dist/esm/protocol/MessageExchange.js.map +1 -1
- package/package.json +6 -6
- package/src/action/protocols.ts +68 -2
- package/src/action/request/Read.ts +2 -2
- package/src/action/response/ReadResult.ts +8 -1
- package/src/action/server/AttributeResponse.ts +145 -118
- package/src/action/server/AttributeSubscriptionResponse.ts +90 -0
- package/src/action/server/DataResponse.ts +70 -0
- package/src/action/server/EventResponse.ts +381 -0
- package/src/action/server/ServerInteraction.ts +18 -4
- package/src/action/server/index.ts +3 -0
- package/src/events/OccurrenceManager.ts +126 -100
- package/src/interaction/InteractionMessenger.ts +93 -8
- package/src/interaction/index.ts +0 -1
- package/src/peer/ControllerCommissioningFlow.ts +1 -1
- package/src/protocol/MessageExchange.ts +13 -1
- package/dist/cjs/interaction/ServerSubscription.d.ts +0 -116
- package/dist/cjs/interaction/ServerSubscription.d.ts.map +0 -1
- package/dist/cjs/interaction/ServerSubscription.js +0 -778
- package/dist/cjs/interaction/ServerSubscription.js.map +0 -6
- package/dist/esm/interaction/ServerSubscription.d.ts +0 -116
- package/dist/esm/interaction/ServerSubscription.d.ts.map +0 -1
- package/dist/esm/interaction/ServerSubscription.js +0 -778
- package/dist/esm/interaction/ServerSubscription.js.map +0 -6
- 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 {
|
|
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
|
-
|
|
23
|
+
TlvSchema,
|
|
24
24
|
} from "#types";
|
|
25
|
-
import {
|
|
26
|
-
|
|
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<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
#
|
|
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
|
-
//
|
|
67
|
-
#
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
constructor(node: NodeProtocol, session: SessionT) {
|
|
62
|
+
super(node, session);
|
|
63
|
+
}
|
|
72
64
|
|
|
73
|
-
|
|
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
|
|
89
|
+
this.addWildcard(path);
|
|
97
90
|
} else {
|
|
98
|
-
this
|
|
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
|
-
|
|
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
|
|
143
|
-
yield* this
|
|
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
|
|
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
|
|
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
|
-
|
|
173
|
+
protected addConcrete(path: ReadResult.ConcreteAttributePath) {
|
|
162
174
|
const { nodeId, endpointId, clusterId, attributeId } = path;
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
|
214
|
+
owningFabric: this.session.fabric,
|
|
201
215
|
};
|
|
202
|
-
const permission = this
|
|
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
|
|
222
|
+
this.addStatus(path, Status.UnsupportedAccess);
|
|
209
223
|
return;
|
|
210
224
|
|
|
211
225
|
case AccessControl.Authority.Restricted:
|
|
212
|
-
this
|
|
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
|
|
233
|
+
this.addStatus(path, Status.UnsupportedEndpoint);
|
|
220
234
|
return;
|
|
221
235
|
}
|
|
222
236
|
if (cluster === undefined) {
|
|
223
|
-
this
|
|
237
|
+
this.addStatus(path, Status.UnsupportedCluster);
|
|
224
238
|
return;
|
|
225
239
|
}
|
|
226
|
-
if (attribute === undefined) {
|
|
227
|
-
this
|
|
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
|
|
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
|
|
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
|
|
269
|
+
this.#currentState = cluster.open(this.session);
|
|
255
270
|
} else if (this.#currentState === undefined) {
|
|
256
|
-
this.#currentState = cluster.open(this
|
|
271
|
+
this.#currentState = cluster.open(this.session);
|
|
257
272
|
}
|
|
258
273
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
this
|
|
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
|
-
|
|
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
|
|
311
|
+
this.readClusterForWildcard(cluster, path);
|
|
297
312
|
}
|
|
298
313
|
} else {
|
|
299
314
|
const cluster = endpoint[clusterId];
|
|
300
315
|
if (cluster !== undefined) {
|
|
301
|
-
this
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.#
|
|
389
|
+
this.#currentState = this.#guardedCurrentCluster.open(this.session);
|
|
361
390
|
}
|
|
362
391
|
const value = this.#currentState[attribute.id];
|
|
363
|
-
if (value
|
|
364
|
-
//
|
|
365
|
-
this
|
|
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
|
-
|
|
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
|
-
|
|
401
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
+
}
|