@matter/protocol 0.13.0 → 0.13.1-alpha.0-20250501-80c86b03e
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/response/ReadResult.d.ts +4 -0
- package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeResponse.d.ts +1 -1
- package/dist/cjs/action/server/AttributeResponse.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeResponse.js +37 -17
- package/dist/cjs/action/server/AttributeResponse.js.map +2 -2
- package/dist/cjs/interaction/AttributeDataEncoder.d.ts +1 -1
- package/dist/cjs/interaction/AttributeDataEncoder.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionEndpointStructure.d.ts +37 -1
- package/dist/cjs/interaction/InteractionEndpointStructure.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionEndpointStructure.js +34 -15
- package/dist/cjs/interaction/InteractionEndpointStructure.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/cjs/interaction/ServerSubscription.d.ts +1 -2
- package/dist/cjs/interaction/ServerSubscription.d.ts.map +1 -1
- package/dist/cjs/interaction/ServerSubscription.js +20 -20
- package/dist/cjs/interaction/ServerSubscription.js.map +1 -1
- package/dist/cjs/interaction/SubscriptionClient.d.ts +2 -1
- package/dist/cjs/interaction/SubscriptionClient.d.ts.map +1 -1
- package/dist/cjs/interaction/SubscriptionClient.js +2 -1
- package/dist/cjs/interaction/SubscriptionClient.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/protocol/ExchangeManager.d.ts.map +1 -1
- package/dist/cjs/protocol/ExchangeManager.js +7 -2
- package/dist/cjs/protocol/ExchangeManager.js.map +1 -1
- package/dist/cjs/protocol/ProtocolHandler.d.ts +1 -0
- package/dist/cjs/protocol/ProtocolHandler.d.ts.map +1 -1
- package/dist/cjs/securechannel/SecureChannelProtocol.d.ts +1 -0
- package/dist/cjs/securechannel/SecureChannelProtocol.d.ts.map +1 -1
- package/dist/cjs/securechannel/SecureChannelProtocol.js +1 -0
- package/dist/cjs/securechannel/SecureChannelProtocol.js.map +1 -1
- package/dist/cjs/session/case/CaseServer.d.ts +1 -0
- package/dist/cjs/session/case/CaseServer.d.ts.map +1 -1
- package/dist/cjs/session/case/CaseServer.js +1 -0
- package/dist/cjs/session/case/CaseServer.js.map +1 -1
- package/dist/cjs/session/pase/PaseServer.d.ts +1 -0
- package/dist/cjs/session/pase/PaseServer.d.ts.map +1 -1
- package/dist/cjs/session/pase/PaseServer.js +1 -0
- package/dist/cjs/session/pase/PaseServer.js.map +1 -1
- package/dist/esm/action/response/ReadResult.d.ts +4 -0
- package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeResponse.d.ts +1 -1
- package/dist/esm/action/server/AttributeResponse.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeResponse.js +37 -17
- package/dist/esm/action/server/AttributeResponse.js.map +1 -1
- package/dist/esm/interaction/AttributeDataEncoder.d.ts +1 -1
- package/dist/esm/interaction/AttributeDataEncoder.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionEndpointStructure.d.ts +37 -1
- package/dist/esm/interaction/InteractionEndpointStructure.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionEndpointStructure.js +21 -7
- package/dist/esm/interaction/InteractionEndpointStructure.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +1 -1
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/interaction/ServerSubscription.d.ts +1 -2
- package/dist/esm/interaction/ServerSubscription.d.ts.map +1 -1
- package/dist/esm/interaction/ServerSubscription.js +4 -4
- package/dist/esm/interaction/ServerSubscription.js.map +1 -1
- package/dist/esm/interaction/SubscriptionClient.d.ts +2 -1
- package/dist/esm/interaction/SubscriptionClient.d.ts.map +1 -1
- package/dist/esm/interaction/SubscriptionClient.js +2 -1
- package/dist/esm/interaction/SubscriptionClient.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/protocol/ExchangeManager.d.ts.map +1 -1
- package/dist/esm/protocol/ExchangeManager.js +7 -2
- package/dist/esm/protocol/ExchangeManager.js.map +1 -1
- package/dist/esm/protocol/ProtocolHandler.d.ts +1 -0
- package/dist/esm/protocol/ProtocolHandler.d.ts.map +1 -1
- package/dist/esm/securechannel/SecureChannelProtocol.d.ts +1 -0
- package/dist/esm/securechannel/SecureChannelProtocol.d.ts.map +1 -1
- package/dist/esm/securechannel/SecureChannelProtocol.js +1 -0
- package/dist/esm/securechannel/SecureChannelProtocol.js.map +1 -1
- package/dist/esm/session/case/CaseServer.d.ts +1 -0
- package/dist/esm/session/case/CaseServer.d.ts.map +1 -1
- package/dist/esm/session/case/CaseServer.js +1 -0
- package/dist/esm/session/case/CaseServer.js.map +1 -1
- package/dist/esm/session/pase/PaseServer.d.ts +1 -0
- package/dist/esm/session/pase/PaseServer.d.ts.map +1 -1
- package/dist/esm/session/pase/PaseServer.js +1 -0
- package/dist/esm/session/pase/PaseServer.js.map +1 -1
- package/package.json +6 -6
- package/src/action/response/ReadResult.ts +4 -0
- package/src/action/server/AttributeResponse.ts +40 -17
- package/src/interaction/AttributeDataEncoder.ts +1 -1
- package/src/interaction/InteractionEndpointStructure.ts +62 -12
- package/src/interaction/InteractionMessenger.ts +1 -1
- package/src/interaction/ServerSubscription.ts +5 -5
- package/src/interaction/SubscriptionClient.ts +2 -2
- package/src/interaction/index.ts +0 -1
- package/src/protocol/ExchangeManager.ts +15 -2
- package/src/protocol/ProtocolHandler.ts +1 -0
- package/src/securechannel/SecureChannelProtocol.ts +1 -0
- package/src/session/case/CaseServer.ts +1 -0
- package/src/session/pase/PaseServer.ts +1 -0
- package/dist/cjs/interaction/InteractionServer.d.ts +0 -132
- package/dist/cjs/interaction/InteractionServer.d.ts.map +0 -1
- package/dist/cjs/interaction/InteractionServer.js +0 -1166
- package/dist/cjs/interaction/InteractionServer.js.map +0 -6
- package/dist/esm/interaction/InteractionServer.d.ts +0 -132
- package/dist/esm/interaction/InteractionServer.d.ts.map +0 -1
- package/dist/esm/interaction/InteractionServer.js +0 -1177
- package/dist/esm/interaction/InteractionServer.js.map +0 -6
- package/src/interaction/InteractionServer.ts +0 -1649
|
@@ -1,1649 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2022-2025 Matter.js Authors
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
Crypto,
|
|
9
|
-
Diagnostic,
|
|
10
|
-
ImplementationError,
|
|
11
|
-
InternalError,
|
|
12
|
-
Logger,
|
|
13
|
-
MatterError,
|
|
14
|
-
MatterFlowError,
|
|
15
|
-
Observable,
|
|
16
|
-
ServerAddressIp,
|
|
17
|
-
} from "#general";
|
|
18
|
-
import { AttributeModel, ClusterModel, CommandModel, GLOBAL_IDS, MatterModel, Specification } from "#model";
|
|
19
|
-
import { PeerAddress } from "#peer/PeerAddress.js";
|
|
20
|
-
import { SessionManager } from "#session/SessionManager.js";
|
|
21
|
-
import {
|
|
22
|
-
ArraySchema,
|
|
23
|
-
AttributeId,
|
|
24
|
-
ClusterId,
|
|
25
|
-
CommandId,
|
|
26
|
-
DEFAULT_MAX_PATHS_PER_INVOKE,
|
|
27
|
-
EndpointNumber,
|
|
28
|
-
EventId,
|
|
29
|
-
EventNumber,
|
|
30
|
-
INTERACTION_PROTOCOL_ID,
|
|
31
|
-
NodeId,
|
|
32
|
-
ReceivedStatusResponseError,
|
|
33
|
-
StatusCode,
|
|
34
|
-
StatusResponseError,
|
|
35
|
-
TlvAny,
|
|
36
|
-
TlvAttributePath,
|
|
37
|
-
TlvClusterPath,
|
|
38
|
-
TlvCommandPath,
|
|
39
|
-
TlvEventFilter,
|
|
40
|
-
TlvEventPath,
|
|
41
|
-
TlvInvokeResponseData,
|
|
42
|
-
TlvInvokeResponseForSend,
|
|
43
|
-
TlvNoArguments,
|
|
44
|
-
TlvNoResponse,
|
|
45
|
-
TlvSubscribeResponse,
|
|
46
|
-
TypeFromSchema,
|
|
47
|
-
ValidationError,
|
|
48
|
-
} from "#types";
|
|
49
|
-
import { AnyAttributeServer, AttributeServer, FabricScopedAttributeServer } from "../cluster/server/AttributeServer.js";
|
|
50
|
-
import { CommandServer } from "../cluster/server/CommandServer.js";
|
|
51
|
-
import { AnyEventServer } from "../cluster/server/EventServer.js";
|
|
52
|
-
import { Message, SessionType } from "../codec/MessageCodec.js";
|
|
53
|
-
import { EndpointInterface } from "../endpoint/EndpointInterface.js";
|
|
54
|
-
import { MessageExchange } from "../protocol/MessageExchange.js";
|
|
55
|
-
import { ProtocolHandler } from "../protocol/ProtocolHandler.js";
|
|
56
|
-
import { assertSecureSession, NoAssociatedFabricError, SecureSession } from "../session/SecureSession.js";
|
|
57
|
-
import {
|
|
58
|
-
decodeAttributeValueWithSchema,
|
|
59
|
-
decodeListAttributeValueWithSchema,
|
|
60
|
-
expandPathsInAttributeData,
|
|
61
|
-
} from "./AttributeDataDecoder.js";
|
|
62
|
-
import { DataReportPayloadIterator, EventReportPayload } from "./AttributeDataEncoder.js";
|
|
63
|
-
import { InteractionEndpointStructure } from "./InteractionEndpointStructure.js";
|
|
64
|
-
import {
|
|
65
|
-
DataReport,
|
|
66
|
-
InteractionRecipient,
|
|
67
|
-
InteractionServerMessenger,
|
|
68
|
-
InvokeRequest,
|
|
69
|
-
MessageType,
|
|
70
|
-
ReadRequest,
|
|
71
|
-
SubscribeRequest,
|
|
72
|
-
TimedRequest,
|
|
73
|
-
WriteRequest,
|
|
74
|
-
WriteResponse,
|
|
75
|
-
} from "./InteractionMessenger.js";
|
|
76
|
-
import { ServerSubscription, ServerSubscriptionContext } from "./ServerSubscription.js";
|
|
77
|
-
import { ServerSubscriptionConfig } from "./SubscriptionOptions.js";
|
|
78
|
-
|
|
79
|
-
const logger = Logger.get("InteractionServer");
|
|
80
|
-
|
|
81
|
-
export interface PeerSubscription {
|
|
82
|
-
subscriptionId: number;
|
|
83
|
-
peerAddress: PeerAddress;
|
|
84
|
-
minIntervalFloorSeconds: number;
|
|
85
|
-
maxIntervalCeilingSeconds: number;
|
|
86
|
-
attributeRequests?: TypeFromSchema<typeof TlvAttributePath>[];
|
|
87
|
-
eventRequests?: TypeFromSchema<typeof TlvEventPath>[];
|
|
88
|
-
isFabricFiltered: boolean;
|
|
89
|
-
maxInterval: number;
|
|
90
|
-
sendInterval: number;
|
|
91
|
-
operationalAddress?: ServerAddressIp;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface CommandPath {
|
|
95
|
-
nodeId?: NodeId;
|
|
96
|
-
endpointId: EndpointNumber;
|
|
97
|
-
clusterId: ClusterId;
|
|
98
|
-
commandId: CommandId;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface AttributePath {
|
|
102
|
-
nodeId?: NodeId;
|
|
103
|
-
endpointId: EndpointNumber;
|
|
104
|
-
clusterId: ClusterId;
|
|
105
|
-
attributeId: AttributeId;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface EventPath {
|
|
109
|
-
nodeId?: NodeId;
|
|
110
|
-
endpointId: EndpointNumber;
|
|
111
|
-
clusterId: ClusterId;
|
|
112
|
-
eventId: EventId;
|
|
113
|
-
isUrgent?: boolean;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface AttributeWithPath {
|
|
117
|
-
path: AttributePath;
|
|
118
|
-
attribute: AnyAttributeServer<any>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface EventWithPath {
|
|
122
|
-
path: EventPath;
|
|
123
|
-
event: AnyEventServer<any, any>;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface CommandWithPath {
|
|
127
|
-
path: CommandPath;
|
|
128
|
-
command: CommandServer<any, any>;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function genericElementPathToId(
|
|
132
|
-
endpointId: EndpointNumber | undefined,
|
|
133
|
-
clusterId: ClusterId | undefined,
|
|
134
|
-
elementId: number | undefined,
|
|
135
|
-
) {
|
|
136
|
-
return `${endpointId}/${clusterId}/${elementId}`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function commandPathToId({ endpointId, clusterId, commandId }: CommandPath) {
|
|
140
|
-
return genericElementPathToId(endpointId, clusterId, commandId);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function attributePathToId({ endpointId, clusterId, attributeId }: TypeFromSchema<typeof TlvAttributePath>) {
|
|
144
|
-
return genericElementPathToId(endpointId, clusterId, attributeId);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function eventPathToId({ endpointId, clusterId, eventId }: TypeFromSchema<typeof TlvEventPath>) {
|
|
148
|
-
return genericElementPathToId(endpointId, clusterId, eventId);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function clusterPathToId({ nodeId, endpointId, clusterId }: TypeFromSchema<typeof TlvClusterPath>) {
|
|
152
|
-
return `${nodeId}/${endpointId}/${clusterId}`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function isConcreteAttributePath(
|
|
156
|
-
path: TypeFromSchema<typeof TlvAttributePath>,
|
|
157
|
-
): path is TypeFromSchema<typeof TlvAttributePath> & AttributePath {
|
|
158
|
-
const { endpointId, clusterId, attributeId } = path;
|
|
159
|
-
return endpointId !== undefined && clusterId !== undefined && attributeId !== undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function validateReadAttributesPath(path: TypeFromSchema<typeof TlvAttributePath>, isGroupSession = false) {
|
|
163
|
-
if (isGroupSession) {
|
|
164
|
-
throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
|
|
165
|
-
}
|
|
166
|
-
const { clusterId, attributeId } = path;
|
|
167
|
-
if (clusterId === undefined && attributeId !== undefined) {
|
|
168
|
-
if (!GLOBAL_IDS.has(attributeId)) {
|
|
169
|
-
throw new StatusResponseError(
|
|
170
|
-
`Illegal read request for wildcard cluster and non global attribute ${attributeId}`,
|
|
171
|
-
StatusCode.InvalidAction,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function validateWriteAttributesPath(path: TypeFromSchema<typeof TlvAttributePath>, isGroupSession = false) {
|
|
178
|
-
const { endpointId, clusterId, attributeId } = path;
|
|
179
|
-
if (clusterId === undefined || attributeId === undefined) {
|
|
180
|
-
throw new StatusResponseError(
|
|
181
|
-
"Illegal write request with wildcard cluster or attribute ID",
|
|
182
|
-
StatusCode.InvalidAction,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
if (isGroupSession && endpointId !== undefined) {
|
|
186
|
-
throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function isConcreteEventPath(
|
|
191
|
-
path: TypeFromSchema<typeof TlvEventPath>,
|
|
192
|
-
): path is TypeFromSchema<typeof TlvEventPath> & EventPath {
|
|
193
|
-
const { endpointId, clusterId, eventId } = path;
|
|
194
|
-
return endpointId !== undefined && clusterId !== undefined && eventId !== undefined;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function validateReadEventPath(path: TypeFromSchema<typeof TlvEventPath>, isGroupSession = false) {
|
|
198
|
-
const { clusterId, eventId } = path;
|
|
199
|
-
if (clusterId === undefined && eventId !== undefined) {
|
|
200
|
-
throw new StatusResponseError("Illegal read request with wildcard cluster ID", StatusCode.InvalidAction);
|
|
201
|
-
}
|
|
202
|
-
if (isGroupSession) {
|
|
203
|
-
throw new StatusResponseError("Illegal read request with group session", StatusCode.InvalidAction);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function isConcreteCommandPath(
|
|
208
|
-
path: TypeFromSchema<typeof TlvCommandPath>,
|
|
209
|
-
): path is TypeFromSchema<typeof TlvCommandPath> & CommandPath {
|
|
210
|
-
const { endpointId, clusterId, commandId } = path;
|
|
211
|
-
return endpointId !== undefined && clusterId !== undefined && commandId !== undefined;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function validateCommandPath(path: TypeFromSchema<typeof TlvCommandPath>, isGroupSession = false) {
|
|
215
|
-
const { endpointId, clusterId, commandId } = path;
|
|
216
|
-
if (clusterId === undefined || commandId === undefined) {
|
|
217
|
-
throw new StatusResponseError(
|
|
218
|
-
"Illegal write request with wildcard cluster or attribute ID",
|
|
219
|
-
StatusCode.InvalidAction,
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
if (isGroupSession && endpointId !== undefined) {
|
|
223
|
-
throw new StatusResponseError("Illegal write request with group ID and endpoint ID", StatusCode.InvalidAction);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function getMatterModelCluster(clusterId: ClusterId) {
|
|
228
|
-
return MatterModel.standard.get(ClusterModel, clusterId);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function getMatterModelClusterAttribute(clusterId: ClusterId, attributeId: AttributeId) {
|
|
232
|
-
return getMatterModelCluster(clusterId)?.get(AttributeModel, attributeId);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function getMatterModelClusterCommand(clusterId: ClusterId, commandId: CommandId) {
|
|
236
|
-
return getMatterModelCluster(clusterId)?.get(CommandModel, commandId);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Interfaces {@link InteractionServer} with other components.
|
|
241
|
-
*/
|
|
242
|
-
export interface InteractionContext {
|
|
243
|
-
readonly sessions: SessionManager;
|
|
244
|
-
readonly structure: InteractionEndpointStructure;
|
|
245
|
-
readonly subscriptionOptions?: Partial<ServerSubscriptionConfig>;
|
|
246
|
-
readonly maxPathsPerInvoke?: number;
|
|
247
|
-
initiateExchange(address: PeerAddress, protocolId: number): MessageExchange;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Translates interactions from the Matter protocol to matter.js APIs.
|
|
252
|
-
*/
|
|
253
|
-
export class InteractionServer implements ProtocolHandler, InteractionRecipient {
|
|
254
|
-
readonly id = INTERACTION_PROTOCOL_ID;
|
|
255
|
-
#context: InteractionContext;
|
|
256
|
-
#nextSubscriptionId = Crypto.getRandomUInt32();
|
|
257
|
-
#isClosing = false;
|
|
258
|
-
#clientHandler?: ProtocolHandler;
|
|
259
|
-
readonly #subscriptionConfig: ServerSubscriptionConfig;
|
|
260
|
-
readonly #maxPathsPerInvoke;
|
|
261
|
-
readonly #subscriptionEstablishmentStarted = Observable<[peerAddress: PeerAddress]>();
|
|
262
|
-
|
|
263
|
-
constructor(context: InteractionContext) {
|
|
264
|
-
this.#context = context;
|
|
265
|
-
|
|
266
|
-
this.#subscriptionConfig = ServerSubscriptionConfig.of(context.subscriptionOptions);
|
|
267
|
-
this.#maxPathsPerInvoke = context.maxPathsPerInvoke ?? DEFAULT_MAX_PATHS_PER_INVOKE;
|
|
268
|
-
|
|
269
|
-
this.#context.structure.change.on(async () => {
|
|
270
|
-
this.#context.sessions.updateAllSubscriptions();
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
protected get isClosing() {
|
|
275
|
-
return this.#isClosing;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
get maxPathsPerInvoke() {
|
|
279
|
-
return this.#maxPathsPerInvoke;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
get subscriptionEstablishmentStarted() {
|
|
283
|
-
return this.#subscriptionEstablishmentStarted;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async onNewExchange(exchange: MessageExchange, message: Message) {
|
|
287
|
-
// Note - changes here must be copied to TransactionalInteractionServer as it does not call super() to avoid
|
|
288
|
-
// the stack frame
|
|
289
|
-
if (this.#isClosing) return; // We are closing, ignore anything newly incoming
|
|
290
|
-
|
|
291
|
-
// An incoming data report as the first message is not a valid server operation. We instead delegate to a
|
|
292
|
-
// client implementation if available
|
|
293
|
-
if (message.payloadHeader.messageType === MessageType.SubscribeRequest && this.#clientHandler) {
|
|
294
|
-
return this.#clientHandler.onNewExchange(exchange, message);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
await new InteractionServerMessenger(exchange).handleRequest(this);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
get clientHandler(): ProtocolHandler | undefined {
|
|
301
|
-
return this.#clientHandler;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
set clientHandler(clientHandler: ProtocolHandler) {
|
|
305
|
-
this.#clientHandler = clientHandler;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async #collectEventDataForRead(
|
|
309
|
-
{ eventRequests, eventFilters, isFabricFiltered }: ReadRequest,
|
|
310
|
-
exchange: MessageExchange,
|
|
311
|
-
message: Message,
|
|
312
|
-
) {
|
|
313
|
-
let eventReportsPayload: undefined | EventReportPayload[];
|
|
314
|
-
if (eventRequests) {
|
|
315
|
-
eventReportsPayload = [];
|
|
316
|
-
for (const requestPath of eventRequests) {
|
|
317
|
-
validateReadEventPath(requestPath);
|
|
318
|
-
|
|
319
|
-
const events = this.#endpointStructure.getEvents([requestPath]);
|
|
320
|
-
|
|
321
|
-
// Requested event path not found in any cluster server on any endpoint
|
|
322
|
-
if (events.length === 0) {
|
|
323
|
-
if (isConcreteEventPath(requestPath)) {
|
|
324
|
-
const { endpointId, clusterId, eventId } = requestPath;
|
|
325
|
-
try {
|
|
326
|
-
this.#endpointStructure.validateConcreteEventPath(endpointId, clusterId, eventId);
|
|
327
|
-
throw new InternalError(
|
|
328
|
-
"validateConcreteEventPath should throw StatusResponseError but did not.",
|
|
329
|
-
);
|
|
330
|
-
} catch (e) {
|
|
331
|
-
StatusResponseError.accept(e);
|
|
332
|
-
|
|
333
|
-
logger.debug(
|
|
334
|
-
`Read event from ${
|
|
335
|
-
exchange.channel.name
|
|
336
|
-
}: ${this.#endpointStructure.resolveEventName(requestPath)}: unsupported path: Status=${
|
|
337
|
-
e.code
|
|
338
|
-
}`,
|
|
339
|
-
);
|
|
340
|
-
eventReportsPayload?.push({
|
|
341
|
-
hasFabricSensitiveData: false,
|
|
342
|
-
eventStatus: { path: requestPath, status: { status: e.code } },
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Wildcard path: Just leave out values
|
|
347
|
-
logger.debug(
|
|
348
|
-
`Read event from ${exchange.channel.name}: ${this.#endpointStructure.resolveEventName(
|
|
349
|
-
requestPath,
|
|
350
|
-
)}: ignore non-existing event`,
|
|
351
|
-
);
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const reportsForPath = new Array<EventReportPayload>();
|
|
356
|
-
for (const { path, event } of events) {
|
|
357
|
-
try {
|
|
358
|
-
const matchingEvents = await this.readEvent(
|
|
359
|
-
path,
|
|
360
|
-
eventFilters,
|
|
361
|
-
event,
|
|
362
|
-
exchange,
|
|
363
|
-
isFabricFiltered,
|
|
364
|
-
message,
|
|
365
|
-
);
|
|
366
|
-
logger.debug(
|
|
367
|
-
`Read event from ${exchange.channel.name}: ${this.#endpointStructure.resolveEventName(
|
|
368
|
-
path,
|
|
369
|
-
)}=${Diagnostic.json(matchingEvents)}`,
|
|
370
|
-
);
|
|
371
|
-
const { schema } = event;
|
|
372
|
-
reportsForPath.push(
|
|
373
|
-
...matchingEvents.map(({ number, priority, epochTimestamp, payload }) => ({
|
|
374
|
-
hasFabricSensitiveData: event.hasFabricSensitiveData,
|
|
375
|
-
eventData: {
|
|
376
|
-
path,
|
|
377
|
-
eventNumber: number,
|
|
378
|
-
priority,
|
|
379
|
-
epochTimestamp,
|
|
380
|
-
payload,
|
|
381
|
-
schema,
|
|
382
|
-
},
|
|
383
|
-
})),
|
|
384
|
-
);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
logger.error(
|
|
387
|
-
`Error while reading event from ${
|
|
388
|
-
exchange.channel.name
|
|
389
|
-
} to ${this.#endpointStructure.resolveEventName(path)}:`,
|
|
390
|
-
error,
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
StatusResponseError.accept(error);
|
|
394
|
-
|
|
395
|
-
// Add StatusResponseErrors, but only when the initial path was concrete, else error are ignored
|
|
396
|
-
if (isConcreteEventPath(requestPath)) {
|
|
397
|
-
eventReportsPayload?.push({
|
|
398
|
-
hasFabricSensitiveData: false,
|
|
399
|
-
eventStatus: { path, status: { status: error.code } },
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
eventReportsPayload.push(
|
|
405
|
-
...reportsForPath.sort((a, b) => {
|
|
406
|
-
const eventNumberA = a.eventData?.eventNumber ?? EventNumber(0);
|
|
407
|
-
const eventNumberB = b.eventData?.eventNumber ?? EventNumber(0);
|
|
408
|
-
if (eventNumberA > eventNumberB) {
|
|
409
|
-
return 1;
|
|
410
|
-
} else if (eventNumberA < eventNumberB) {
|
|
411
|
-
return -1;
|
|
412
|
-
} else {
|
|
413
|
-
return 0;
|
|
414
|
-
}
|
|
415
|
-
}),
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
return eventReportsPayload;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Returns an iterator that yields the data reports and events data for the given read request.
|
|
424
|
-
*/
|
|
425
|
-
*#iterateReadAttributesPaths(
|
|
426
|
-
{ attributeRequests, dataVersionFilters, isFabricFiltered }: ReadRequest,
|
|
427
|
-
eventReportsPayload: EventReportPayload[] | undefined,
|
|
428
|
-
exchange: MessageExchange,
|
|
429
|
-
message: Message,
|
|
430
|
-
) {
|
|
431
|
-
const dataVersionFilterMap = new Map<string, number>(
|
|
432
|
-
dataVersionFilters?.map(({ path, dataVersion }) => [clusterPathToId(path), dataVersion]) ?? [],
|
|
433
|
-
);
|
|
434
|
-
if (dataVersionFilterMap.size > 0) {
|
|
435
|
-
logger.debug(
|
|
436
|
-
`DataVersionFilters: ${Array.from(dataVersionFilterMap.entries())
|
|
437
|
-
.map(([path, version]) => `${path}=${version}`)
|
|
438
|
-
.join(", ")}`,
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
for (const requestPath of attributeRequests ?? []) {
|
|
443
|
-
validateReadAttributesPath(requestPath);
|
|
444
|
-
|
|
445
|
-
const attributes = this.#endpointStructure.getAttributes([requestPath]);
|
|
446
|
-
|
|
447
|
-
// Requested attribute path not found in any cluster server on any endpoint
|
|
448
|
-
if (attributes.length === 0) {
|
|
449
|
-
// TODO Add checks for nodeId -> UnknownNode
|
|
450
|
-
if (isConcreteAttributePath(requestPath)) {
|
|
451
|
-
const { endpointId, clusterId, attributeId } = requestPath;
|
|
452
|
-
// Concrete path, but still unknown for us, so generate the right error status
|
|
453
|
-
try {
|
|
454
|
-
this.#endpointStructure.validateConcreteAttributePath(endpointId, clusterId, attributeId);
|
|
455
|
-
throw new InternalError(
|
|
456
|
-
"validateConcreteAttributePath should throw StatusResponseError but did not.",
|
|
457
|
-
);
|
|
458
|
-
} catch (e) {
|
|
459
|
-
StatusResponseError.accept(e);
|
|
460
|
-
logger.debug(
|
|
461
|
-
`Error reading attribute from ${
|
|
462
|
-
exchange.channel.name
|
|
463
|
-
}: ${this.#endpointStructure.resolveAttributeName(requestPath)}: unsupported path: Status=${
|
|
464
|
-
e.code
|
|
465
|
-
}`,
|
|
466
|
-
);
|
|
467
|
-
yield {
|
|
468
|
-
hasFabricSensitiveData: false,
|
|
469
|
-
attributeStatus: { path: requestPath, status: { status: e.code } },
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Wildcard path and we do not know any of the attributes: Ignore the error
|
|
475
|
-
logger.debug(
|
|
476
|
-
`Read from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
|
|
477
|
-
requestPath,
|
|
478
|
-
)}: ${this.#endpointStructure.resolveAttributeName(requestPath)}: ignore non-existing attribute`,
|
|
479
|
-
);
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Process all known attributes for the given path
|
|
484
|
-
for (const { path, attribute } of attributes) {
|
|
485
|
-
const { nodeId, endpointId, clusterId } = path;
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
// We accept attributes not in the model ans readable, because existence is checked already
|
|
489
|
-
if (getMatterModelClusterAttribute(clusterId, attribute.id)?.readable === false) {
|
|
490
|
-
throw new StatusResponseError(
|
|
491
|
-
`Attribute ${attribute.id} is not readable.`,
|
|
492
|
-
StatusCode.UnsupportedRead,
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
let value, version;
|
|
497
|
-
try {
|
|
498
|
-
// TODO Optimize read later here too to optimize context usage
|
|
499
|
-
({ value, version } = this.readAttribute(path, attribute, exchange, isFabricFiltered, message));
|
|
500
|
-
} catch (e) {
|
|
501
|
-
NoAssociatedFabricError.accept(e);
|
|
502
|
-
|
|
503
|
-
// TODO: Remove when we remove legacy API
|
|
504
|
-
// This is not fully correct but should be sufficient for now
|
|
505
|
-
// This is fixed in the new API already, so this error should never throw
|
|
506
|
-
// Fabric scoped attributes are access errors, fabric sensitive attributes are just filtered
|
|
507
|
-
// Assume for now that in this place we only need to handle fabric sensitive case
|
|
508
|
-
if (endpointId === undefined || clusterId === undefined) {
|
|
509
|
-
throw new MatterFlowError("Should never happen");
|
|
510
|
-
}
|
|
511
|
-
const cluster = this.#endpointStructure.getClusterServer(endpointId, clusterId);
|
|
512
|
-
if (cluster === undefined || cluster.datasource == undefined) {
|
|
513
|
-
throw new MatterFlowError("Should never happen");
|
|
514
|
-
}
|
|
515
|
-
version = cluster.datasource.version;
|
|
516
|
-
value = [];
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const versionFilterValue =
|
|
520
|
-
endpointId !== undefined && clusterId !== undefined
|
|
521
|
-
? dataVersionFilterMap.get(clusterPathToId({ nodeId, endpointId, clusterId }))
|
|
522
|
-
: undefined;
|
|
523
|
-
if (versionFilterValue !== undefined && versionFilterValue === version) {
|
|
524
|
-
logger.debug(
|
|
525
|
-
`Read attribute from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
|
|
526
|
-
path,
|
|
527
|
-
)}=${Diagnostic.json(value)} (version=${version}) ignored because of dataVersionFilter`,
|
|
528
|
-
);
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
logger.debug(
|
|
533
|
-
`Read attribute from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
|
|
534
|
-
path,
|
|
535
|
-
)}=${Diagnostic.json(value)} (version=${version})`,
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
const { schema } = attribute;
|
|
539
|
-
yield {
|
|
540
|
-
hasFabricSensitiveData: attribute.hasFabricSensitiveData,
|
|
541
|
-
attributeData: { path, dataVersion: version, payload: value, schema },
|
|
542
|
-
};
|
|
543
|
-
} catch (error) {
|
|
544
|
-
const what = `reading ${this.#endpointStructure.resolveAttributeName(path)} from ${exchange.channel.name}`;
|
|
545
|
-
|
|
546
|
-
if (!(error instanceof StatusResponseError)) {
|
|
547
|
-
const wrappedError = new ImplementationError(`Unhandled error ${what}`);
|
|
548
|
-
wrappedError.cause = error;
|
|
549
|
-
throw wrappedError;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
logger.error(`Error ${what}:`, error.message);
|
|
553
|
-
|
|
554
|
-
// Add StatusResponseErrors, but only when the initial path was concrete, else error are ignored
|
|
555
|
-
if (isConcreteAttributePath(requestPath)) {
|
|
556
|
-
yield {
|
|
557
|
-
hasFabricSensitiveData: false,
|
|
558
|
-
attributeStatus: { path, status: { status: error.code } },
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (eventReportsPayload !== undefined) {
|
|
566
|
-
for (const eventReport of eventReportsPayload) {
|
|
567
|
-
yield eventReport;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
async handleReadRequest(
|
|
573
|
-
exchange: MessageExchange,
|
|
574
|
-
readRequest: ReadRequest,
|
|
575
|
-
message: Message,
|
|
576
|
-
): Promise<{ dataReport: DataReport; payload?: DataReportPayloadIterator }> {
|
|
577
|
-
const { attributeRequests, eventRequests, isFabricFiltered, interactionModelRevision } = readRequest;
|
|
578
|
-
logger.debug(
|
|
579
|
-
`Received read request from ${exchange.channel.name}: attributes:${
|
|
580
|
-
attributeRequests?.map(path => this.#endpointStructure.resolveAttributeName(path)).join(", ") ?? "none"
|
|
581
|
-
}, events:${
|
|
582
|
-
eventRequests?.map(path => this.#endpointStructure.resolveEventName(path)).join(", ") ?? "none"
|
|
583
|
-
} isFabricFiltered=${isFabricFiltered}`,
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
587
|
-
logger.debug(
|
|
588
|
-
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
if (attributeRequests === undefined && eventRequests === undefined) {
|
|
592
|
-
return {
|
|
593
|
-
dataReport: {
|
|
594
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
595
|
-
suppressResponse: true,
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (message.packetHeader.sessionType !== SessionType.Unicast) {
|
|
601
|
-
throw new StatusResponseError(
|
|
602
|
-
"Subscriptions are only allowed on unicast sessions",
|
|
603
|
-
StatusCode.InvalidAction,
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return {
|
|
608
|
-
dataReport: {
|
|
609
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
610
|
-
suppressResponse: true,
|
|
611
|
-
},
|
|
612
|
-
payload: this.#iterateReadAttributesPaths(
|
|
613
|
-
readRequest,
|
|
614
|
-
await this.#collectEventDataForRead(readRequest, exchange, message),
|
|
615
|
-
exchange,
|
|
616
|
-
message,
|
|
617
|
-
),
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
protected readAttribute(
|
|
622
|
-
_path: AttributePath,
|
|
623
|
-
attribute: AnyAttributeServer<any>,
|
|
624
|
-
exchange: MessageExchange,
|
|
625
|
-
isFabricFiltered: boolean,
|
|
626
|
-
message: Message,
|
|
627
|
-
offline = false,
|
|
628
|
-
) {
|
|
629
|
-
return attribute.getWithVersion(exchange.session, isFabricFiltered, offline ? undefined : message);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Reads the attributes for the given endpoint.
|
|
634
|
-
* This can currently only be used for subscriptions because errors are ignored!
|
|
635
|
-
*/
|
|
636
|
-
protected readEndpointAttributesForSubscription(
|
|
637
|
-
attributes: { path: AttributePath; attribute: AnyAttributeServer<any> }[],
|
|
638
|
-
exchange: MessageExchange,
|
|
639
|
-
isFabricFiltered: boolean,
|
|
640
|
-
message: Message,
|
|
641
|
-
offline = false,
|
|
642
|
-
) {
|
|
643
|
-
const result = new Array<{
|
|
644
|
-
path: AttributePath;
|
|
645
|
-
attribute: AnyAttributeServer<unknown>;
|
|
646
|
-
value: any;
|
|
647
|
-
version: number;
|
|
648
|
-
}>();
|
|
649
|
-
for (const { path, attribute } of attributes) {
|
|
650
|
-
try {
|
|
651
|
-
const { version, value } = this.readAttribute(
|
|
652
|
-
path,
|
|
653
|
-
attribute,
|
|
654
|
-
exchange,
|
|
655
|
-
isFabricFiltered,
|
|
656
|
-
message,
|
|
657
|
-
offline,
|
|
658
|
-
);
|
|
659
|
-
result.push({ path, value, version, attribute });
|
|
660
|
-
} catch (error) {
|
|
661
|
-
if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
|
|
662
|
-
logger.warn(
|
|
663
|
-
`Permission denied reading attribute ${this.#endpointStructure.resolveAttributeName(path)}`,
|
|
664
|
-
);
|
|
665
|
-
} else {
|
|
666
|
-
logger.warn(
|
|
667
|
-
`Error reading attribute ${this.#endpointStructure.resolveAttributeName(path)}:`,
|
|
668
|
-
error,
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
return result;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
protected async readEvent(
|
|
677
|
-
_path: EventPath,
|
|
678
|
-
eventFilters: TypeFromSchema<typeof TlvEventFilter>[] | undefined,
|
|
679
|
-
event: AnyEventServer<any, any>,
|
|
680
|
-
exchange: MessageExchange,
|
|
681
|
-
isFabricFiltered: boolean,
|
|
682
|
-
message: Message,
|
|
683
|
-
) {
|
|
684
|
-
return event.get(exchange.session, isFabricFiltered, message, eventFilters);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
async handleWriteRequest(
|
|
688
|
-
exchange: MessageExchange,
|
|
689
|
-
{ suppressResponse, timedRequest, writeRequests, interactionModelRevision, moreChunkedMessages }: WriteRequest,
|
|
690
|
-
message: Message,
|
|
691
|
-
): Promise<WriteResponse> {
|
|
692
|
-
const sessionType = message.packetHeader.sessionType;
|
|
693
|
-
logger.debug(
|
|
694
|
-
`Received write request from ${exchange.channel.name}: ${writeRequests
|
|
695
|
-
.map(req => this.#endpointStructure.resolveAttributeName(req.path))
|
|
696
|
-
.join(", ")}, suppressResponse=${suppressResponse}, moreChunkedMessages=${moreChunkedMessages}`,
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
if (moreChunkedMessages && suppressResponse) {
|
|
700
|
-
throw new StatusResponseError(
|
|
701
|
-
"MoreChunkedMessages and SuppressResponse cannot be used together in write messages",
|
|
702
|
-
StatusCode.InvalidAction,
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
707
|
-
logger.debug(
|
|
708
|
-
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
|
|
713
|
-
|
|
714
|
-
if (receivedWithinTimedInteraction && moreChunkedMessages) {
|
|
715
|
-
throw new StatusResponseError(
|
|
716
|
-
"Write Request action that is part of a Timed Write Interaction SHALL NOT be chunked.",
|
|
717
|
-
StatusCode.InvalidAction,
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
if (exchange.hasExpiredTimedInteraction()) {
|
|
722
|
-
exchange.clearTimedInteraction(); // ??
|
|
723
|
-
throw new StatusResponseError(`Timed request window expired. Decline write request.`, StatusCode.Timeout);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
if (timedRequest !== exchange.hasTimedInteraction()) {
|
|
727
|
-
throw new StatusResponseError(
|
|
728
|
-
`timedRequest flag of write interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
|
|
729
|
-
StatusCode.TimedRequestMismatch,
|
|
730
|
-
);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (receivedWithinTimedInteraction) {
|
|
734
|
-
logger.debug(
|
|
735
|
-
`Write request from ${exchange.channel.name} successfully received while timed interaction is running.`,
|
|
736
|
-
);
|
|
737
|
-
exchange.clearTimedInteraction();
|
|
738
|
-
if (sessionType !== SessionType.Unicast) {
|
|
739
|
-
throw new StatusResponseError(
|
|
740
|
-
"Write requests are only allowed on unicast sessions when a timed interaction is running.",
|
|
741
|
-
StatusCode.InvalidAction,
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if (sessionType === SessionType.Group && !suppressResponse) {
|
|
747
|
-
throw new StatusResponseError(
|
|
748
|
-
"Write requests are only allowed as group casts when suppressResponse=true.",
|
|
749
|
-
StatusCode.InvalidAction,
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
const writeData = expandPathsInAttributeData(writeRequests, true);
|
|
754
|
-
|
|
755
|
-
const writeResults = new Array<{
|
|
756
|
-
path: TypeFromSchema<typeof TlvAttributePath>;
|
|
757
|
-
statusCode: StatusCode;
|
|
758
|
-
clusterStatusCode?: number;
|
|
759
|
-
}>();
|
|
760
|
-
const attributeListWrites = new Set<AttributeServer<any>>();
|
|
761
|
-
const clusterDataVersionInfo = new Map<string, number>();
|
|
762
|
-
const inaccessiblePaths = new Set<string>();
|
|
763
|
-
|
|
764
|
-
// TODO Add handling for moreChunkedMessages here when adopting for Matter 1.4
|
|
765
|
-
|
|
766
|
-
for (const writeRequest of writeData) {
|
|
767
|
-
const { path: writePath, dataVersion } = writeRequest;
|
|
768
|
-
|
|
769
|
-
validateWriteAttributesPath(writePath);
|
|
770
|
-
|
|
771
|
-
const attributes = this.#endpointStructure.getAttributes([writePath], true);
|
|
772
|
-
|
|
773
|
-
// No existing attribute matches the given path and is writable
|
|
774
|
-
if (attributes.length === 0) {
|
|
775
|
-
if (isConcreteAttributePath(writePath)) {
|
|
776
|
-
const { endpointId, clusterId, attributeId } = writePath;
|
|
777
|
-
|
|
778
|
-
// was a concrete path
|
|
779
|
-
try {
|
|
780
|
-
this.#endpointStructure.validateConcreteAttributePath(endpointId, clusterId, attributeId);
|
|
781
|
-
|
|
782
|
-
// Ok it is a valid concrete path, so it is not writable
|
|
783
|
-
throw new StatusResponseError(
|
|
784
|
-
`Attribute ${attributeId} is not writable.`,
|
|
785
|
-
StatusCode.UnsupportedWrite,
|
|
786
|
-
);
|
|
787
|
-
} catch (e) {
|
|
788
|
-
StatusResponseError.accept(e);
|
|
789
|
-
|
|
790
|
-
logger.debug(
|
|
791
|
-
`Write from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
|
|
792
|
-
writePath,
|
|
793
|
-
)} not allowed: Status=${e.code}`,
|
|
794
|
-
);
|
|
795
|
-
writeResults.push({ path: writePath, statusCode: e.code });
|
|
796
|
-
}
|
|
797
|
-
} else {
|
|
798
|
-
// Wildcard path: Just ignore
|
|
799
|
-
logger.debug(
|
|
800
|
-
`Write from ${exchange.channel.name}: ${this.#endpointStructure.resolveAttributeName(
|
|
801
|
-
writePath,
|
|
802
|
-
)}: ignore non-existing (wildcard) attribute`,
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Concrete path and found and writable
|
|
809
|
-
if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
|
|
810
|
-
const { endpointId, clusterId } = writePath;
|
|
811
|
-
const { attribute } = attributes[0];
|
|
812
|
-
|
|
813
|
-
if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
|
|
814
|
-
logger.debug(`This write requires a timed interaction which is not initialized.`);
|
|
815
|
-
writeResults.push({ path: writePath, statusCode: StatusCode.NeedsTimedInteraction });
|
|
816
|
-
continue;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (
|
|
820
|
-
attribute instanceof FabricScopedAttributeServer &&
|
|
821
|
-
(!exchange.session.isSecure || !(exchange.session as SecureSession).fabric)
|
|
822
|
-
) {
|
|
823
|
-
logger.debug(`This write requires a secure session with a fabric assigned which is missing.`);
|
|
824
|
-
writeResults.push({ path: writePath, statusCode: StatusCode.UnsupportedAccess });
|
|
825
|
-
continue;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
// Check the provided dataVersion with the dataVersion of the cluster
|
|
829
|
-
// And remember this initial dataVersion for all checks inside this write transaction to allow proper
|
|
830
|
-
// processing of chunked lists
|
|
831
|
-
if (dataVersion !== undefined) {
|
|
832
|
-
const datasource = this.#endpointStructure.getClusterServer(endpointId, clusterId)?.datasource;
|
|
833
|
-
const { nodeId } = writePath;
|
|
834
|
-
const clusterKey = clusterPathToId({ nodeId, endpointId, clusterId });
|
|
835
|
-
const currentDataVersion = clusterDataVersionInfo.get(clusterKey) ?? datasource?.version;
|
|
836
|
-
|
|
837
|
-
if (currentDataVersion !== undefined) {
|
|
838
|
-
if (dataVersion !== currentDataVersion) {
|
|
839
|
-
logger.debug(
|
|
840
|
-
`This write requires a specific data version (${dataVersion}) which do not match the current cluster data version (${currentDataVersion}).`,
|
|
841
|
-
);
|
|
842
|
-
writeResults.push({ path: writePath, statusCode: StatusCode.DataVersionMismatch });
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
|
-
clusterDataVersionInfo.set(clusterKey, currentDataVersion);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
for (const { path, attribute } of attributes) {
|
|
851
|
-
const { schema, defaultValue } = attribute;
|
|
852
|
-
const pathId = attributePathToId(path);
|
|
853
|
-
|
|
854
|
-
try {
|
|
855
|
-
if (
|
|
856
|
-
!(attribute instanceof AttributeServer) &&
|
|
857
|
-
!(attribute instanceof FabricScopedAttributeServer)
|
|
858
|
-
) {
|
|
859
|
-
throw new StatusResponseError(
|
|
860
|
-
"Fixed attributes cannot be written",
|
|
861
|
-
StatusCode.UnsupportedWrite,
|
|
862
|
-
);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (inaccessiblePaths.has(pathId)) {
|
|
866
|
-
logger.debug(`This write is not allowed due to previous access denied.`);
|
|
867
|
-
continue;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
const { endpointId } = path;
|
|
871
|
-
const { listIndex } = writePath;
|
|
872
|
-
const value =
|
|
873
|
-
listIndex === undefined
|
|
874
|
-
? decodeAttributeValueWithSchema(schema, [writeRequest], defaultValue)
|
|
875
|
-
: decodeListAttributeValueWithSchema(
|
|
876
|
-
schema as ArraySchema<any>,
|
|
877
|
-
[writeRequest],
|
|
878
|
-
this.readAttribute(path, attribute, exchange, true, message).value ?? defaultValue,
|
|
879
|
-
);
|
|
880
|
-
logger.debug(
|
|
881
|
-
`Handle write request from ${
|
|
882
|
-
exchange.channel.name
|
|
883
|
-
} resolved to: ${this.#endpointStructure.resolveAttributeName(path)}=${Diagnostic.json(
|
|
884
|
-
value,
|
|
885
|
-
)} (listIndex=${listIndex}, for-version=${dataVersion})`,
|
|
886
|
-
);
|
|
887
|
-
|
|
888
|
-
if (attribute.requiresTimedInteraction && !receivedWithinTimedInteraction) {
|
|
889
|
-
logger.debug(`This write requires a timed interaction which is not initialized.`);
|
|
890
|
-
throw new StatusResponseError(
|
|
891
|
-
"This write requires a timed interaction which is not initialized.",
|
|
892
|
-
StatusCode.NeedsTimedInteraction,
|
|
893
|
-
);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
await this.writeAttribute(
|
|
897
|
-
path,
|
|
898
|
-
attribute,
|
|
899
|
-
value,
|
|
900
|
-
exchange,
|
|
901
|
-
message,
|
|
902
|
-
this.#endpointStructure.getEndpoint(endpointId)!,
|
|
903
|
-
receivedWithinTimedInteraction,
|
|
904
|
-
schema instanceof ArraySchema,
|
|
905
|
-
);
|
|
906
|
-
if (schema instanceof ArraySchema && !attributeListWrites.has(attribute)) {
|
|
907
|
-
attributeListWrites.add(attribute);
|
|
908
|
-
}
|
|
909
|
-
} catch (error: any) {
|
|
910
|
-
if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
|
|
911
|
-
inaccessiblePaths.add(pathId);
|
|
912
|
-
}
|
|
913
|
-
if (attributes.length === 1 && isConcreteAttributePath(writePath)) {
|
|
914
|
-
// For Multi-Attribute-Writes we ignore errors
|
|
915
|
-
logger.error(
|
|
916
|
-
`Error while handling write request from ${
|
|
917
|
-
exchange.channel.name
|
|
918
|
-
} to ${this.#endpointStructure.resolveAttributeName(path)}:`,
|
|
919
|
-
error instanceof StatusResponseError ? error.message : error,
|
|
920
|
-
);
|
|
921
|
-
if (error instanceof StatusResponseError) {
|
|
922
|
-
writeResults.push({ path, statusCode: error.code, clusterStatusCode: error.clusterCode });
|
|
923
|
-
continue;
|
|
924
|
-
}
|
|
925
|
-
writeResults.push({ path, statusCode: StatusCode.ConstraintError });
|
|
926
|
-
continue;
|
|
927
|
-
} else {
|
|
928
|
-
logger.debug(
|
|
929
|
-
`While handling write request from ${
|
|
930
|
-
exchange.channel.name
|
|
931
|
-
} to ${this.#endpointStructure.resolveAttributeName(path)} ignored: ${error.message}`,
|
|
932
|
-
);
|
|
933
|
-
|
|
934
|
-
// TODO - This behavior may be wrong.
|
|
935
|
-
//
|
|
936
|
-
// If a wildcard write fails we should either:
|
|
937
|
-
//
|
|
938
|
-
// 1. Ignore entirely (add nothing to write results), or
|
|
939
|
-
// 2. Add an error response for the concrete attribute that failed.
|
|
940
|
-
//
|
|
941
|
-
// Spec is a little ambiguous. After request path expansion, in core 1.2 8.7.3.2 it states:
|
|
942
|
-
//
|
|
943
|
-
// "If the path indicates attribute data that is not writable, then the path SHALL be
|
|
944
|
-
// discarded"
|
|
945
|
-
//
|
|
946
|
-
// So is this "not writable" -- so it should be #1 -- or is this "writable" but with invalid
|
|
947
|
-
// data? The latter is error case #2 but spec doesn't make clear what the status code would
|
|
948
|
-
// be... We could fall back to CONSTRAINT_ERROR like we do above though
|
|
949
|
-
//
|
|
950
|
-
// Currently what we do is add a success response for every concrete path that fails.
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
writeResults.push({ path, statusCode: StatusCode.Success });
|
|
954
|
-
}
|
|
955
|
-
//.filter(({ statusCode }) => statusCode !== StatusCode.Success); // see https://github.com/project-chip/connectedhomeip/issues/26198
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const errorResults = writeResults.filter(({ statusCode }) => statusCode !== StatusCode.Success);
|
|
959
|
-
logger.debug(
|
|
960
|
-
`Write request from ${exchange.channel.name} done ${
|
|
961
|
-
errorResults.length
|
|
962
|
-
? `with following errors: ${errorResults
|
|
963
|
-
.map(
|
|
964
|
-
({ path, statusCode }) =>
|
|
965
|
-
`${this.#endpointStructure.resolveAttributeName(path)}=${Diagnostic.json(statusCode)}`,
|
|
966
|
-
)
|
|
967
|
-
.join(", ")}`
|
|
968
|
-
: "without errors"
|
|
969
|
-
}`,
|
|
970
|
-
);
|
|
971
|
-
|
|
972
|
-
const response = {
|
|
973
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
974
|
-
writeResponses: writeResults.map(({ path, statusCode, clusterStatusCode }) => ({
|
|
975
|
-
path,
|
|
976
|
-
status: { status: statusCode, clusterStatus: clusterStatusCode },
|
|
977
|
-
})),
|
|
978
|
-
};
|
|
979
|
-
|
|
980
|
-
// Trigger attribute events for delayed list writes
|
|
981
|
-
for (const attribute of attributeListWrites.values()) {
|
|
982
|
-
try {
|
|
983
|
-
attribute.triggerDelayedChangeEvents();
|
|
984
|
-
} catch (error) {
|
|
985
|
-
logger.error(
|
|
986
|
-
`Ignored Error while writing attribute from ${exchange.channel.name} to ${attribute.name}:`,
|
|
987
|
-
error,
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
return response;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
protected async writeAttribute(
|
|
996
|
-
_path: AttributePath,
|
|
997
|
-
attribute: AttributeServer<any>,
|
|
998
|
-
value: any,
|
|
999
|
-
exchange: MessageExchange,
|
|
1000
|
-
message: Message,
|
|
1001
|
-
_endpoint: EndpointInterface,
|
|
1002
|
-
_receivedWithinTimedInteraction?: boolean,
|
|
1003
|
-
isListWrite = false,
|
|
1004
|
-
) {
|
|
1005
|
-
attribute.set(value, exchange.session, message, isListWrite);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
async handleSubscribeRequest(
|
|
1009
|
-
exchange: MessageExchange,
|
|
1010
|
-
request: SubscribeRequest,
|
|
1011
|
-
messenger: InteractionServerMessenger,
|
|
1012
|
-
message: Message,
|
|
1013
|
-
): Promise<void> {
|
|
1014
|
-
const {
|
|
1015
|
-
minIntervalFloorSeconds,
|
|
1016
|
-
maxIntervalCeilingSeconds,
|
|
1017
|
-
attributeRequests,
|
|
1018
|
-
dataVersionFilters,
|
|
1019
|
-
eventRequests,
|
|
1020
|
-
eventFilters,
|
|
1021
|
-
keepSubscriptions,
|
|
1022
|
-
isFabricFiltered,
|
|
1023
|
-
interactionModelRevision,
|
|
1024
|
-
} = request;
|
|
1025
|
-
logger.debug(
|
|
1026
|
-
`Received subscribe request from ${exchange.channel.name} (keepSubscriptions=${keepSubscriptions}, isFabricFiltered=${isFabricFiltered})`,
|
|
1027
|
-
);
|
|
1028
|
-
|
|
1029
|
-
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
1030
|
-
logger.debug(
|
|
1031
|
-
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (message.packetHeader.sessionType !== SessionType.Unicast) {
|
|
1036
|
-
throw new StatusResponseError(
|
|
1037
|
-
"Subscriptions are only allowed on unicast sessions",
|
|
1038
|
-
StatusCode.InvalidAction,
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
assertSecureSession(exchange.session, "Subscriptions are only implemented on secure sessions");
|
|
1043
|
-
const session = exchange.session;
|
|
1044
|
-
const fabric = session.fabric;
|
|
1045
|
-
if (fabric === undefined)
|
|
1046
|
-
throw new StatusResponseError(
|
|
1047
|
-
"Subscriptions are only implemented after a fabric has been assigned",
|
|
1048
|
-
StatusCode.InvalidAction,
|
|
1049
|
-
);
|
|
1050
|
-
|
|
1051
|
-
if (!keepSubscriptions) {
|
|
1052
|
-
const clearedCount = await this.#context.sessions.clearSubscriptionsForNode(
|
|
1053
|
-
fabric.addressOf(session.peerNodeId),
|
|
1054
|
-
true,
|
|
1055
|
-
);
|
|
1056
|
-
if (clearedCount > 0) {
|
|
1057
|
-
logger.debug(
|
|
1058
|
-
`Cleared ${clearedCount} subscriptions for Subscriber node ${session.peerNodeId} because keepSubscriptions=false`,
|
|
1059
|
-
);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (
|
|
1064
|
-
(!Array.isArray(attributeRequests) || attributeRequests.length === 0) &&
|
|
1065
|
-
(!Array.isArray(eventRequests) || eventRequests.length === 0)
|
|
1066
|
-
) {
|
|
1067
|
-
throw new StatusResponseError("No attributes or events requested", StatusCode.InvalidAction);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
logger.debug(
|
|
1071
|
-
`Subscribe to attributes:${
|
|
1072
|
-
attributeRequests?.map(path => this.#endpointStructure.resolveAttributeName(path)).join(", ") ?? "none"
|
|
1073
|
-
}, events:${
|
|
1074
|
-
eventRequests?.map(path => this.#endpointStructure.resolveEventName(path)).join(", ") ?? "none"
|
|
1075
|
-
}`,
|
|
1076
|
-
);
|
|
1077
|
-
|
|
1078
|
-
if (dataVersionFilters !== undefined && dataVersionFilters.length > 0) {
|
|
1079
|
-
logger.debug(
|
|
1080
|
-
`DataVersionFilters: ${dataVersionFilters
|
|
1081
|
-
.map(
|
|
1082
|
-
({ path: { nodeId, endpointId, clusterId }, dataVersion }) =>
|
|
1083
|
-
`${clusterPathToId({ nodeId, endpointId, clusterId })}=${dataVersion}`,
|
|
1084
|
-
)
|
|
1085
|
-
.join(", ")}`,
|
|
1086
|
-
);
|
|
1087
|
-
}
|
|
1088
|
-
if (eventFilters !== undefined && eventFilters.length > 0)
|
|
1089
|
-
logger.debug(
|
|
1090
|
-
`Event filters: ${eventFilters.map(filter => `${filter.nodeId}/${filter.eventMin}`).join(", ")}`,
|
|
1091
|
-
);
|
|
1092
|
-
|
|
1093
|
-
// Validate of the paths before proceeding
|
|
1094
|
-
attributeRequests?.forEach(path => validateReadAttributesPath(path));
|
|
1095
|
-
eventRequests?.forEach(path => validateReadEventPath(path));
|
|
1096
|
-
|
|
1097
|
-
if (minIntervalFloorSeconds < 0) {
|
|
1098
|
-
throw new StatusResponseError(
|
|
1099
|
-
"minIntervalFloorSeconds should be greater or equal to 0",
|
|
1100
|
-
StatusCode.InvalidAction,
|
|
1101
|
-
);
|
|
1102
|
-
}
|
|
1103
|
-
if (maxIntervalCeilingSeconds < 0) {
|
|
1104
|
-
throw new StatusResponseError(
|
|
1105
|
-
"maxIntervalCeilingSeconds should be greater or equal to 1",
|
|
1106
|
-
StatusCode.InvalidAction,
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1109
|
-
if (maxIntervalCeilingSeconds < minIntervalFloorSeconds) {
|
|
1110
|
-
throw new StatusResponseError(
|
|
1111
|
-
"maxIntervalCeilingSeconds should be greater or equal to minIntervalFloorSeconds",
|
|
1112
|
-
StatusCode.InvalidAction,
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
if (this.#nextSubscriptionId === 0xffffffff) this.#nextSubscriptionId = 0;
|
|
1117
|
-
const subscriptionId = this.#nextSubscriptionId++;
|
|
1118
|
-
|
|
1119
|
-
this.#subscriptionEstablishmentStarted.emit(session.peerAddress);
|
|
1120
|
-
let subscription: ServerSubscription;
|
|
1121
|
-
try {
|
|
1122
|
-
subscription = await this.#establishSubscription(
|
|
1123
|
-
subscriptionId,
|
|
1124
|
-
request,
|
|
1125
|
-
messenger,
|
|
1126
|
-
session,
|
|
1127
|
-
exchange,
|
|
1128
|
-
message,
|
|
1129
|
-
);
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
logger.error(
|
|
1132
|
-
`Subscription ${subscriptionId} for Session ${session.id}: Error while sending initial data reports`,
|
|
1133
|
-
error instanceof MatterError ? error.message : error,
|
|
1134
|
-
);
|
|
1135
|
-
if (error instanceof StatusResponseError && !(error instanceof ReceivedStatusResponseError)) {
|
|
1136
|
-
logger.info(`Sending status response ${error.code} for interaction error: ${error.message}`);
|
|
1137
|
-
await messenger.sendStatus(error.code, {
|
|
1138
|
-
logContext: {
|
|
1139
|
-
for: "I/SubscriptionSeed-Status",
|
|
1140
|
-
},
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
await messenger.close();
|
|
1144
|
-
return; // Make sure to not bubble up the exception
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
const maxInterval = subscription.maxInterval;
|
|
1148
|
-
// Then send the subscription response
|
|
1149
|
-
await messenger.send(
|
|
1150
|
-
MessageType.SubscribeResponse,
|
|
1151
|
-
TlvSubscribeResponse.encode({
|
|
1152
|
-
subscriptionId,
|
|
1153
|
-
maxInterval,
|
|
1154
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
1155
|
-
}),
|
|
1156
|
-
{
|
|
1157
|
-
logContext: {
|
|
1158
|
-
subId: subscriptionId,
|
|
1159
|
-
maxInterval,
|
|
1160
|
-
},
|
|
1161
|
-
},
|
|
1162
|
-
);
|
|
1163
|
-
|
|
1164
|
-
// When an error occurs while sending the response, the subscription is not yet active and will be cleaned up by GC
|
|
1165
|
-
subscription.activate();
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
async #establishSubscription(
|
|
1169
|
-
id: number,
|
|
1170
|
-
{
|
|
1171
|
-
minIntervalFloorSeconds,
|
|
1172
|
-
maxIntervalCeilingSeconds,
|
|
1173
|
-
attributeRequests,
|
|
1174
|
-
dataVersionFilters,
|
|
1175
|
-
eventRequests,
|
|
1176
|
-
eventFilters,
|
|
1177
|
-
isFabricFiltered,
|
|
1178
|
-
}: SubscribeRequest,
|
|
1179
|
-
messenger: InteractionServerMessenger,
|
|
1180
|
-
session: SecureSession,
|
|
1181
|
-
exchange: MessageExchange,
|
|
1182
|
-
message: Message,
|
|
1183
|
-
) {
|
|
1184
|
-
const context: ServerSubscriptionContext = {
|
|
1185
|
-
session,
|
|
1186
|
-
structure: this.#endpointStructure,
|
|
1187
|
-
|
|
1188
|
-
readAttribute: (path, attribute, offline) =>
|
|
1189
|
-
this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
|
|
1190
|
-
|
|
1191
|
-
readEndpointAttributesForSubscription: attributes =>
|
|
1192
|
-
this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
|
|
1193
|
-
|
|
1194
|
-
readEvent: (path, event, eventFilters) =>
|
|
1195
|
-
this.readEvent(path, eventFilters, event, exchange, isFabricFiltered, message),
|
|
1196
|
-
|
|
1197
|
-
initiateExchange: (address: PeerAddress, protocolId) => this.#context.initiateExchange(address, protocolId),
|
|
1198
|
-
};
|
|
1199
|
-
|
|
1200
|
-
const subscription = new ServerSubscription({
|
|
1201
|
-
id,
|
|
1202
|
-
context,
|
|
1203
|
-
criteria: {
|
|
1204
|
-
attributeRequests,
|
|
1205
|
-
dataVersionFilters,
|
|
1206
|
-
eventRequests,
|
|
1207
|
-
eventFilters,
|
|
1208
|
-
isFabricFiltered,
|
|
1209
|
-
},
|
|
1210
|
-
minIntervalFloorSeconds,
|
|
1211
|
-
maxIntervalCeilingSeconds,
|
|
1212
|
-
subscriptionOptions: this.#subscriptionConfig,
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
try {
|
|
1216
|
-
// Send initial data report to prime the subscription with initial data
|
|
1217
|
-
await subscription.sendInitialReport(messenger);
|
|
1218
|
-
} catch (error) {
|
|
1219
|
-
await subscription.close(); // Cleanup
|
|
1220
|
-
throw error;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
logger.info(
|
|
1224
|
-
`Successfully created subscription ${id} for Session ${
|
|
1225
|
-
session.id
|
|
1226
|
-
} to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`,
|
|
1227
|
-
);
|
|
1228
|
-
return subscription;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
async establishFormerSubscription(
|
|
1232
|
-
{
|
|
1233
|
-
subscriptionId,
|
|
1234
|
-
attributeRequests,
|
|
1235
|
-
eventRequests,
|
|
1236
|
-
isFabricFiltered,
|
|
1237
|
-
minIntervalFloorSeconds,
|
|
1238
|
-
maxIntervalCeilingSeconds,
|
|
1239
|
-
maxInterval,
|
|
1240
|
-
sendInterval,
|
|
1241
|
-
}: PeerSubscription,
|
|
1242
|
-
session: SecureSession,
|
|
1243
|
-
) {
|
|
1244
|
-
const exchange = this.#context.initiateExchange(session.peerAddress, INTERACTION_PROTOCOL_ID);
|
|
1245
|
-
const message = {} as Message;
|
|
1246
|
-
logger.debug(
|
|
1247
|
-
`Send DataReports to re-establish subscription ${subscriptionId} to `,
|
|
1248
|
-
Diagnostic.dict({ isFabricFiltered, maxInterval, sendInterval }),
|
|
1249
|
-
);
|
|
1250
|
-
const context: ServerSubscriptionContext = {
|
|
1251
|
-
session,
|
|
1252
|
-
structure: this.#endpointStructure,
|
|
1253
|
-
|
|
1254
|
-
readAttribute: (path, attribute, offline) =>
|
|
1255
|
-
this.readAttribute(path, attribute, exchange, isFabricFiltered, message, offline),
|
|
1256
|
-
|
|
1257
|
-
readEndpointAttributesForSubscription: attributes =>
|
|
1258
|
-
this.readEndpointAttributesForSubscription(attributes, exchange, isFabricFiltered, message),
|
|
1259
|
-
|
|
1260
|
-
readEvent: (path, event, eventFilters) =>
|
|
1261
|
-
this.readEvent(path, eventFilters, event, exchange, isFabricFiltered, message),
|
|
1262
|
-
|
|
1263
|
-
initiateExchange: (address: PeerAddress, protocolId) => this.#context.initiateExchange(address, protocolId),
|
|
1264
|
-
};
|
|
1265
|
-
|
|
1266
|
-
const subscription = new ServerSubscription({
|
|
1267
|
-
id: subscriptionId,
|
|
1268
|
-
context,
|
|
1269
|
-
minIntervalFloorSeconds,
|
|
1270
|
-
maxIntervalCeilingSeconds,
|
|
1271
|
-
criteria: {
|
|
1272
|
-
attributeRequests,
|
|
1273
|
-
eventRequests,
|
|
1274
|
-
isFabricFiltered,
|
|
1275
|
-
},
|
|
1276
|
-
subscriptionOptions: this.#subscriptionConfig,
|
|
1277
|
-
useAsMaxInterval: maxInterval,
|
|
1278
|
-
useAsSendInterval: sendInterval,
|
|
1279
|
-
});
|
|
1280
|
-
|
|
1281
|
-
try {
|
|
1282
|
-
// Send initial data report to prime the subscription with initial data
|
|
1283
|
-
await subscription.sendInitialReport(new InteractionServerMessenger(exchange));
|
|
1284
|
-
subscription.activate();
|
|
1285
|
-
logger.info(
|
|
1286
|
-
`Successfully re-established subscription ${subscriptionId} for Session ${
|
|
1287
|
-
session.id
|
|
1288
|
-
} to ${session.peerAddress}. Updates: ${minIntervalFloorSeconds} - ${maxIntervalCeilingSeconds} => ${subscription.maxInterval} seconds (sendInterval = ${subscription.sendInterval} seconds)`,
|
|
1289
|
-
);
|
|
1290
|
-
} catch (error) {
|
|
1291
|
-
await subscription.close(); // Cleanup
|
|
1292
|
-
throw error;
|
|
1293
|
-
}
|
|
1294
|
-
return subscription;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
async handleInvokeRequest(
|
|
1298
|
-
exchange: MessageExchange,
|
|
1299
|
-
{ invokeRequests, timedRequest, suppressResponse, interactionModelRevision }: InvokeRequest,
|
|
1300
|
-
messenger: InteractionServerMessenger,
|
|
1301
|
-
message: Message,
|
|
1302
|
-
): Promise<void> {
|
|
1303
|
-
logger.debug(
|
|
1304
|
-
`Received invoke request from ${exchange.channel.name}${invokeRequests.length > 0 ? ` with ${invokeRequests.length} commands` : ""}: ${invokeRequests
|
|
1305
|
-
.map(({ commandPath: { endpointId, clusterId, commandId } }) =>
|
|
1306
|
-
this.#endpointStructure.resolveCommandName({ endpointId, clusterId, commandId }),
|
|
1307
|
-
)
|
|
1308
|
-
.join(", ")}, suppressResponse=${suppressResponse}`,
|
|
1309
|
-
);
|
|
1310
|
-
|
|
1311
|
-
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
1312
|
-
logger.debug(
|
|
1313
|
-
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
1314
|
-
);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
const receivedWithinTimedInteraction = exchange.hasActiveTimedInteraction();
|
|
1318
|
-
if (exchange.hasExpiredTimedInteraction()) {
|
|
1319
|
-
exchange.clearTimedInteraction(); // ??
|
|
1320
|
-
throw new StatusResponseError(`Timed request window expired. Decline invoke request.`, StatusCode.Timeout);
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
if (timedRequest !== exchange.hasTimedInteraction()) {
|
|
1324
|
-
throw new StatusResponseError(
|
|
1325
|
-
`timedRequest flag of invoke interaction (${timedRequest}) mismatch with expected timed interaction (${receivedWithinTimedInteraction}).`,
|
|
1326
|
-
StatusCode.TimedRequestMismatch,
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
if (receivedWithinTimedInteraction) {
|
|
1331
|
-
logger.debug(`Invoke request from ${exchange.channel.name} received while timed interaction is running.`);
|
|
1332
|
-
exchange.clearTimedInteraction();
|
|
1333
|
-
if (message.packetHeader.sessionType !== SessionType.Unicast) {
|
|
1334
|
-
throw new StatusResponseError(
|
|
1335
|
-
"Invoke requests are only allowed on unicast sessions when a timed interaction is running.",
|
|
1336
|
-
StatusCode.InvalidAction,
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
if (invokeRequests.length > this.#maxPathsPerInvoke) {
|
|
1342
|
-
throw new StatusResponseError(
|
|
1343
|
-
`Only ${this.#maxPathsPerInvoke} invoke requests are supported in one message. This message contains ${invokeRequests.length}`,
|
|
1344
|
-
StatusCode.InvalidAction,
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// Validate all commandPaths before proceeding to make sure not to have executed partial commands
|
|
1349
|
-
invokeRequests.forEach(({ commandPath }) => validateCommandPath(commandPath));
|
|
1350
|
-
|
|
1351
|
-
// Perform additional cross-command validation required for batch invoke
|
|
1352
|
-
if (invokeRequests.length > 1) {
|
|
1353
|
-
const pathsUsed = new Set<string>();
|
|
1354
|
-
const commandRefsUsed = new Set<number>();
|
|
1355
|
-
invokeRequests.forEach(({ commandPath, commandRef }) => {
|
|
1356
|
-
if (!isConcreteCommandPath(commandPath)) {
|
|
1357
|
-
throw new StatusResponseError("Illegal wildcard path in batch invoke", StatusCode.InvalidAction);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
const commandPathId = commandPathToId(commandPath);
|
|
1361
|
-
if (pathsUsed.has(commandPathId)) {
|
|
1362
|
-
throw new StatusResponseError(
|
|
1363
|
-
`Duplicate command path (${commandPathId}) in batch invoke`,
|
|
1364
|
-
StatusCode.InvalidAction,
|
|
1365
|
-
);
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
if (commandRef === undefined) {
|
|
1369
|
-
throw new StatusResponseError(
|
|
1370
|
-
`Command reference missing in batch invoke of ${commandPathId}`,
|
|
1371
|
-
StatusCode.InvalidAction,
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
if (commandRefsUsed.has(commandRef)) {
|
|
1376
|
-
throw new StatusResponseError(
|
|
1377
|
-
`Duplicate command reference ${commandRef} in invoke of ${commandPathId}`,
|
|
1378
|
-
StatusCode.InvalidAction,
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
pathsUsed.add(commandPathId);
|
|
1383
|
-
commandRefsUsed.add(commandRef);
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
const isGroupSession = message.packetHeader.sessionType === SessionType.Group;
|
|
1388
|
-
const invokeResponseMessage: TypeFromSchema<typeof TlvInvokeResponseForSend> = {
|
|
1389
|
-
suppressResponse: false, // Deprecated but must be present
|
|
1390
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
1391
|
-
invokeResponses: [],
|
|
1392
|
-
moreChunkedMessages: invokeRequests.length > 1, // Assume for now we have multiple responses when having multiple invokes
|
|
1393
|
-
};
|
|
1394
|
-
const emptyInvokeResponseBytes = TlvInvokeResponseForSend.encode(invokeResponseMessage);
|
|
1395
|
-
let messageSize = emptyInvokeResponseBytes.length;
|
|
1396
|
-
let invokeResultsProcessed = 0;
|
|
1397
|
-
|
|
1398
|
-
// To lower potential latency when we would process all invoke messages and just send responses at the end we
|
|
1399
|
-
// assemble response on the fly locally here and send when message becomes too big
|
|
1400
|
-
const processResponseResult = async (
|
|
1401
|
-
invokeResponse: TypeFromSchema<typeof TlvInvokeResponseData>,
|
|
1402
|
-
): Promise<void> => {
|
|
1403
|
-
invokeResultsProcessed++;
|
|
1404
|
-
|
|
1405
|
-
if (isGroupSession) {
|
|
1406
|
-
// We send no responses at all for group sessions
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
const encodedInvokeResponse = TlvInvokeResponseData.encodeTlv(invokeResponse);
|
|
1410
|
-
const invokeResponseBytes = TlvAny.getEncodedByteLength(encodedInvokeResponse);
|
|
1411
|
-
|
|
1412
|
-
if (
|
|
1413
|
-
messageSize + invokeResponseBytes > exchange.maxPayloadSize ||
|
|
1414
|
-
invokeResultsProcessed === invokeRequests.length
|
|
1415
|
-
) {
|
|
1416
|
-
let lastMessageProcessed = false;
|
|
1417
|
-
if (messageSize + invokeResponseBytes <= exchange.maxPayloadSize) {
|
|
1418
|
-
// last invoke response and matches in the message
|
|
1419
|
-
invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
|
|
1420
|
-
lastMessageProcessed = true;
|
|
1421
|
-
}
|
|
1422
|
-
// Send the response when the message is full or when all responses are processed
|
|
1423
|
-
if (invokeResponseMessage.invokeResponses.length > 0) {
|
|
1424
|
-
if (invokeRequests.length > 1) {
|
|
1425
|
-
logger.debug(
|
|
1426
|
-
`Send ${lastMessageProcessed ? "final " : ""}invoke response for ${invokeResponseMessage.invokeResponses} commands`,
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
const moreChunkedMessages = lastMessageProcessed ? undefined : true;
|
|
1430
|
-
await messenger.send(
|
|
1431
|
-
MessageType.InvokeResponse,
|
|
1432
|
-
TlvInvokeResponseForSend.encode({
|
|
1433
|
-
...invokeResponseMessage,
|
|
1434
|
-
moreChunkedMessages,
|
|
1435
|
-
}),
|
|
1436
|
-
{
|
|
1437
|
-
logContext: {
|
|
1438
|
-
invokeMsgFlags: Diagnostic.asFlags({
|
|
1439
|
-
suppressResponse,
|
|
1440
|
-
moreChunkedMessages,
|
|
1441
|
-
}),
|
|
1442
|
-
},
|
|
1443
|
-
},
|
|
1444
|
-
);
|
|
1445
|
-
invokeResponseMessage.invokeResponses = [];
|
|
1446
|
-
messageSize = emptyInvokeResponseBytes.length;
|
|
1447
|
-
}
|
|
1448
|
-
if (!lastMessageProcessed) {
|
|
1449
|
-
invokeResultsProcessed--; // Correct counter again because we recall the method
|
|
1450
|
-
return processResponseResult(invokeResponse);
|
|
1451
|
-
}
|
|
1452
|
-
} else {
|
|
1453
|
-
invokeResponseMessage.invokeResponses.push(encodedInvokeResponse);
|
|
1454
|
-
messageSize += invokeResponseBytes;
|
|
1455
|
-
}
|
|
1456
|
-
};
|
|
1457
|
-
|
|
1458
|
-
// We could do more fancy parallel command processing, but it makes no sense for now, so lets simply process
|
|
1459
|
-
// invoked commands one by one sequentially
|
|
1460
|
-
for (const { commandPath, commandFields, commandRef } of invokeRequests) {
|
|
1461
|
-
const commands = this.#endpointStructure.getCommands([commandPath]);
|
|
1462
|
-
|
|
1463
|
-
if (commands.length === 0) {
|
|
1464
|
-
if (isConcreteCommandPath(commandPath)) {
|
|
1465
|
-
const { endpointId, clusterId, commandId } = commandPath;
|
|
1466
|
-
|
|
1467
|
-
let result;
|
|
1468
|
-
|
|
1469
|
-
try {
|
|
1470
|
-
this.#endpointStructure.validateConcreteCommandPath(endpointId, clusterId, commandId);
|
|
1471
|
-
throw new InternalError(
|
|
1472
|
-
"validateConcreteCommandPath should throw StatusResponseError but did not.",
|
|
1473
|
-
);
|
|
1474
|
-
} catch (e) {
|
|
1475
|
-
StatusResponseError.accept(e);
|
|
1476
|
-
|
|
1477
|
-
logger.debug(
|
|
1478
|
-
`Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
|
|
1479
|
-
commandPath,
|
|
1480
|
-
)} unsupported path: Status=${e.code}`,
|
|
1481
|
-
);
|
|
1482
|
-
result = { status: { commandPath, status: { status: e.code }, commandRef } };
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
await processResponseResult(result);
|
|
1486
|
-
} else {
|
|
1487
|
-
// Wildcard path: Just ignore
|
|
1488
|
-
logger.debug(
|
|
1489
|
-
`Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
|
|
1490
|
-
commandPath,
|
|
1491
|
-
)} ignore non-existing command`,
|
|
1492
|
-
);
|
|
1493
|
-
}
|
|
1494
|
-
continue;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
const isConcretePath = isConcreteCommandPath(commandPath);
|
|
1498
|
-
for (const { command, path } of commands) {
|
|
1499
|
-
const { endpointId, clusterId, commandId } = path;
|
|
1500
|
-
if (endpointId === undefined) {
|
|
1501
|
-
// Should never happen
|
|
1502
|
-
logger.error(
|
|
1503
|
-
`Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
|
|
1504
|
-
path,
|
|
1505
|
-
)} invalid path because empty endpoint!`,
|
|
1506
|
-
);
|
|
1507
|
-
if (isConcretePath) {
|
|
1508
|
-
await processResponseResult({
|
|
1509
|
-
status: {
|
|
1510
|
-
commandPath: path,
|
|
1511
|
-
status: { status: StatusCode.UnsupportedEndpoint },
|
|
1512
|
-
commandRef,
|
|
1513
|
-
},
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
const endpoint = this.#endpointStructure.getEndpoint(endpointId);
|
|
1519
|
-
if (endpoint === undefined) {
|
|
1520
|
-
// Should never happen
|
|
1521
|
-
logger.error(
|
|
1522
|
-
`Invoke from ${exchange.channel.name}: ${this.#endpointStructure.resolveCommandName(
|
|
1523
|
-
path,
|
|
1524
|
-
)} invalid path because endpoint not found!`,
|
|
1525
|
-
);
|
|
1526
|
-
if (isConcretePath) {
|
|
1527
|
-
await processResponseResult({
|
|
1528
|
-
status: {
|
|
1529
|
-
commandPath: path,
|
|
1530
|
-
status: { status: StatusCode.UnsupportedEndpoint },
|
|
1531
|
-
commandRef,
|
|
1532
|
-
},
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
1535
|
-
continue;
|
|
1536
|
-
}
|
|
1537
|
-
if (command.requiresTimedInteraction && !receivedWithinTimedInteraction) {
|
|
1538
|
-
logger.debug(`This invoke requires a timed interaction which is not initialized.`);
|
|
1539
|
-
if (isConcretePath) {
|
|
1540
|
-
await processResponseResult({
|
|
1541
|
-
status: {
|
|
1542
|
-
commandPath: path,
|
|
1543
|
-
status: { status: StatusCode.NeedsTimedInteraction },
|
|
1544
|
-
commandRef,
|
|
1545
|
-
},
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
continue;
|
|
1549
|
-
}
|
|
1550
|
-
if (
|
|
1551
|
-
getMatterModelClusterCommand(clusterId, commandId)?.fabricScoped &&
|
|
1552
|
-
(!exchange.session.isSecure || !(exchange.session as SecureSession).fabric)
|
|
1553
|
-
) {
|
|
1554
|
-
logger.debug(`This invoke requires a secure session with a fabric assigned which is missing.`);
|
|
1555
|
-
if (isConcretePath) {
|
|
1556
|
-
await processResponseResult({
|
|
1557
|
-
status: { commandPath: path, status: { status: StatusCode.UnsupportedAccess }, commandRef },
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
continue;
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
let result;
|
|
1564
|
-
try {
|
|
1565
|
-
result = await this.invokeCommand(
|
|
1566
|
-
path,
|
|
1567
|
-
command,
|
|
1568
|
-
exchange,
|
|
1569
|
-
commandFields ?? TlvNoArguments.encodeTlv(commandFields),
|
|
1570
|
-
message,
|
|
1571
|
-
endpoint,
|
|
1572
|
-
receivedWithinTimedInteraction,
|
|
1573
|
-
);
|
|
1574
|
-
} catch (e) {
|
|
1575
|
-
StatusResponseError.accept(e);
|
|
1576
|
-
|
|
1577
|
-
let errorCode = e.code;
|
|
1578
|
-
const errorLogText = `Error ${Diagnostic.hex(errorCode)}${
|
|
1579
|
-
e.clusterCode !== undefined ? `/${Diagnostic.hex(e.clusterCode)}` : ""
|
|
1580
|
-
} while invoking command: ${e.message}`;
|
|
1581
|
-
|
|
1582
|
-
if (e instanceof ValidationError) {
|
|
1583
|
-
logger.info(
|
|
1584
|
-
`Validation-${errorLogText}${e.fieldName !== undefined ? ` in field ${e.fieldName}` : ""}`,
|
|
1585
|
-
);
|
|
1586
|
-
if (errorCode === StatusCode.InvalidAction) {
|
|
1587
|
-
errorCode = StatusCode.InvalidCommand;
|
|
1588
|
-
}
|
|
1589
|
-
} else {
|
|
1590
|
-
logger.info(errorLogText);
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
result = {
|
|
1594
|
-
code: errorCode,
|
|
1595
|
-
clusterCode: e.clusterCode,
|
|
1596
|
-
responseId: command.responseId,
|
|
1597
|
-
response: TlvNoResponse.encodeTlv(),
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
const { code, clusterCode, responseId, response } = result;
|
|
1601
|
-
if (response.length === 0) {
|
|
1602
|
-
await processResponseResult({
|
|
1603
|
-
status: { commandPath: path, status: { status: code, clusterStatus: clusterCode }, commandRef },
|
|
1604
|
-
});
|
|
1605
|
-
} else {
|
|
1606
|
-
await processResponseResult({
|
|
1607
|
-
command: {
|
|
1608
|
-
commandPath: { ...path, commandId: responseId },
|
|
1609
|
-
commandFields: response,
|
|
1610
|
-
commandRef,
|
|
1611
|
-
},
|
|
1612
|
-
});
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
protected async invokeCommand(
|
|
1619
|
-
_path: CommandPath,
|
|
1620
|
-
command: CommandServer<any, any>,
|
|
1621
|
-
exchange: MessageExchange,
|
|
1622
|
-
commandFields: any,
|
|
1623
|
-
message: Message,
|
|
1624
|
-
endpoint: EndpointInterface,
|
|
1625
|
-
_receivedWithinTimedInteraction = false,
|
|
1626
|
-
) {
|
|
1627
|
-
return command.invoke(exchange.session, commandFields, message, endpoint);
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
handleTimedRequest(exchange: MessageExchange, { timeout, interactionModelRevision }: TimedRequest) {
|
|
1631
|
-
logger.debug(`Received timed request (${timeout}ms) from ${exchange.channel.name}`);
|
|
1632
|
-
|
|
1633
|
-
if (interactionModelRevision > Specification.INTERACTION_MODEL_REVISION) {
|
|
1634
|
-
logger.debug(
|
|
1635
|
-
`Interaction model revision of sender ${interactionModelRevision} is higher than supported ${Specification.INTERACTION_MODEL_REVISION}.`,
|
|
1636
|
-
);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
exchange.startTimedInteraction(timeout);
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
async close() {
|
|
1643
|
-
this.#isClosing = true;
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
get #endpointStructure() {
|
|
1647
|
-
return this.#context.structure;
|
|
1648
|
-
}
|
|
1649
|
-
}
|