@matter/protocol 0.16.0-alpha.0-20251016-b56cf5683 → 0.16.0-alpha.0-20251018-dd1ea6a8a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/cjs/action/client/ClientInteraction.d.ts +10 -5
  2. package/dist/cjs/action/client/ClientInteraction.d.ts.map +1 -1
  3. package/dist/cjs/action/client/ClientInteraction.js +129 -18
  4. package/dist/cjs/action/client/ClientInteraction.js.map +1 -1
  5. package/dist/cjs/action/request/Invoke.d.ts +36 -8
  6. package/dist/cjs/action/request/Invoke.d.ts.map +1 -1
  7. package/dist/cjs/action/request/Invoke.js +80 -15
  8. package/dist/cjs/action/request/Invoke.js.map +1 -1
  9. package/dist/cjs/action/request/Read.d.ts +1 -1
  10. package/dist/cjs/action/request/Read.d.ts.map +1 -1
  11. package/dist/cjs/action/request/Read.js +10 -2
  12. package/dist/cjs/action/request/Read.js.map +1 -1
  13. package/dist/cjs/action/request/Specifier.d.ts +16 -1
  14. package/dist/cjs/action/request/Specifier.d.ts.map +1 -1
  15. package/dist/cjs/action/request/Specifier.js +56 -1
  16. package/dist/cjs/action/request/Specifier.js.map +1 -1
  17. package/dist/cjs/action/request/Write.d.ts +5 -3
  18. package/dist/cjs/action/request/Write.d.ts.map +1 -1
  19. package/dist/cjs/action/request/Write.js +52 -12
  20. package/dist/cjs/action/request/Write.js.map +1 -1
  21. package/dist/cjs/action/response/InvokeResult.d.ts +6 -0
  22. package/dist/cjs/action/response/InvokeResult.d.ts.map +1 -1
  23. package/dist/cjs/interaction/InteractionClient.d.ts.map +1 -1
  24. package/dist/cjs/interaction/InteractionClient.js +91 -74
  25. package/dist/cjs/interaction/InteractionClient.js.map +1 -1
  26. package/dist/cjs/interaction/InteractionMessenger.d.ts +3 -2
  27. package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
  28. package/dist/cjs/interaction/InteractionMessenger.js +10 -3
  29. package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
  30. package/dist/cjs/protocol/ExchangeManager.d.ts.map +1 -1
  31. package/dist/cjs/protocol/ExchangeManager.js +20 -17
  32. package/dist/cjs/protocol/ExchangeManager.js.map +1 -1
  33. package/dist/cjs/protocol/MessageChannel.d.ts.map +1 -1
  34. package/dist/cjs/protocol/MessageChannel.js +1 -1
  35. package/dist/cjs/protocol/MessageChannel.js.map +1 -1
  36. package/dist/cjs/protocol/MessageExchange.d.ts +1 -0
  37. package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
  38. package/dist/cjs/protocol/MessageExchange.js +14 -4
  39. package/dist/cjs/protocol/MessageExchange.js.map +1 -1
  40. package/dist/cjs/protocol/ProtocolHandler.d.ts +7 -2
  41. package/dist/cjs/protocol/ProtocolHandler.d.ts.map +1 -1
  42. package/dist/cjs/securechannel/SecureChannelProtocol.d.ts +1 -1
  43. package/dist/cjs/securechannel/SecureChannelProtocol.d.ts.map +1 -1
  44. package/dist/cjs/securechannel/SecureChannelProtocol.js +2 -1
  45. package/dist/cjs/securechannel/SecureChannelProtocol.js.map +1 -1
  46. package/dist/cjs/session/GroupSession.d.ts +8 -1
  47. package/dist/cjs/session/GroupSession.d.ts.map +1 -1
  48. package/dist/cjs/session/GroupSession.js +10 -0
  49. package/dist/cjs/session/GroupSession.js.map +1 -1
  50. package/dist/cjs/session/NodeSession.d.ts +3 -1
  51. package/dist/cjs/session/NodeSession.d.ts.map +1 -1
  52. package/dist/cjs/session/NodeSession.js +13 -0
  53. package/dist/cjs/session/NodeSession.js.map +2 -2
  54. package/dist/cjs/session/SecureSession.d.ts +2 -0
  55. package/dist/cjs/session/SecureSession.d.ts.map +1 -1
  56. package/dist/cjs/session/SecureSession.js.map +1 -1
  57. package/dist/cjs/session/case/CaseClient.d.ts.map +1 -1
  58. package/dist/cjs/session/case/CaseClient.js +3 -15
  59. package/dist/cjs/session/case/CaseClient.js.map +1 -1
  60. package/dist/cjs/session/case/CaseServer.d.ts.map +1 -1
  61. package/dist/cjs/session/case/CaseServer.js +10 -18
  62. package/dist/cjs/session/case/CaseServer.js.map +1 -1
  63. package/dist/cjs/session/pase/PaseClient.js +1 -1
  64. package/dist/cjs/session/pase/PaseClient.js.map +1 -1
  65. package/dist/cjs/session/pase/PaseServer.d.ts.map +1 -1
  66. package/dist/cjs/session/pase/PaseServer.js +2 -2
  67. package/dist/cjs/session/pase/PaseServer.js.map +1 -1
  68. package/dist/esm/action/client/ClientInteraction.d.ts +10 -5
  69. package/dist/esm/action/client/ClientInteraction.d.ts.map +1 -1
  70. package/dist/esm/action/client/ClientInteraction.js +140 -20
  71. package/dist/esm/action/client/ClientInteraction.js.map +1 -1
  72. package/dist/esm/action/request/Invoke.d.ts +36 -8
  73. package/dist/esm/action/request/Invoke.d.ts.map +1 -1
  74. package/dist/esm/action/request/Invoke.js +81 -16
  75. package/dist/esm/action/request/Invoke.js.map +1 -1
  76. package/dist/esm/action/request/Read.d.ts +1 -1
  77. package/dist/esm/action/request/Read.d.ts.map +1 -1
  78. package/dist/esm/action/request/Read.js +12 -4
  79. package/dist/esm/action/request/Read.js.map +1 -1
  80. package/dist/esm/action/request/Specifier.d.ts +16 -1
  81. package/dist/esm/action/request/Specifier.d.ts.map +1 -1
  82. package/dist/esm/action/request/Specifier.js +56 -1
  83. package/dist/esm/action/request/Specifier.js.map +1 -1
  84. package/dist/esm/action/request/Write.d.ts +5 -3
  85. package/dist/esm/action/request/Write.d.ts.map +1 -1
  86. package/dist/esm/action/request/Write.js +53 -13
  87. package/dist/esm/action/request/Write.js.map +1 -1
  88. package/dist/esm/action/response/InvokeResult.d.ts +6 -0
  89. package/dist/esm/action/response/InvokeResult.d.ts.map +1 -1
  90. package/dist/esm/interaction/InteractionClient.d.ts.map +1 -1
  91. package/dist/esm/interaction/InteractionClient.js +92 -74
  92. package/dist/esm/interaction/InteractionClient.js.map +1 -1
  93. package/dist/esm/interaction/InteractionMessenger.d.ts +3 -2
  94. package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
  95. package/dist/esm/interaction/InteractionMessenger.js +10 -3
  96. package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
  97. package/dist/esm/protocol/ExchangeManager.d.ts.map +1 -1
  98. package/dist/esm/protocol/ExchangeManager.js +20 -17
  99. package/dist/esm/protocol/ExchangeManager.js.map +1 -1
  100. package/dist/esm/protocol/MessageChannel.d.ts.map +1 -1
  101. package/dist/esm/protocol/MessageChannel.js +2 -2
  102. package/dist/esm/protocol/MessageChannel.js.map +1 -1
  103. package/dist/esm/protocol/MessageExchange.d.ts +1 -0
  104. package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
  105. package/dist/esm/protocol/MessageExchange.js +14 -4
  106. package/dist/esm/protocol/MessageExchange.js.map +1 -1
  107. package/dist/esm/protocol/ProtocolHandler.d.ts +7 -2
  108. package/dist/esm/protocol/ProtocolHandler.d.ts.map +1 -1
  109. package/dist/esm/securechannel/SecureChannelProtocol.d.ts +1 -1
  110. package/dist/esm/securechannel/SecureChannelProtocol.d.ts.map +1 -1
  111. package/dist/esm/securechannel/SecureChannelProtocol.js +2 -1
  112. package/dist/esm/securechannel/SecureChannelProtocol.js.map +1 -1
  113. package/dist/esm/session/GroupSession.d.ts +8 -1
  114. package/dist/esm/session/GroupSession.d.ts.map +1 -1
  115. package/dist/esm/session/GroupSession.js +11 -1
  116. package/dist/esm/session/GroupSession.js.map +1 -1
  117. package/dist/esm/session/NodeSession.d.ts +3 -1
  118. package/dist/esm/session/NodeSession.d.ts.map +1 -1
  119. package/dist/esm/session/NodeSession.js +13 -0
  120. package/dist/esm/session/NodeSession.js.map +2 -2
  121. package/dist/esm/session/SecureSession.d.ts +2 -0
  122. package/dist/esm/session/SecureSession.d.ts.map +1 -1
  123. package/dist/esm/session/SecureSession.js.map +1 -1
  124. package/dist/esm/session/case/CaseClient.d.ts.map +1 -1
  125. package/dist/esm/session/case/CaseClient.js +5 -17
  126. package/dist/esm/session/case/CaseClient.js.map +1 -1
  127. package/dist/esm/session/case/CaseServer.d.ts.map +1 -1
  128. package/dist/esm/session/case/CaseServer.js +12 -20
  129. package/dist/esm/session/case/CaseServer.js.map +1 -1
  130. package/dist/esm/session/pase/PaseClient.js +1 -1
  131. package/dist/esm/session/pase/PaseClient.js.map +1 -1
  132. package/dist/esm/session/pase/PaseServer.d.ts.map +1 -1
  133. package/dist/esm/session/pase/PaseServer.js +3 -2
  134. package/dist/esm/session/pase/PaseServer.js.map +1 -1
  135. package/package.json +6 -6
  136. package/src/action/client/ClientInteraction.ts +181 -27
  137. package/src/action/request/Invoke.ts +149 -27
  138. package/src/action/request/Read.ts +27 -7
  139. package/src/action/request/Specifier.ts +80 -1
  140. package/src/action/request/Write.ts +71 -19
  141. package/src/action/response/InvokeResult.ts +8 -0
  142. package/src/interaction/InteractionClient.ts +135 -96
  143. package/src/interaction/InteractionMessenger.ts +15 -3
  144. package/src/protocol/ExchangeManager.ts +27 -29
  145. package/src/protocol/MessageChannel.ts +2 -2
  146. package/src/protocol/MessageExchange.ts +18 -4
  147. package/src/protocol/ProtocolHandler.ts +7 -2
  148. package/src/securechannel/SecureChannelProtocol.ts +5 -1
  149. package/src/session/GroupSession.ts +12 -1
  150. package/src/session/NodeSession.ts +21 -0
  151. package/src/session/SecureSession.ts +2 -0
  152. package/src/session/case/CaseClient.ts +3 -23
  153. package/src/session/case/CaseServer.ts +15 -20
  154. package/src/session/pase/PaseClient.ts +1 -1
  155. package/src/session/pase/PaseServer.ts +3 -2
@@ -4,23 +4,37 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { resolvePathForSpecifier } from "#action/index.js";
7
8
  import { Interactable, InteractionSession } from "#action/Interactable.js";
8
- import { Invoke } from "#action/request/Invoke.js";
9
+ import { ClientInvoke, Invoke } from "#action/request/Invoke.js";
9
10
  import { Read } from "#action/request/Read.js";
10
11
  import { Subscribe } from "#action/request/Subscribe.js";
11
12
  import { Write } from "#action/request/Write.js";
12
- import { InvokeResult } from "#action/response/InvokeResult.js";
13
+ import { DecodedInvokeResult, InvokeResult } from "#action/response/InvokeResult.js";
13
14
  import { ReadResult } from "#action/response/ReadResult.js";
14
15
  import { SubscribeResult } from "#action/response/SubscribeResult.js";
15
16
  import { WriteResult } from "#action/response/WriteResult.js";
16
- import { BasicSet, Environment, Environmental, ImplementationError, PromiseQueue, Seconds } from "#general";
17
+ import {
18
+ BasicSet,
19
+ Diagnostic,
20
+ Duration,
21
+ Environment,
22
+ Environmental,
23
+ ImplementationError,
24
+ isObject,
25
+ Logger,
26
+ PromiseQueue,
27
+ Seconds,
28
+ } from "#general";
17
29
  import { InteractionClientMessenger, MessageType } from "#interaction/InteractionMessenger.js";
18
30
  import { InteractionQueue } from "#peer/InteractionQueue.js";
19
31
  import { ExchangeProvider } from "#protocol/ExchangeProvider.js";
20
- import { TlvSubscribeResponse } from "#types";
32
+ import { Status, TlvNoResponse, TlvSubscribeResponse } from "#types";
21
33
  import { ClientSubscriptions } from "./ClientSubscriptions.js";
22
34
  import { InputChunk } from "./InputChunk.js";
23
35
 
36
+ const logger = Logger.get("ClientInteraction");
37
+
24
38
  export interface ClientInteractionContext {
25
39
  exchanges: ExchangeProvider;
26
40
  subscriptions: ClientSubscriptions;
@@ -29,6 +43,9 @@ export interface ClientInteractionContext {
29
43
 
30
44
  export const DEFAULT_MIN_INTERVAL_FLOOR = Seconds(1);
31
45
 
46
+ const DEFAULT_TIMED_REQUEST_TIMEOUT = Seconds(10);
47
+ const DEFAULT_MINIMUM_RESPONSE_TIMEOUT_WITH_FAILSAFE = Seconds(30);
48
+
32
49
  /**
33
50
  * Client-side implementation of the Matter protocol.
34
51
  */
@@ -74,37 +91,85 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
74
91
  }
75
92
 
76
93
  async *read(request: Read, _session?: SessionT): ReadResult {
94
+ const readPathsCount = (request.attributeRequests?.length ?? 0) + (request.eventRequests?.length ?? 0);
95
+ if (readPathsCount > 9) {
96
+ logger.debug(
97
+ "Read interactions with more then 9 paths might be not allowed by the device. Consider splitting then into several read requests.",
98
+ );
99
+ }
100
+
101
+ this.#begin(request);
102
+
103
+ let messenger: undefined | InteractionClientMessenger;
77
104
  try {
78
- this.#begin(request);
79
- const messenger = await InteractionClientMessenger.create(this.#exchanges);
105
+ messenger = await InteractionClientMessenger.create(this.#exchanges);
80
106
 
107
+ logger.debug("Read »", messenger.exchange.via, request);
81
108
  await messenger.sendReadRequest(request);
82
109
 
110
+ let attributeReportCount = 0;
111
+ let eventReportCount = 0;
112
+
83
113
  for await (const report of messenger.readDataReports()) {
114
+ attributeReportCount += report.attributeReports?.length ?? 0;
115
+ eventReportCount += report.eventReports?.length ?? 0;
84
116
  yield InputChunk(report);
85
117
  }
118
+
119
+ logger.debug(
120
+ "Read «",
121
+ messenger.exchange.via,
122
+ Diagnostic.weak(
123
+ attributeReportCount + eventReportCount === 0
124
+ ? "(empty)"
125
+ : Diagnostic.dict({ attributes: attributeReportCount, events: eventReportCount }),
126
+ ),
127
+ );
86
128
  } finally {
129
+ await messenger?.close();
87
130
  this.#end(request);
88
131
  }
89
132
  }
90
133
 
134
+ /**
135
+ * Write chosen attributes remotely to the node.
136
+ * The returned attribute write status information is returned. No error is thrown for individual attribute write
137
+ * failures.
138
+ */
91
139
  async write<T extends Write>(request: T, _session?: SessionT): WriteResult<T> {
140
+ this.#begin(request);
141
+
92
142
  let messenger: undefined | InteractionClientMessenger;
93
143
  try {
94
- this.#begin(request);
95
144
  messenger = await InteractionClientMessenger.create(this.#exchanges);
145
+
146
+ if (request.timedRequest) {
147
+ await messenger.sendTimedRequest(request.timeout ?? DEFAULT_TIMED_REQUEST_TIMEOUT);
148
+ }
149
+
150
+ logger.info("Write »", messenger.exchange.via, request);
151
+
96
152
  const response = await messenger.sendWriteCommand(request);
97
153
  if (request.suppressResponse) {
98
154
  return undefined as Awaited<WriteResult<T>>;
99
155
  }
100
156
  if (!response || !response.writeResponses?.length) {
101
- return new Array<WriteResult.AttributeStatus>() as Awaited<WriteResult<T>>;
102
- } else {
103
- return response.writeResponses.map(
104
- ({
105
- path: { nodeId, endpointId, clusterId, attributeId, listIndex },
106
- status: { status, clusterStatus },
107
- }) => ({
157
+ return [] as Awaited<WriteResult<T>>;
158
+ }
159
+
160
+ let successCount = 0;
161
+ let failureCount = 0;
162
+ const result = response.writeResponses.map(
163
+ ({
164
+ path: { nodeId, endpointId, clusterId, attributeId, listIndex },
165
+ status: { status, clusterStatus },
166
+ }) => {
167
+ if (status === Status.Success) {
168
+ successCount++;
169
+ } else {
170
+ failureCount++;
171
+ }
172
+ return {
108
173
  kind: "attr-status",
109
174
  path: {
110
175
  nodeId,
@@ -115,21 +180,53 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
115
180
  },
116
181
  status,
117
182
  clusterStatus,
118
- }),
119
- ) as Awaited<WriteResult<T>>;
120
- }
183
+ };
184
+ },
185
+ ) as Awaited<WriteResult<T>>;
186
+
187
+ logger.info(
188
+ "Write «",
189
+ messenger.exchange.via,
190
+ Diagnostic.weak(
191
+ successCount + failureCount === 0
192
+ ? "(empty)"
193
+ : Diagnostic.dict({ success: successCount, failure: failureCount }),
194
+ ),
195
+ );
196
+
197
+ return result;
121
198
  } finally {
122
199
  await messenger?.close();
123
200
  this.#end(request);
124
201
  }
125
202
  }
126
203
 
127
- async *invoke(request: Invoke, _session?: SessionT): InvokeResult {
128
- let messenger: undefined | InteractionClientMessenger;
204
+ async *invoke(request: ClientInvoke, _session?: SessionT): DecodedInvokeResult {
205
+ this.#begin(request);
206
+
207
+ let messenger: InteractionClientMessenger | undefined;
129
208
  try {
130
- this.#begin(request);
131
209
  messenger = await InteractionClientMessenger.create(this.#exchanges);
132
- const result = await messenger.sendInvokeCommand(request);
210
+
211
+ if (request.timedRequest) {
212
+ await messenger.sendTimedRequest(request.timeout ?? DEFAULT_TIMED_REQUEST_TIMEOUT);
213
+ }
214
+
215
+ logger.info(
216
+ "Invoke »",
217
+ messenger.exchange.via,
218
+ Diagnostic.asFlags({ suppressResponse: request.suppressResponse, timed: request.timedRequest }),
219
+ request,
220
+ );
221
+
222
+ const { expectedProcessingTime, useExtendedFailSafeMessageResponseTimeout } = request;
223
+ const result = await messenger.sendInvokeCommand(
224
+ request,
225
+ expectedProcessingTime ??
226
+ (useExtendedFailSafeMessageResponseTimeout
227
+ ? DEFAULT_MINIMUM_RESPONSE_TIMEOUT_WITH_FAILSAFE
228
+ : undefined),
229
+ );
133
230
  if (!request.suppressResponse) {
134
231
  if (result && result.invokeResponses?.length) {
135
232
  const chunk: InvokeResult.Chunk = result.invokeResponses
@@ -140,15 +237,38 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
140
237
  commandRef,
141
238
  commandFields,
142
239
  } = response.command;
143
- const res: InvokeResult.CommandResponse = {
240
+ const cmd = request.commands.get(commandRef);
241
+ if (!cmd) {
242
+ throw new ImplementationError(
243
+ `No response schema found for commandRef ${commandRef} (endpoint ${endpointId}, cluster ${clusterId}, command ${commandId})`,
244
+ );
245
+ }
246
+ const responseSchema = Invoke.commandOf(cmd).responseSchema;
247
+ if (commandFields === undefined && responseSchema !== TlvNoResponse) {
248
+ throw new ImplementationError(
249
+ `No command fields found for commandRef ${commandRef} (endpoint ${endpointId}, cluster ${clusterId}, command ${commandId})`,
250
+ );
251
+ }
252
+
253
+ const data =
254
+ commandFields === undefined ? undefined : responseSchema.decodeTlv(commandFields);
255
+
256
+ logger.info(
257
+ "Invoke «",
258
+ messenger!.exchange.via,
259
+ Diagnostic.strong(resolvePathForSpecifier(cmd)),
260
+ isObject(data) ? Diagnostic.dict(data) : Diagnostic.weak("(no payload)"),
261
+ );
262
+
263
+ const res: InvokeResult.DecodedCommandResponse = {
144
264
  kind: "cmd-response",
145
265
  path: {
146
266
  endpointId: endpointId!,
147
- clusterId: clusterId,
148
- commandId: commandId,
267
+ clusterId,
268
+ commandId,
149
269
  },
150
270
  commandRef,
151
- data: commandFields!, // TODO add decoding
271
+ data,
152
272
  };
153
273
  return res;
154
274
  } else if (response.status !== undefined) {
@@ -187,12 +307,37 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
187
307
  }
188
308
 
189
309
  async subscribe(request: Subscribe, _session?: SessionT): SubscribeResult {
310
+ const subscriptionPathsCount = (request.attributeRequests?.length ?? 0) + (request.eventRequests?.length ?? 0);
311
+ if (subscriptionPathsCount > 3) {
312
+ logger.debug("Subscribe interactions with more then 3 paths might be not allowed by the device.");
313
+ }
314
+
315
+ if (!request.keepSubscriptions) {
316
+ for (const subscription of this.#subscriptions) {
317
+ logger.debug(
318
+ `Removing subscription with ID ${subscription.subscriptionId} because new subscription replaces it`,
319
+ );
320
+ subscription.close();
321
+ }
322
+ }
323
+
324
+ this.#begin(request);
325
+
190
326
  let messenger: undefined | InteractionClientMessenger;
191
327
  try {
192
- this.#begin(request);
193
-
194
328
  messenger = await InteractionClientMessenger.create(this.#exchanges);
195
329
 
330
+ logger.info(
331
+ "Subscribe »",
332
+ messenger.exchange.via,
333
+ Diagnostic.asFlags({ keepSubscriptions: request.keepSubscriptions }),
334
+ Diagnostic.dict({
335
+ min: Duration.format(request.minIntervalFloor),
336
+ max: Duration.format(request.maxIntervalCeiling),
337
+ }),
338
+ request,
339
+ );
340
+
196
341
  await messenger.sendSubscribeRequest({
197
342
  minIntervalFloorSeconds: Seconds.of(DEFAULT_MIN_INTERVAL_FLOOR),
198
343
  maxIntervalCeilingSeconds: Seconds.of(DEFAULT_MIN_INTERVAL_FLOOR), // TODO use better max fallback
@@ -204,6 +349,15 @@ export class ClientInteraction<SessionT extends InteractionSession = Interaction
204
349
  const responseMessage = await messenger.nextMessage(MessageType.SubscribeResponse);
205
350
  const response = TlvSubscribeResponse.decode(responseMessage.payload);
206
351
 
352
+ logger.info(
353
+ "Subscription successful «",
354
+ messenger.exchange.via,
355
+ Diagnostic.dict({
356
+ subId: response.subscriptionId,
357
+ interval: Duration.format(Seconds(response.maxInterval)),
358
+ }),
359
+ );
360
+
207
361
  return this.#subscriptions.add(request, response);
208
362
  } finally {
209
363
  await messenger?.close();
@@ -4,32 +4,46 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { Diagnostic, Duration, isObject } from "#general";
7
8
  import { FALLBACK_INTERACTIONMODEL_REVISION } from "#session/Session.js";
8
- import { ClusterType, CommandData, InvokeRequest, TlvSchema, TlvStream, TypeFromSchema } from "#types";
9
+ import { ClusterType, CommandData, FabricIndex, InvokeRequest, ObjectSchema, TlvSchema, TypeFromSchema } from "#types";
9
10
  import { MalformedRequestError } from "./MalformedRequestError.js";
10
- import { Specifier } from "./Specifier.js";
11
+ import { resolvePathForSpecifier, Specifier } from "./Specifier.js";
12
+
13
+ export interface InvokeCommandData extends CommandData {
14
+ timed?: boolean;
15
+ }
11
16
 
12
17
  export interface Invoke extends InvokeRequest {
13
18
  /** Timeout only relevant for Client Interactions */
14
- timeout?: number;
19
+ timeout?: Duration;
20
+ expectedProcessingTime?: Duration;
21
+ useExtendedFailSafeMessageResponseTimeout?: boolean;
22
+ }
23
+
24
+ export interface ClientInvoke extends Invoke {
25
+ commands: Map<number | undefined, Invoke.CommandRequest<any>>;
15
26
  }
16
27
 
17
28
  /**
18
29
  * Request invocation of one or more commands.
19
30
  */
20
- export function Invoke(options: Invoke.Definition): Invoke;
31
+ export function Invoke(options: Invoke.Definition): ClientInvoke;
21
32
 
22
33
  /**
23
34
  * Request invocation multiple commands with defined options
24
35
  */
25
- export function Invoke(options: Invoke.Definition, ...commands: CommandData[]): Invoke;
36
+ export function Invoke(options: Invoke.Definition, ...commands: Invoke.CommandRequest<any>[]): ClientInvoke;
26
37
 
27
38
  /**
28
39
  * Request invocation multiple commands as list of Commands with default options.
29
40
  */
30
- export function Invoke(...commands: CommandData[]): Invoke;
41
+ export function Invoke(...commands: Invoke.CommandRequest<any>[]): ClientInvoke;
31
42
 
32
- export function Invoke(optionsOrData: Invoke.Definition | CommandData, ...commands: CommandData[]): Invoke {
43
+ export function Invoke(
44
+ optionsOrData: Invoke.Definition | Invoke.CommandRequest<any>,
45
+ ...commands: Invoke.CommandRequest<any>[]
46
+ ): ClientInvoke {
33
47
  if (optionsOrData === undefined) {
34
48
  throw new MalformedRequestError(`Invocation requires at least one command`);
35
49
  }
@@ -37,62 +51,141 @@ export function Invoke(optionsOrData: Invoke.Definition | CommandData, ...comman
37
51
  let options;
38
52
  if ("commands" in optionsOrData) {
39
53
  options = optionsOrData;
54
+ commands = [...optionsOrData.commands, ...commands];
40
55
  } else {
41
56
  commands = [optionsOrData, ...commands];
42
57
  options = {};
43
58
  }
44
59
 
45
60
  const {
46
- commands: invokeRequests = [],
47
61
  interactionModelRevision = FALLBACK_INTERACTIONMODEL_REVISION,
48
62
  suppressResponse = false,
49
- timed: timedRequest = false,
63
+ timeout,
64
+ expectedProcessingTime,
65
+ useExtendedFailSafeMessageResponseTimeout = false,
66
+ skipValidation = false,
50
67
  } = options;
68
+ let timedRequest = !!options.timed || !!timeout;
51
69
 
52
- if (commands.length) {
53
- for (const entry of commands) {
54
- invokeRequests.push(entry);
55
- }
70
+ if (!commands?.length) {
71
+ throw new MalformedRequestError(`Invocation requires at least one command`);
56
72
  }
57
73
 
58
- if (!invokeRequests?.length) {
59
- throw new MalformedRequestError(`Invocation requires at least one command`);
74
+ if (commands.length > 1) {
75
+ const commandRefs = new Set<number>();
76
+ for (const { commandRef } of commands) {
77
+ if (commandRef === undefined) {
78
+ throw new MalformedRequestError(`CommandRef required when invoking multiple commands`);
79
+ }
80
+ if (commandRefs.has(commandRef)) {
81
+ throw new MalformedRequestError(`Duplicate commandRef ${commandRef} in multiple command invoke`);
82
+ }
83
+ commandRefs.add(commandRef);
84
+ }
60
85
  }
61
86
 
87
+ const commandMap = new Map<number | undefined, Invoke.CommandRequest<any>>();
88
+ const invokeRequests: InvokeCommandData[] = commands.map(cmd => {
89
+ const cmdData = Invoke.Command(cmd, skipValidation);
90
+ timedRequest ||= !!cmdData.timed;
91
+ commandMap.set(cmdData.commandRef, cmd);
92
+ return cmdData;
93
+ });
94
+
62
95
  return {
96
+ timedRequest,
97
+ timeout,
63
98
  invokeRequests,
64
99
  interactionModelRevision,
65
100
  suppressResponse,
66
- timedRequest,
67
- };
101
+ expectedProcessingTime,
102
+ useExtendedFailSafeMessageResponseTimeout,
103
+
104
+ // Additional meta-data for client side processing
105
+ commands: commandMap,
106
+ [Diagnostic.value]: () =>
107
+ Diagnostic.list(
108
+ commands.map(cmd => {
109
+ const { commandRef } = cmd;
110
+ const fields = "fields" in cmd ? cmd.fields : undefined;
111
+
112
+ return [
113
+ Diagnostic.strong(resolvePathForSpecifier(cmd)),
114
+ "with",
115
+ isObject(fields) ? Diagnostic.dict(fields) : "(no payload)",
116
+ commandRef !== undefined ? `(ref ${commandRef})` : "",
117
+ ];
118
+ }),
119
+ ),
120
+ } as ClientInvoke;
68
121
  }
69
122
 
70
123
  export namespace Invoke {
71
124
  export interface Definition {
72
- commands: CommandData[];
125
+ /** List of commands to invoke */
126
+ commands: Invoke.CommandRequest<any>[];
127
+
128
+ /** Tell the server to not send a response */
73
129
  suppressResponse?: boolean;
130
+
131
+ /** Whether this is sent as a timed request, if no timeout is specified a default is used */
74
132
  timed?: boolean;
133
+
134
+ /** Timeout when sent as a timed request, if timed flag is not set it will be set automatically */
135
+ timeout?: Duration;
136
+
137
+ /** Interaction model revision to use, if not specified a default is used */
75
138
  interactionModelRevision?: number;
139
+
140
+ /** Processing time of the command of the server assumed for this invoke. If not set a default is used */
141
+ expectedProcessingTime?: Duration;
142
+
143
+ /** Whether to use extended timeout for fail-safe messages. Overwrites the expectedProcessingTime if both are set */
144
+ useExtendedFailSafeMessageResponseTimeout?: boolean;
145
+
146
+ /** Whether to skip validation of command fields against schema */
147
+ skipValidation?: boolean;
76
148
  }
77
149
 
78
- export function Command<const C extends ClusterType>(request: Invoke.CommandRequest<C>): CommandData {
150
+ export function Command<const C extends ClusterType>(
151
+ request: Invoke.CommandRequest<C>,
152
+ skipValidation = false,
153
+ ): InvokeCommandData {
79
154
  const command = Invoke.commandOf(request);
155
+ const { requestSchema, requestId, timed } = command;
156
+ const { commandRef } = request;
157
+
158
+ let fields: any = "fields" in request ? request.fields : undefined;
159
+ if (requestSchema instanceof ObjectSchema) {
160
+ if (fields === undefined) {
161
+ // If developer did not provide a request object, create an empty one if it needs to be an object
162
+ // This can happen when all object properties are optional
163
+ fields = {};
164
+ }
165
+ if (requestSchema.isFabricScoped && fields.fabricIndex === undefined) {
166
+ fields.fabricIndex = FabricIndex.NO_FABRIC;
167
+ }
168
+ }
80
169
 
81
- let commandFields: TlvStream | undefined;
82
-
83
- if ("fields" in request) {
84
- commandFields = command.requestSchema.encodeTlv(request.fields, { forWriteInteraction: true });
170
+ if (!skipValidation) {
171
+ requestSchema.validate(fields);
85
172
  }
86
173
 
87
- const result: CommandData = {
174
+ const commandFields = requestSchema.encodeTlv(fields);
175
+
176
+ const result: InvokeCommandData = {
88
177
  commandPath: {
178
+ // Endpoint id is added below if not wildcard
89
179
  clusterId: Specifier.clusterFor(request.cluster).id,
90
- commandId: command.requestId,
180
+ commandId: requestId,
91
181
  },
92
182
  commandFields,
183
+ timed: timed ?? false,
184
+ commandRef,
93
185
  };
94
186
 
95
- const endpointId = Specifier.endpointIdOf(request);
187
+ // Optional endpoint is handled by the Specifier utility, so we can just cast here
188
+ const endpointId = Specifier.endpointIdOf(request as ConcreteCommandRequest);
96
189
  if (endpointId !== undefined) {
97
190
  result.commandPath.endpointId = endpointId;
98
191
  }
@@ -103,12 +196,41 @@ export namespace Invoke {
103
196
  export type CommandRequest<
104
197
  C extends Specifier.Cluster = Specifier.Cluster,
105
198
  CMD extends Specifier.Command<Specifier.ClusterFor<C>> = Specifier.Command<Specifier.ClusterFor<C>>,
199
+ > = ConcreteCommandRequest<C, CMD> | WildcardCommandRequest<C, CMD>;
200
+
201
+ export type ConcreteCommandRequest<
202
+ C extends Specifier.Cluster = Specifier.Cluster,
203
+ CMD extends Specifier.Command<Specifier.ClusterFor<C>> = Specifier.Command<Specifier.ClusterFor<C>>,
204
+ > = WildcardCommandRequest<C, CMD> & { endpoint: Specifier.Endpoint };
205
+
206
+ export function ConcreteCommandRequest<const C extends ClusterType>(
207
+ data: Invoke.ConcreteCommandRequest<C>,
208
+ ): Invoke.ConcreteCommandRequest<any> {
209
+ if (data.endpoint === undefined) {
210
+ throw new MalformedRequestError(`ConcreteCommandRequest requires an endpoint`);
211
+ }
212
+ return data;
213
+ }
214
+
215
+ export type WildcardCommandRequest<
216
+ C extends Specifier.Cluster = Specifier.Cluster,
217
+ CMD extends Specifier.Command<Specifier.ClusterFor<C>> = Specifier.Command<Specifier.ClusterFor<C>>,
106
218
  > = {
107
- endpoint?: Specifier.Endpoint;
108
219
  cluster: C;
109
220
  command: CMD;
221
+ commandName?: string;
222
+ commandRef?: number;
110
223
  } & Fields<Specifier.CommandFor<Specifier.ClusterFor<C>, CMD>["requestSchema"]>;
111
224
 
225
+ export function WildcardCommandRequest<const C extends ClusterType>(
226
+ data: Invoke.WildcardCommandRequest<C>,
227
+ ): Invoke.WildcardCommandRequest<any> {
228
+ if ("endpoint" in data && data.endpoint !== undefined) {
229
+ throw new MalformedRequestError(`ConcreteCommandRequest must not have an endpoint`);
230
+ }
231
+ return data;
232
+ }
233
+
112
234
  export function commandOf<const R extends CommandRequest>(request: R): ClusterType.Command {
113
235
  if (typeof request.command === "string") {
114
236
  const cluster = Specifier.clusterFor(request.cluster);
@@ -4,9 +4,9 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { camelize } from "#general";
8
- import { FALLBACK_INTERACTIONMODEL_REVISION } from "#session/Session.js";
9
- import type {
7
+ import { camelize, Diagnostic } from "#general";
8
+ import { Specification } from "#model";
9
+ import {
10
10
  AttributePath,
11
11
  ClusterType,
12
12
  DataVersionFilter,
@@ -16,7 +16,7 @@ import type {
16
16
  ReadRequest,
17
17
  } from "#types";
18
18
  import { MalformedRequestError } from "./MalformedRequestError.js";
19
- import { Specifier } from "./Specifier.js";
19
+ import { resolvePathForSpecifier, Specifier } from "./Specifier.js";
20
20
 
21
21
  /**
22
22
  * A read request formulated using Matter protocol semantics.
@@ -47,10 +47,30 @@ export function Read(optionsOrSelector: Read.Options | Read.Selector, ...selecto
47
47
  }
48
48
  let { attributes: attributeRequests, versionFilters, events: eventRequests, eventFilters } = options;
49
49
 
50
- const result: Read = {
50
+ const result = {
51
51
  isFabricFiltered: options.fabricFilter ?? true,
52
- interactionModelRevision: options.interactionModelRevision ?? FALLBACK_INTERACTIONMODEL_REVISION,
53
- };
52
+ interactionModelRevision: options.interactionModelRevision ?? Specification.INTERACTION_MODEL_REVISION,
53
+ [Diagnostic.value]: () =>
54
+ Diagnostic.dict({
55
+ attributes: attributeRequests?.length
56
+ ? attributeRequests?.map(path => resolvePathForSpecifier(path)).join(", ")
57
+ : undefined,
58
+ events: eventRequests?.length
59
+ ? eventRequests?.map(path => resolvePathForSpecifier(path)).join(", ")
60
+ : undefined,
61
+ dataVersionFilters: versionFilters?.length
62
+ ? versionFilters
63
+ .map(
64
+ ({ path: { endpointId, clusterId }, dataVersion }) =>
65
+ `${endpointId}/${clusterId}=${dataVersion}`,
66
+ )
67
+ .join(", ")
68
+ : undefined,
69
+ eventFilters: eventFilters?.length
70
+ ? eventFilters.map(({ nodeId, eventMin }) => `${nodeId}=${eventMin}`).join(", ")
71
+ : undefined,
72
+ }),
73
+ } as Read;
54
74
 
55
75
  for (const selector of selectors) {
56
76
  reifySelector(selector);