@matter/protocol 0.13.1-alpha.0-20250515-a4c61c546 → 0.13.1-alpha.0-20250517-99a1e848a
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/Interactable.d.ts +5 -1
- package/dist/cjs/action/Interactable.d.ts.map +1 -1
- package/dist/cjs/action/client/ClientInteraction.d.ts +3 -4
- package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/cjs/action/client/ClientInteraction.js +28 -16
- package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
- package/dist/cjs/action/protocols.d.ts +7 -2
- package/dist/cjs/action/protocols.d.ts.map +1 -1
- package/dist/cjs/action/request/Write.d.ts +43 -2
- package/dist/cjs/action/request/Write.d.ts.map +1 -1
- package/dist/cjs/action/request/Write.js +74 -0
- package/dist/cjs/action/request/Write.js.map +2 -2
- package/dist/cjs/action/response/InvokeResult.d.ts +1 -2
- package/dist/cjs/action/response/InvokeResult.d.ts.map +1 -1
- package/dist/cjs/action/response/ReadResult.d.ts +5 -14
- package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
- package/dist/cjs/action/response/WriteResult.d.ts +19 -5
- package/dist/cjs/action/response/WriteResult.d.ts.map +1 -1
- package/dist/cjs/action/server/AccessControl.js +4 -4
- package/dist/cjs/action/server/AccessControl.js.map +1 -1
- package/dist/cjs/action/server/{AttributeResponse.d.ts → AttributeReadResponse.d.ts} +4 -4
- package/dist/cjs/action/server/AttributeReadResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/{AttributeResponse.js → AttributeReadResponse.js} +12 -12
- package/dist/cjs/action/server/{AttributeResponse.js.map → AttributeReadResponse.js.map} +2 -2
- package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts +4 -4
- package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeSubscriptionResponse.js +2 -2
- package/dist/cjs/action/server/AttributeSubscriptionResponse.js.map +1 -1
- package/dist/cjs/action/server/AttributeWriteResponse.d.ts +28 -0
- package/dist/cjs/action/server/AttributeWriteResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/AttributeWriteResponse.js +349 -0
- package/dist/cjs/action/server/AttributeWriteResponse.js.map +6 -0
- package/dist/cjs/action/server/DataResponse.d.ts +4 -3
- package/dist/cjs/action/server/DataResponse.d.ts.map +1 -1
- package/dist/cjs/action/server/DataResponse.js +1 -1
- package/dist/cjs/action/server/DataResponse.js.map +1 -1
- package/dist/cjs/action/server/{EventResponse.d.ts → EventReadResponse.d.ts} +4 -4
- package/dist/cjs/action/server/EventReadResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/{EventResponse.js → EventReadResponse.js} +8 -8
- package/dist/cjs/action/server/EventReadResponse.js.map +6 -0
- package/dist/cjs/action/server/ServerInteraction.d.ts +3 -4
- package/dist/cjs/action/server/ServerInteraction.d.ts.map +1 -1
- package/dist/cjs/action/server/ServerInteraction.js +12 -10
- package/dist/cjs/action/server/ServerInteraction.js.map +1 -1
- package/dist/cjs/action/server/index.d.ts +3 -2
- package/dist/cjs/action/server/index.d.ts.map +1 -1
- package/dist/cjs/action/server/index.js +3 -2
- package/dist/cjs/action/server/index.js.map +1 -1
- package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionClient.js +12 -1
- package/dist/cjs/interaction/InteractionClient.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +8 -3
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/action/Interactable.d.ts +5 -1
- package/dist/esm/action/Interactable.d.ts.map +1 -1
- package/dist/esm/action/client/ClientInteraction.d.ts +3 -4
- package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
- package/dist/esm/action/client/ClientInteraction.js +29 -17
- package/dist/esm/action/client/ClientInteraction.js.map +1 -1
- package/dist/esm/action/protocols.d.ts +7 -2
- package/dist/esm/action/protocols.d.ts.map +1 -1
- package/dist/esm/action/request/Write.d.ts +43 -2
- package/dist/esm/action/request/Write.d.ts.map +1 -1
- package/dist/esm/action/request/Write.js +70 -0
- package/dist/esm/action/request/Write.js.map +2 -2
- package/dist/esm/action/response/InvokeResult.d.ts +1 -2
- package/dist/esm/action/response/InvokeResult.d.ts.map +1 -1
- package/dist/esm/action/response/ReadResult.d.ts +5 -14
- package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
- package/dist/esm/action/response/WriteResult.d.ts +19 -5
- package/dist/esm/action/response/WriteResult.d.ts.map +1 -1
- package/dist/esm/action/server/AccessControl.js +4 -4
- package/dist/esm/action/server/AccessControl.js.map +1 -1
- package/dist/esm/action/server/{AttributeResponse.d.ts → AttributeReadResponse.d.ts} +4 -4
- package/dist/esm/action/server/AttributeReadResponse.d.ts.map +1 -0
- package/dist/esm/action/server/{AttributeResponse.js → AttributeReadResponse.js} +9 -9
- package/dist/esm/action/server/{AttributeResponse.js.map → AttributeReadResponse.js.map} +2 -2
- package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts +4 -4
- package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeSubscriptionResponse.js +2 -2
- package/dist/esm/action/server/AttributeSubscriptionResponse.js.map +1 -1
- package/dist/esm/action/server/AttributeWriteResponse.d.ts +28 -0
- package/dist/esm/action/server/AttributeWriteResponse.d.ts.map +1 -0
- package/dist/esm/action/server/AttributeWriteResponse.js +335 -0
- package/dist/esm/action/server/AttributeWriteResponse.js.map +6 -0
- package/dist/esm/action/server/DataResponse.d.ts +4 -3
- package/dist/esm/action/server/DataResponse.d.ts.map +1 -1
- package/dist/esm/action/server/DataResponse.js +1 -1
- package/dist/esm/action/server/DataResponse.js.map +1 -1
- package/dist/esm/action/server/{EventResponse.d.ts → EventReadResponse.d.ts} +4 -4
- package/dist/esm/action/server/EventReadResponse.d.ts.map +1 -0
- package/dist/esm/action/server/{EventResponse.js → EventReadResponse.js} +5 -5
- package/dist/esm/action/server/EventReadResponse.js.map +6 -0
- package/dist/esm/action/server/ServerInteraction.d.ts +3 -4
- package/dist/esm/action/server/ServerInteraction.d.ts.map +1 -1
- package/dist/esm/action/server/ServerInteraction.js +12 -10
- package/dist/esm/action/server/ServerInteraction.js.map +1 -1
- package/dist/esm/action/server/index.d.ts +3 -2
- package/dist/esm/action/server/index.d.ts.map +1 -1
- package/dist/esm/action/server/index.js +3 -2
- package/dist/esm/action/server/index.js.map +1 -1
- package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionClient.js +12 -1
- package/dist/esm/interaction/InteractionClient.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +8 -3
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/package.json +6 -6
- package/src/action/Interactable.ts +6 -1
- package/src/action/client/ClientInteraction.ts +31 -23
- package/src/action/protocols.ts +8 -2
- package/src/action/request/Write.ts +127 -4
- package/src/action/response/InvokeResult.ts +1 -2
- package/src/action/response/ReadResult.ts +5 -22
- package/src/action/response/WriteResult.ts +21 -5
- package/src/action/server/AccessControl.ts +4 -4
- package/src/action/server/{AttributeResponse.ts → AttributeReadResponse.ts} +14 -13
- package/src/action/server/AttributeSubscriptionResponse.ts +5 -5
- package/src/action/server/AttributeWriteResponse.ts +437 -0
- package/src/action/server/DataResponse.ts +5 -4
- package/src/action/server/{EventResponse.ts → EventReadResponse.ts} +6 -5
- package/src/action/server/ServerInteraction.ts +16 -14
- package/src/action/server/index.ts +3 -2
- package/src/interaction/InteractionClient.ts +20 -1
- package/src/interaction/InteractionMessenger.ts +8 -3
- package/dist/cjs/action/server/AttributeResponse.d.ts.map +0 -1
- package/dist/cjs/action/server/EventResponse.d.ts.map +0 -1
- package/dist/cjs/action/server/EventResponse.js.map +0 -6
- package/dist/esm/action/server/AttributeResponse.d.ts.map +0 -1
- package/dist/esm/action/server/EventResponse.d.ts.map +0 -1
- package/dist/esm/action/server/EventResponse.js.map +0 -6
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2025 Project CHIP Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { InteractionSession } from "#action/Interactable.js";
|
|
8
|
+
import { AttributeTypeProtocol, ClusterProtocol, EndpointProtocol, NodeProtocol } from "#action/protocols.js";
|
|
9
|
+
import { Write } from "#action/request/Write.js";
|
|
10
|
+
import { WriteResult } from "#action/response/WriteResult.js";
|
|
11
|
+
import { AccessControl } from "#action/server/AccessControl.js";
|
|
12
|
+
import { DataResponse, FallbackLimits, WildcardPathFlagsCodec } from "#action/server/DataResponse.js";
|
|
13
|
+
import { Diagnostic, InternalError, Logger } from "#general";
|
|
14
|
+
import { AttributeModel, DataModelPath, ElementTag, FabricIndex as FabricIndexField } from "#model";
|
|
15
|
+
import {
|
|
16
|
+
ArraySchema,
|
|
17
|
+
AttributePath,
|
|
18
|
+
FabricIndex,
|
|
19
|
+
Status,
|
|
20
|
+
StatusCode,
|
|
21
|
+
StatusResponseError,
|
|
22
|
+
TlvSchema,
|
|
23
|
+
TlvStream,
|
|
24
|
+
} from "#types";
|
|
25
|
+
|
|
26
|
+
const logger = Logger.get("AttributeWriteResponse");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Implements read of attribute data for Matter "read" and "subscribe" interactions.
|
|
30
|
+
*
|
|
31
|
+
* TODO - profile; ensure nested functions are properly JITed and/or inlined
|
|
32
|
+
*/
|
|
33
|
+
export class AttributeWriteResponse<
|
|
34
|
+
SessionT extends InteractionSession = InteractionSession,
|
|
35
|
+
> extends DataResponse<SessionT> {
|
|
36
|
+
#fabricIndex: FabricIndex;
|
|
37
|
+
// The following state updates as data producers execute. This serves both to convey state between functions and as
|
|
38
|
+
// a cache between producers that touch the same endpoint and/or cluster
|
|
39
|
+
#currentEndpoint?: EndpointProtocol;
|
|
40
|
+
#currentCluster?: ClusterProtocol;
|
|
41
|
+
#previousProcessedAttributePath?: WriteResult.ConcreteAttributePath;
|
|
42
|
+
#wildcardPathFlags = 0;
|
|
43
|
+
|
|
44
|
+
// Count how many attribute status (on error) and attribute values (on success) we have emitted
|
|
45
|
+
#statusCount = 0;
|
|
46
|
+
#successCount = 0;
|
|
47
|
+
#errorCount = 0;
|
|
48
|
+
|
|
49
|
+
constructor(node: NodeProtocol, session: SessionT) {
|
|
50
|
+
super(node, session);
|
|
51
|
+
this.#fabricIndex = session.fabric ?? FabricIndex.NO_FABRIC;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async process<T extends Write>({ writeRequests, suppressResponse }: T): WriteResult<T> {
|
|
55
|
+
const writeResponses = new Array<WriteResult.AttributeStatus>();
|
|
56
|
+
for (const { path, data, dataVersion } of writeRequests) {
|
|
57
|
+
if (path.endpointId === undefined || path.clusterId === undefined || path.attributeId === undefined) {
|
|
58
|
+
// dataVersion silently ignored for Wildcard?
|
|
59
|
+
const responses = await this.#processWildcard(path, data);
|
|
60
|
+
if (responses !== undefined) {
|
|
61
|
+
writeResponses.push(...responses);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
writeResponses.push(
|
|
65
|
+
await this.#writeConcrete(path as WriteResult.ConcreteAttributePath, data, dataVersion),
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!suppressResponse) {
|
|
71
|
+
return writeResponses as Awaited<WriteResult<T>>;
|
|
72
|
+
}
|
|
73
|
+
return undefined as Awaited<WriteResult<T>>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Guarded accessor for this.#currentEndpoint. This should never be undefined */
|
|
77
|
+
get #guardedCurrentEndpoint() {
|
|
78
|
+
if (this.#currentEndpoint === undefined) {
|
|
79
|
+
throw new InternalError("currentEndpoint is not set. Should never happen");
|
|
80
|
+
}
|
|
81
|
+
return this.#currentEndpoint;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Guarded accessor for this.#currentCluster. This should never be undefined */
|
|
85
|
+
get #guardedCurrentCluster(): ClusterProtocol {
|
|
86
|
+
if (this.#currentCluster === undefined) {
|
|
87
|
+
throw new InternalError("currentCluster is not set. Should never happen");
|
|
88
|
+
}
|
|
89
|
+
return this.#currentCluster;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get counts() {
|
|
93
|
+
return {
|
|
94
|
+
status: this.#statusCount,
|
|
95
|
+
success: this.#successCount,
|
|
96
|
+
existent: this.#successCount + this.#errorCount,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate a wildcard path and update internal state.
|
|
102
|
+
*/
|
|
103
|
+
async #processWildcard(path: AttributePath, value: TlvStream) {
|
|
104
|
+
const { nodeId, endpointId, wildcardPathFlags } = path;
|
|
105
|
+
|
|
106
|
+
if (nodeId !== undefined && nodeId !== this.nodeId) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// TODO clarify isf that's needed or not
|
|
111
|
+
this.#wildcardPathFlags = wildcardPathFlags ? WildcardPathFlagsCodec.encode(wildcardPathFlags) : 0;
|
|
112
|
+
|
|
113
|
+
// TODO: Add Group handling and validation
|
|
114
|
+
/*
|
|
115
|
+
if (isGroupSession && endpointId !== undefined) {
|
|
116
|
+
throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
|
|
117
|
+
}
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
if (endpointId === undefined) {
|
|
121
|
+
const responses = new Array<WriteResult.AttributeStatus>();
|
|
122
|
+
for (const endpoint of this.node) {
|
|
123
|
+
const response = await this.#writeEndpointForWildcard(endpoint, path, value);
|
|
124
|
+
if (response !== undefined) {
|
|
125
|
+
responses.push(response);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return responses;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const endpoint = this.node[endpointId];
|
|
132
|
+
if (endpoint) {
|
|
133
|
+
const response = await this.#writeEndpointForWildcard(endpoint, path, value);
|
|
134
|
+
if (response !== undefined) {
|
|
135
|
+
return [response];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Write to a concrete path and update internal state.
|
|
142
|
+
*/
|
|
143
|
+
async #writeConcrete(path: WriteResult.ConcreteAttributePath, value: TlvStream, version?: number) {
|
|
144
|
+
const { nodeId, endpointId, clusterId, attributeId } = path;
|
|
145
|
+
|
|
146
|
+
if (nodeId !== undefined && this.nodeId !== nodeId) {
|
|
147
|
+
return this.#asStatus(path, Status.UnsupportedNode);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Resolve path elements
|
|
151
|
+
const endpoint = this.node[endpointId];
|
|
152
|
+
const cluster = endpoint?.[clusterId];
|
|
153
|
+
const attribute = cluster?.type.attributes[attributeId];
|
|
154
|
+
let limits;
|
|
155
|
+
if (attribute === undefined) {
|
|
156
|
+
// We still need to authorize the user for access even though this path doesn't resolve. Spec is not
|
|
157
|
+
// explicit on what privilege level we should require as normally that information comes from the resolved
|
|
158
|
+
// attribute. So attempt to resolve via the active model
|
|
159
|
+
const modelAttr = this.node.matter
|
|
160
|
+
.member(path.clusterId, [ElementTag.Cluster])
|
|
161
|
+
?.member(path.attributeId, [ElementTag.Attribute]);
|
|
162
|
+
|
|
163
|
+
if (modelAttr) {
|
|
164
|
+
// OK cluster doesn't exist at that location, but we do understand semantically, so use limits from the
|
|
165
|
+
// model
|
|
166
|
+
limits = AccessControl(modelAttr as AttributeModel).limits;
|
|
167
|
+
} else {
|
|
168
|
+
// We've got no idea. This effectively falls back to "view" privilege
|
|
169
|
+
limits = FallbackLimits;
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
limits = attribute.limits;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Validate access. Order here prescribed by 1.4 core spec 8.4.3.2
|
|
176
|
+
// We need some fallback location if cluster is not defined
|
|
177
|
+
const location = {
|
|
178
|
+
...(cluster?.location ?? {
|
|
179
|
+
path: DataModelPath.none,
|
|
180
|
+
endpoint: endpointId,
|
|
181
|
+
cluster: clusterId,
|
|
182
|
+
}),
|
|
183
|
+
owningFabric: this.session.fabric,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const permission = this.session.authorityAt(limits.writeLevel, location);
|
|
187
|
+
switch (permission) {
|
|
188
|
+
case AccessControl.Authority.Granted:
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case AccessControl.Authority.Unauthorized:
|
|
192
|
+
return this.#asStatus(path, Status.UnsupportedAccess);
|
|
193
|
+
|
|
194
|
+
case AccessControl.Authority.Restricted:
|
|
195
|
+
return this.#asStatus(path, Status.AccessRestricted);
|
|
196
|
+
|
|
197
|
+
default:
|
|
198
|
+
throw new InternalError(`Unsupported authorization state ${permission}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (endpoint === undefined) {
|
|
202
|
+
return this.#asStatus(path, Status.UnsupportedEndpoint);
|
|
203
|
+
}
|
|
204
|
+
if (cluster === undefined) {
|
|
205
|
+
return this.#asStatus(path, Status.UnsupportedCluster);
|
|
206
|
+
}
|
|
207
|
+
if (attribute === undefined || !cluster.type.attributes[attribute.id]) {
|
|
208
|
+
return this.#asStatus(path, Status.UnsupportedAttribute);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!limits.writable) {
|
|
212
|
+
this.#errorCount++;
|
|
213
|
+
return this.#asStatus(path, Status.UnsupportedWrite);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Old implementation aka Matter 1.2 and lower need the ACL check moved here.
|
|
217
|
+
// see https://github.com/project-chip/connectedhomeip/issues/33735
|
|
218
|
+
// We have patched our tests for now
|
|
219
|
+
|
|
220
|
+
if (limits.timed && !this.session.timed) {
|
|
221
|
+
this.#errorCount++;
|
|
222
|
+
return this.#asStatus(path, Status.NeedsTimedInteraction);
|
|
223
|
+
}
|
|
224
|
+
if (limits.fabricScoped && this.session.fabric === undefined) {
|
|
225
|
+
this.#errorCount++;
|
|
226
|
+
return this.#asStatus(path, Status.UnsupportedAccess);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (version !== undefined && version !== cluster.version) {
|
|
230
|
+
this.#errorCount++;
|
|
231
|
+
return this.#asStatus(path, Status.DataVersionMismatch);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Update internal state for target endpoint
|
|
235
|
+
if (this.#currentEndpoint !== endpoint) {
|
|
236
|
+
this.#currentEndpoint = endpoint;
|
|
237
|
+
this.#currentCluster = cluster;
|
|
238
|
+
} else if (this.#currentCluster !== cluster) {
|
|
239
|
+
this.#currentCluster = cluster;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return await this.writeValue(attribute, path, value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Starts new chunk or adds to current chunk all values from {@link endpoint} selected by {@link path}.
|
|
247
|
+
*
|
|
248
|
+
* Emits previous chunk if it exists and was not for this endpoint. This means that our chunk size is one endpoint
|
|
249
|
+
* worth of data, except for the initial error chunk if there are path errors.
|
|
250
|
+
*
|
|
251
|
+
* {@link this.#wildcardPathFlags} to numeric bitmap must be set prior to invocation.
|
|
252
|
+
*
|
|
253
|
+
* TODO - skip endpoints for which subject is unauthorized as optimization
|
|
254
|
+
*/
|
|
255
|
+
#writeEndpointForWildcard(endpoint: EndpointProtocol, path: AttributePath, value: TlvStream) {
|
|
256
|
+
const { clusterId, attributeId } = path;
|
|
257
|
+
if (clusterId === undefined || attributeId === undefined) {
|
|
258
|
+
throw new StatusResponseError(
|
|
259
|
+
"Wildcard path write must specify a clusterId and attributeId",
|
|
260
|
+
StatusCode.InvalidAction,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (endpoint.wildcardPathFlags & this.#wildcardPathFlags) {
|
|
265
|
+
return; // TODO ???
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (this.#currentEndpoint !== endpoint) {
|
|
269
|
+
this.#currentEndpoint = endpoint;
|
|
270
|
+
this.#currentCluster = undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const cluster = endpoint[clusterId];
|
|
274
|
+
if (cluster !== undefined) {
|
|
275
|
+
return this.#writeClusterForWildcard(cluster, path, value);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Read values from a specific {@link cluster} for a wildcard path.
|
|
281
|
+
*
|
|
282
|
+
* Depends on state initialized by {@link #writeEndpointForWildcard}.
|
|
283
|
+
*
|
|
284
|
+
* TODO - skip clusters for which subject is unauthorized
|
|
285
|
+
*/
|
|
286
|
+
#writeClusterForWildcard(cluster: ClusterProtocol, path: AttributePath, value: TlvStream) {
|
|
287
|
+
if (cluster.type.wildcardPathFlags & this.#wildcardPathFlags) {
|
|
288
|
+
return; // TODO ???
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (this.#currentCluster !== cluster) {
|
|
292
|
+
this.#currentCluster = cluster;
|
|
293
|
+
}
|
|
294
|
+
const { attributeId } = path;
|
|
295
|
+
|
|
296
|
+
if (attributeId === undefined) {
|
|
297
|
+
throw new StatusResponseError("Wildcard path write must specify an attributeId", StatusCode.InvalidAction);
|
|
298
|
+
} else {
|
|
299
|
+
const attribute = cluster.type.attributes[attributeId];
|
|
300
|
+
if (attribute !== undefined) {
|
|
301
|
+
return this.#writeAttributeForWildcard(attribute, path, value);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Read values from a specific {@link attribute} for a wildcard path.
|
|
308
|
+
*
|
|
309
|
+
* Depends on state initialized by {@link #writeClusterForWildcard}.
|
|
310
|
+
*/
|
|
311
|
+
#writeAttributeForWildcard(attribute: AttributeTypeProtocol, path: AttributePath, value: TlvStream) {
|
|
312
|
+
if (!this.#guardedCurrentCluster.type.attributes[attribute.id]) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (attribute.wildcardPathFlags & this.#wildcardPathFlags) {
|
|
317
|
+
return; // TODO ????
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (
|
|
321
|
+
!attribute.limits.writable ||
|
|
322
|
+
this.session.authorityAt(attribute.limits.readLevel, this.#guardedCurrentCluster.location) !==
|
|
323
|
+
AccessControl.Authority.Granted ||
|
|
324
|
+
(attribute.limits.timed && !this.session.timed)
|
|
325
|
+
) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return this.writeValue(
|
|
330
|
+
attribute,
|
|
331
|
+
{
|
|
332
|
+
...path,
|
|
333
|
+
endpointId: this.#guardedCurrentEndpoint.id,
|
|
334
|
+
clusterId: this.#guardedCurrentCluster.type.id,
|
|
335
|
+
attributeId: attribute.id,
|
|
336
|
+
},
|
|
337
|
+
value,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Add a status value.
|
|
343
|
+
*/
|
|
344
|
+
#asStatus(path: WriteResult.ConcreteAttributePath, status: Status, clusterStatus?: number) {
|
|
345
|
+
if (status !== Status.Success) {
|
|
346
|
+
logger.debug(
|
|
347
|
+
() =>
|
|
348
|
+
`Error writing attribute ${this.node.inspectPath(path)}: Status=${StatusCode[status]}(${status}), ClusterStatus=${clusterStatus}`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const report: WriteResult.AttributeStatus = {
|
|
353
|
+
kind: "attr-status",
|
|
354
|
+
path,
|
|
355
|
+
status,
|
|
356
|
+
clusterStatus,
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
if (status !== Status.Success) {
|
|
360
|
+
this.#statusCount++;
|
|
361
|
+
}
|
|
362
|
+
return report;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
protected async writeValue(
|
|
366
|
+
attribute: AttributeTypeProtocol,
|
|
367
|
+
path: WriteResult.ConcreteAttributePath,
|
|
368
|
+
value: TlvStream,
|
|
369
|
+
) {
|
|
370
|
+
const { attributeId, listIndex } = path;
|
|
371
|
+
|
|
372
|
+
if (listIndex !== undefined && listIndex !== null) {
|
|
373
|
+
throw new StatusResponseError(
|
|
374
|
+
`Unsupported Write path provided: listIndex === ${listIndex}`,
|
|
375
|
+
Status.InvalidAction,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const previousPath = this.#previousProcessedAttributePath;
|
|
380
|
+
this.#previousProcessedAttributePath = path;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const { tlv } = attribute;
|
|
384
|
+
if (listIndex === undefined) {
|
|
385
|
+
const decoded = this.#decodeWithSchema(tlv, value);
|
|
386
|
+
// REPLACE_ALL
|
|
387
|
+
//await this.session.transaction?.begin();
|
|
388
|
+
logger.debug(() => `Writing attribute ${this.node.inspectPath(path)}=${Diagnostic.json(decoded)}`);
|
|
389
|
+
const writeState = await this.#guardedCurrentCluster.openForWrite(this.session);
|
|
390
|
+
writeState[attributeId] = decoded;
|
|
391
|
+
await this.session.transaction?.commit();
|
|
392
|
+
} else if (listIndex === null) {
|
|
393
|
+
if (
|
|
394
|
+
previousPath?.endpointId !== path.endpointId ||
|
|
395
|
+
previousPath?.clusterId !== path.clusterId ||
|
|
396
|
+
previousPath?.attributeId !== path.attributeId
|
|
397
|
+
) {
|
|
398
|
+
// Mimic chip sdk behavior
|
|
399
|
+
throw new StatusResponseError("ADD list action without a former REPLACE_ALL action", Status.Busy);
|
|
400
|
+
}
|
|
401
|
+
// ADD
|
|
402
|
+
if (!(tlv instanceof ArraySchema)) {
|
|
403
|
+
throw new StatusResponseError(
|
|
404
|
+
`Unsupported Write path provided: listIndex === ${listIndex} but attribute is not a list`,
|
|
405
|
+
Status.InvalidAction,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const writeState = await this.#guardedCurrentCluster.openForWrite(this.session);
|
|
409
|
+
const decoded = this.#decodeWithSchema(tlv.elementSchema, value);
|
|
410
|
+
logger.debug(
|
|
411
|
+
() => `Writing attribute chunk ${this.node.inspectPath(path)} adding ${Diagnostic.json(decoded)}`,
|
|
412
|
+
);
|
|
413
|
+
(writeState[attributeId] as any[]).push(decoded);
|
|
414
|
+
await this.session.transaction?.commit();
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
await this.session.transaction?.rollback();
|
|
418
|
+
if (StatusResponseError.is(error)) {
|
|
419
|
+
this.#errorCount++;
|
|
420
|
+
return this.#asStatus(path, error.code, error.clusterCode);
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.#successCount++;
|
|
426
|
+
return this.#asStatus(path, Status.Success);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#decodeWithSchema(tlv: TlvSchema<any>, value: TlvStream) {
|
|
430
|
+
return tlv.injectField(
|
|
431
|
+
tlv.decodeTlv(value),
|
|
432
|
+
<number>FabricIndexField.id,
|
|
433
|
+
this.#fabricIndex,
|
|
434
|
+
() => true, // We always inject the current fabricIndex for writes
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright 2022-2025 Project CHIP Authors
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { InteractionSession } from "#action/index.js";
|
|
6
7
|
import { NodeProtocol } from "#action/protocols.js";
|
|
7
8
|
import { AccessControl } from "#action/server/AccessControl.js";
|
|
8
9
|
import { AccessLevel } from "#model";
|
|
@@ -19,12 +20,12 @@ export const FallbackLimits: AccessControl.Limits = {
|
|
|
19
20
|
writeLevel: AccessLevel.Administer,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
export abstract class DataResponse<SessionT extends
|
|
23
|
+
export abstract class DataResponse<SessionT extends InteractionSession = InteractionSession> {
|
|
23
24
|
// Configuration
|
|
24
25
|
#session: SessionT;
|
|
25
26
|
#node: NodeProtocol;
|
|
26
27
|
|
|
27
|
-
// The node ID may be expensive to retrieve and is invariant so we cache it here
|
|
28
|
+
// The node ID may be expensive to retrieve and is invariant, so we cache it here
|
|
28
29
|
#cachedNodeId?: NodeId;
|
|
29
30
|
|
|
30
31
|
constructor(node: NodeProtocol, session: SessionT) {
|
|
@@ -63,8 +64,8 @@ export abstract class DataResponse<SessionT extends AccessControl.Session = Acce
|
|
|
63
64
|
status: number;
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
* Number of value responses (aka success we have sent)
|
|
67
|
+
* Number of success or value responses (aka success we have sent)
|
|
67
68
|
*/
|
|
68
|
-
|
|
69
|
+
success: number;
|
|
69
70
|
};
|
|
70
71
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { InteractionSession } from "#action/index.js";
|
|
7
8
|
import { ClusterProtocol, EndpointProtocol, EventTypeProtocol, NodeProtocol } from "#action/protocols.js";
|
|
8
9
|
import { Read } from "#action/request/Read.js";
|
|
9
10
|
import { ReadResult } from "#action/response/ReadResult.js";
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
TlvSchema,
|
|
24
25
|
} from "#types";
|
|
25
26
|
|
|
26
|
-
const logger = Logger.get("
|
|
27
|
+
const logger = Logger.get("EventReadResponse");
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Implements read of event data for Matter "read" and "subscribe" interactions.
|
|
@@ -32,8 +33,8 @@ const logger = Logger.get("EventResponse");
|
|
|
32
33
|
*
|
|
33
34
|
* TODO - profile; ensure nested functions are properly JITed and/or inlined
|
|
34
35
|
*/
|
|
35
|
-
export class
|
|
36
|
-
SessionT extends
|
|
36
|
+
export class EventReadResponse<
|
|
37
|
+
SessionT extends InteractionSession = InteractionSession,
|
|
37
38
|
> extends DataResponse<SessionT> {
|
|
38
39
|
// Normalized Event Filter to just our node-id
|
|
39
40
|
#eventMinVersion?: EventNumber;
|
|
@@ -102,7 +103,7 @@ export class EventResponse<
|
|
|
102
103
|
get counts() {
|
|
103
104
|
return {
|
|
104
105
|
status: this.#statusCount,
|
|
105
|
-
|
|
106
|
+
success: this.#valueCount,
|
|
106
107
|
existent: this.#allowedEventPaths.size,
|
|
107
108
|
};
|
|
108
109
|
}
|
|
@@ -352,7 +353,7 @@ export class EventResponse<
|
|
|
352
353
|
#asStatus(path: ReadResult.ConcreteEventPath, status: Status) {
|
|
353
354
|
logger.debug(`Error reading event ${this.node.inspectPath(path)}: Status=${StatusCode[status]}(${status})`);
|
|
354
355
|
|
|
355
|
-
const report: ReadResult.
|
|
356
|
+
const report: ReadResult.EventStatus = {
|
|
356
357
|
kind: "event-status",
|
|
357
358
|
path,
|
|
358
359
|
status,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Interactable } from "#action/Interactable.js";
|
|
7
|
+
import { Interactable, InteractionSession } from "#action/Interactable.js";
|
|
8
8
|
import { NodeProtocol } from "#action/protocols.js";
|
|
9
9
|
import { Invoke } from "#action/request/Invoke.js";
|
|
10
10
|
import { Read } from "#action/request/Read.js";
|
|
@@ -14,10 +14,10 @@ import { InvokeResult } from "#action/response/InvokeResult.js";
|
|
|
14
14
|
import { ReadResult } from "#action/response/ReadResult.js";
|
|
15
15
|
import { SubscribeResult } from "#action/response/SubscribeResult.js";
|
|
16
16
|
import { WriteResult } from "#action/response/WriteResult.js";
|
|
17
|
-
import {
|
|
18
|
-
import { EventResponse } from "#action/server/EventResponse.js";
|
|
17
|
+
import { EventReadResponse } from "#action/server/EventReadResponse.js";
|
|
19
18
|
import { Logger, NotImplementedError } from "#general";
|
|
20
|
-
import {
|
|
19
|
+
import { AttributeReadResponse } from "./AttributeReadResponse.js";
|
|
20
|
+
import { AttributeWriteResponse } from "./AttributeWriteResponse.js";
|
|
21
21
|
|
|
22
22
|
const logger = Logger.get("ServerInteraction");
|
|
23
23
|
|
|
@@ -31,7 +31,7 @@ const logger = Logger.get("ServerInteraction");
|
|
|
31
31
|
*
|
|
32
32
|
* - InteractionEndpointStructure ({@link NodeProtocol} is largely duplicative)
|
|
33
33
|
*/
|
|
34
|
-
export class ServerInteraction<SessionT extends
|
|
34
|
+
export class ServerInteraction<SessionT extends InteractionSession = InteractionSession>
|
|
35
35
|
implements Interactable<SessionT>
|
|
36
36
|
{
|
|
37
37
|
#node: NodeProtocol;
|
|
@@ -45,18 +45,18 @@ export class ServerInteraction<SessionT extends AccessControl.Session = AccessCo
|
|
|
45
45
|
|
|
46
46
|
let readInfo = "";
|
|
47
47
|
if (Read.containsAttribute(request)) {
|
|
48
|
-
const attributeReader = new
|
|
48
|
+
const attributeReader = new AttributeReadResponse(this.#node, session);
|
|
49
49
|
yield* attributeReader.process(request);
|
|
50
50
|
|
|
51
|
-
const { existent, status,
|
|
52
|
-
readInfo = `${existent} matching attributes (${status ? `${status} status, ` : ""}${
|
|
51
|
+
const { existent, status, success } = attributeReader.counts;
|
|
52
|
+
readInfo = `${existent} matching attributes (${status ? `${status} status, ` : ""}${success ? `${success} values` : ""})`;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (Read.containsEvent(request)) {
|
|
56
|
-
const eventReader = new
|
|
56
|
+
const eventReader = new EventReadResponse(this.#node, session);
|
|
57
57
|
yield* eventReader.process(request);
|
|
58
|
-
const { existent, status,
|
|
59
|
-
readInfo += `${readInfo.length > 0 ? ", " : ""}${existent} matching events (${status ? `${status} status, ` : ""}${
|
|
58
|
+
const { existent, status, success } = eventReader.counts;
|
|
59
|
+
readInfo += `${readInfo.length > 0 ? ", " : ""}${existent} matching events (${status ? `${status} status, ` : ""}${success ? `${success} values` : ""})`;
|
|
60
60
|
}
|
|
61
61
|
logger.debug(`Read request resolved to ${readInfo}`);
|
|
62
62
|
}
|
|
@@ -66,9 +66,11 @@ export class ServerInteraction<SessionT extends AccessControl.Session = AccessCo
|
|
|
66
66
|
throw new NotImplementedError();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
write<T extends Write>(
|
|
70
|
-
// TODO
|
|
71
|
-
|
|
69
|
+
write<T extends Write>(request: T, session: SessionT): WriteResult<T> {
|
|
70
|
+
// TODO - validate request
|
|
71
|
+
|
|
72
|
+
const writer = new AttributeWriteResponse(this.#node, session);
|
|
73
|
+
return writer.process(request);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
invoke<T extends Invoke>(_request: T, _session?: SessionT): InvokeResult<T> {
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export * from "./AccessControl.js";
|
|
8
|
-
export * from "./
|
|
8
|
+
export * from "./AttributeReadResponse.js";
|
|
9
9
|
export * from "./AttributeSubscriptionResponse.js";
|
|
10
|
+
export * from "./AttributeWriteResponse.js";
|
|
10
11
|
export * from "./DataResponse.js";
|
|
11
|
-
export * from "./
|
|
12
|
+
export * from "./EventReadResponse.js";
|
|
12
13
|
export * from "./ServerInteraction.js";
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { AccessControl } from "#clusters/access-control";
|
|
7
8
|
import {
|
|
8
9
|
Diagnostic,
|
|
9
10
|
Environment,
|
|
@@ -63,6 +64,15 @@ const REQUEST_ALL = [{}];
|
|
|
63
64
|
const DEFAULT_TIMED_REQUEST_TIMEOUT_MS = 10_000; // 10 seconds
|
|
64
65
|
const DEFAULT_MINIMUM_RESPONSE_TIMEOUT_WITH_FAILSAFE_MS = 30_000; // 30 seconds
|
|
65
66
|
|
|
67
|
+
const AclClusterId = AccessControl.Complete.id;
|
|
68
|
+
const AclAttributeId = AccessControl.Complete.attributes.acl.id;
|
|
69
|
+
const AclExtensionAttributeId = AccessControl.Complete.attributes.extension.id;
|
|
70
|
+
|
|
71
|
+
function isAclOrExtensionPath(path: { clusterId: ClusterId; attributeId: AttributeId }) {
|
|
72
|
+
const { clusterId, attributeId } = path;
|
|
73
|
+
return clusterId === AclClusterId && (attributeId === AclAttributeId || attributeId === AclExtensionAttributeId);
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
export interface AttributeStatus {
|
|
67
77
|
path: {
|
|
68
78
|
nodeId?: NodeId;
|
|
@@ -554,9 +564,18 @@ export class InteractionClient {
|
|
|
554
564
|
)
|
|
555
565
|
.join(", ")}`,
|
|
556
566
|
);
|
|
567
|
+
// TODO Add multi message write handling with streamed encoding
|
|
557
568
|
const writeRequests = attributes.flatMap(
|
|
558
569
|
({ endpointId, clusterId, attribute: { id, schema }, value, dataVersion }) => {
|
|
559
|
-
if (
|
|
570
|
+
if (
|
|
571
|
+
chunkLists &&
|
|
572
|
+
Array.isArray(value) &&
|
|
573
|
+
schema instanceof ArraySchema &&
|
|
574
|
+
// As implemented for Matter 1.4.2 in https://github.com/project-chip/connectedhomeip/pull/38263
|
|
575
|
+
// Acl writes will no longer be chunked by default, all others still
|
|
576
|
+
// Will be streamlined later ... see https://github.com/project-chip/connectedhomeip/issues/38270
|
|
577
|
+
!isAclOrExtensionPath({ clusterId, attributeId: id })
|
|
578
|
+
) {
|
|
560
579
|
return schema
|
|
561
580
|
.encodeAsChunkedArray(value, { forWriteInteraction: true })
|
|
562
581
|
.map(({ element: data, listIndex }) => ({
|
|
@@ -712,7 +712,7 @@ export class InteractionServerMessenger extends InteractionMessenger {
|
|
|
712
712
|
return data;
|
|
713
713
|
}
|
|
714
714
|
case "attr-status": {
|
|
715
|
-
const { path, status } = report;
|
|
715
|
+
const { path, status, clusterStatus } = report;
|
|
716
716
|
const statusReport: AttributeReportPayload = {
|
|
717
717
|
attributeStatus: {
|
|
718
718
|
path,
|
|
@@ -720,6 +720,9 @@ export class InteractionServerMessenger extends InteractionMessenger {
|
|
|
720
720
|
},
|
|
721
721
|
hasFabricSensitiveData: false,
|
|
722
722
|
};
|
|
723
|
+
if (clusterStatus !== undefined) {
|
|
724
|
+
statusReport.attributeStatus!.status.clusterStatus = clusterStatus;
|
|
725
|
+
}
|
|
723
726
|
return statusReport;
|
|
724
727
|
}
|
|
725
728
|
case "event-value": {
|
|
@@ -745,7 +748,7 @@ export class InteractionServerMessenger extends InteractionMessenger {
|
|
|
745
748
|
return data;
|
|
746
749
|
}
|
|
747
750
|
case "event-status": {
|
|
748
|
-
const { path, status } = report;
|
|
751
|
+
const { path, status, clusterStatus } = report;
|
|
749
752
|
const statusReport: EventReportPayload = {
|
|
750
753
|
eventStatus: {
|
|
751
754
|
path,
|
|
@@ -753,10 +756,12 @@ export class InteractionServerMessenger extends InteractionMessenger {
|
|
|
753
756
|
},
|
|
754
757
|
hasFabricSensitiveData: false,
|
|
755
758
|
};
|
|
759
|
+
if (clusterStatus !== undefined) {
|
|
760
|
+
statusReport.eventStatus!.status.clusterStatus = clusterStatus;
|
|
761
|
+
}
|
|
756
762
|
return statusReport;
|
|
757
763
|
}
|
|
758
764
|
}
|
|
759
|
-
throw new InternalError(`Unknown report type: ${report.kind}`);
|
|
760
765
|
}
|
|
761
766
|
}
|
|
762
767
|
|