@matter/protocol 0.13.1-alpha.0-20250509-28e1567e1 → 0.13.1-alpha.0-20250511-74ef153aa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/action/protocols.d.ts +59 -2
- package/dist/cjs/action/protocols.d.ts.map +1 -1
- package/dist/cjs/action/request/Read.d.ts +2 -2
- package/dist/cjs/action/request/Read.d.ts.map +1 -1
- package/dist/cjs/action/request/Read.js +4 -4
- package/dist/cjs/action/request/Read.js.map +1 -1
- package/dist/cjs/action/response/ReadResult.d.ts +6 -2
- package/dist/cjs/action/response/ReadResult.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeResponse.d.ts +46 -17
- package/dist/cjs/action/server/AttributeResponse.d.ts.map +1 -1
- package/dist/cjs/action/server/AttributeResponse.js +128 -110
- package/dist/cjs/action/server/AttributeResponse.js.map +2 -2
- package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts +36 -0
- package/dist/cjs/action/server/AttributeSubscriptionResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/AttributeSubscriptionResponse.js +86 -0
- package/dist/cjs/action/server/AttributeSubscriptionResponse.js.map +6 -0
- package/dist/cjs/action/server/DataResponse.d.ts +45 -0
- package/dist/cjs/action/server/DataResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/DataResponse.js +69 -0
- package/dist/cjs/action/server/DataResponse.js.map +6 -0
- package/dist/cjs/action/server/EventResponse.d.ts +28 -0
- package/dist/cjs/action/server/EventResponse.d.ts.map +1 -0
- package/dist/cjs/action/server/EventResponse.js +318 -0
- package/dist/cjs/action/server/EventResponse.js.map +6 -0
- package/dist/cjs/action/server/ServerInteraction.d.ts.map +1 -1
- package/dist/cjs/action/server/ServerInteraction.js +15 -2
- package/dist/cjs/action/server/ServerInteraction.js.map +1 -1
- package/dist/cjs/action/server/index.d.ts +3 -0
- package/dist/cjs/action/server/index.d.ts.map +1 -1
- package/dist/cjs/action/server/index.js +3 -0
- package/dist/cjs/action/server/index.js.map +1 -1
- package/dist/cjs/events/OccurrenceManager.d.ts +20 -11
- package/dist/cjs/events/OccurrenceManager.d.ts.map +1 -1
- package/dist/cjs/events/OccurrenceManager.js +113 -74
- package/dist/cjs/events/OccurrenceManager.js.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.d.ts +14 -2
- package/dist/cjs/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/cjs/interaction/InteractionMessenger.js +87 -3
- package/dist/cjs/interaction/InteractionMessenger.js.map +1 -1
- package/dist/cjs/interaction/index.d.ts +0 -1
- package/dist/cjs/interaction/index.d.ts.map +1 -1
- package/dist/cjs/interaction/index.js +0 -1
- package/dist/cjs/interaction/index.js.map +1 -1
- package/dist/cjs/peer/ControllerCommissioningFlow.js +1 -1
- package/dist/cjs/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/cjs/protocol/MessageExchange.js +11 -1
- package/dist/cjs/protocol/MessageExchange.js.map +1 -1
- package/dist/esm/action/protocols.d.ts +59 -2
- package/dist/esm/action/protocols.d.ts.map +1 -1
- package/dist/esm/action/request/Read.d.ts +2 -2
- package/dist/esm/action/request/Read.d.ts.map +1 -1
- package/dist/esm/action/request/Read.js +4 -4
- package/dist/esm/action/request/Read.js.map +1 -1
- package/dist/esm/action/response/ReadResult.d.ts +6 -2
- package/dist/esm/action/response/ReadResult.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeResponse.d.ts +46 -17
- package/dist/esm/action/server/AttributeResponse.d.ts.map +1 -1
- package/dist/esm/action/server/AttributeResponse.js +129 -113
- package/dist/esm/action/server/AttributeResponse.js.map +1 -1
- package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts +36 -0
- package/dist/esm/action/server/AttributeSubscriptionResponse.d.ts.map +1 -0
- package/dist/esm/action/server/AttributeSubscriptionResponse.js +66 -0
- package/dist/esm/action/server/AttributeSubscriptionResponse.js.map +6 -0
- package/dist/esm/action/server/DataResponse.d.ts +45 -0
- package/dist/esm/action/server/DataResponse.d.ts.map +1 -0
- package/dist/esm/action/server/DataResponse.js +49 -0
- package/dist/esm/action/server/DataResponse.js.map +6 -0
- package/dist/esm/action/server/EventResponse.d.ts +28 -0
- package/dist/esm/action/server/EventResponse.d.ts.map +1 -0
- package/dist/esm/action/server/EventResponse.js +305 -0
- package/dist/esm/action/server/EventResponse.js.map +6 -0
- package/dist/esm/action/server/ServerInteraction.d.ts.map +1 -1
- package/dist/esm/action/server/ServerInteraction.js +16 -3
- package/dist/esm/action/server/ServerInteraction.js.map +1 -1
- package/dist/esm/action/server/index.d.ts +3 -0
- package/dist/esm/action/server/index.d.ts.map +1 -1
- package/dist/esm/action/server/index.js +3 -0
- package/dist/esm/action/server/index.js.map +1 -1
- package/dist/esm/events/OccurrenceManager.d.ts +20 -11
- package/dist/esm/events/OccurrenceManager.d.ts.map +1 -1
- package/dist/esm/events/OccurrenceManager.js +117 -80
- package/dist/esm/events/OccurrenceManager.js.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.d.ts +14 -2
- package/dist/esm/interaction/InteractionMessenger.d.ts.map +1 -1
- package/dist/esm/interaction/InteractionMessenger.js +87 -3
- package/dist/esm/interaction/InteractionMessenger.js.map +1 -1
- package/dist/esm/interaction/index.d.ts +0 -1
- package/dist/esm/interaction/index.d.ts.map +1 -1
- package/dist/esm/interaction/index.js +0 -1
- package/dist/esm/interaction/index.js.map +1 -1
- package/dist/esm/peer/ControllerCommissioningFlow.js +1 -1
- package/dist/esm/protocol/MessageExchange.d.ts.map +1 -1
- package/dist/esm/protocol/MessageExchange.js +11 -1
- package/dist/esm/protocol/MessageExchange.js.map +1 -1
- package/package.json +6 -6
- package/src/action/protocols.ts +68 -2
- package/src/action/request/Read.ts +2 -2
- package/src/action/response/ReadResult.ts +8 -1
- package/src/action/server/AttributeResponse.ts +145 -118
- package/src/action/server/AttributeSubscriptionResponse.ts +90 -0
- package/src/action/server/DataResponse.ts +70 -0
- package/src/action/server/EventResponse.ts +381 -0
- package/src/action/server/ServerInteraction.ts +18 -4
- package/src/action/server/index.ts +3 -0
- package/src/events/OccurrenceManager.ts +126 -100
- package/src/interaction/InteractionMessenger.ts +93 -8
- package/src/interaction/index.ts +0 -1
- package/src/peer/ControllerCommissioningFlow.ts +1 -1
- package/src/protocol/MessageExchange.ts +13 -1
- package/dist/cjs/interaction/ServerSubscription.d.ts +0 -116
- package/dist/cjs/interaction/ServerSubscription.d.ts.map +0 -1
- package/dist/cjs/interaction/ServerSubscription.js +0 -778
- package/dist/cjs/interaction/ServerSubscription.js.map +0 -6
- package/dist/esm/interaction/ServerSubscription.d.ts +0 -116
- package/dist/esm/interaction/ServerSubscription.d.ts.map +0 -1
- package/dist/esm/interaction/ServerSubscription.js +0 -778
- package/dist/esm/interaction/ServerSubscription.js.map +0 -6
- package/src/interaction/ServerSubscription.ts +0 -1038
|
@@ -1,1038 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2022-2025 Matter.js Authors
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { NumberedOccurrence } from "#events/Occurrence.js";
|
|
8
|
-
import {
|
|
9
|
-
Diagnostic,
|
|
10
|
-
InternalError,
|
|
11
|
-
Logger,
|
|
12
|
-
MatterAggregateError,
|
|
13
|
-
MatterError,
|
|
14
|
-
MaybePromise,
|
|
15
|
-
NetworkError,
|
|
16
|
-
NoResponseTimeoutError,
|
|
17
|
-
Time,
|
|
18
|
-
Timer,
|
|
19
|
-
isObject,
|
|
20
|
-
} from "#general";
|
|
21
|
-
import { Specification } from "#model";
|
|
22
|
-
import { PeerAddress } from "#peer/PeerAddress.js";
|
|
23
|
-
import type { MessageExchange } from "#protocol/MessageExchange.js";
|
|
24
|
-
import { SecureSession } from "#session/SecureSession.js";
|
|
25
|
-
import {
|
|
26
|
-
EndpointNumber,
|
|
27
|
-
EventNumber,
|
|
28
|
-
INTERACTION_PROTOCOL_ID,
|
|
29
|
-
StatusCode,
|
|
30
|
-
StatusResponseError,
|
|
31
|
-
TlvAttributePath,
|
|
32
|
-
TlvAttributeStatus,
|
|
33
|
-
TlvEventFilter,
|
|
34
|
-
TlvEventPath,
|
|
35
|
-
TlvEventStatus,
|
|
36
|
-
TlvSchema,
|
|
37
|
-
TypeFromSchema,
|
|
38
|
-
} from "#types";
|
|
39
|
-
import { AnyAttributeServer, FabricScopedAttributeServer } from "../cluster/server/AttributeServer.js";
|
|
40
|
-
import { AnyEventServer, FabricSensitiveEventServer } from "../cluster/server/EventServer.js";
|
|
41
|
-
import { NoChannelError } from "../protocol/ChannelManager.js";
|
|
42
|
-
import { EventReportPayload } from "./AttributeDataEncoder.js";
|
|
43
|
-
import {
|
|
44
|
-
AttributePath,
|
|
45
|
-
AttributeWithPath,
|
|
46
|
-
EventPath,
|
|
47
|
-
EventWithPath,
|
|
48
|
-
InteractionEndpointStructure,
|
|
49
|
-
attributePathToId,
|
|
50
|
-
clusterPathToId,
|
|
51
|
-
eventPathToId,
|
|
52
|
-
} from "./InteractionEndpointStructure.js";
|
|
53
|
-
import { InteractionServerMessenger } from "./InteractionMessenger.js";
|
|
54
|
-
import { Subscription, SubscriptionCriteria } from "./Subscription.js";
|
|
55
|
-
|
|
56
|
-
const logger = Logger.get("ServerSubscription");
|
|
57
|
-
|
|
58
|
-
// We use 3 minutes as global max interval because with 60 min as defined by spec the timeframe until the controller
|
|
59
|
-
// establishes a new subscription after e.g a reboot can be up to 60 min and the controller would assume that the value
|
|
60
|
-
// is unchanged. This is too long.
|
|
61
|
-
//
|
|
62
|
-
// chip-tool is not using the option to choose an appropriate interval and respect the 60 min for that and only uses the
|
|
63
|
-
// max sent by the controller which can lead to spamming the network with unneeded packages. So I decided for 3 minutes
|
|
64
|
-
// for now as a compromise until we have something better. This value is fine for non-battery devices and might be
|
|
65
|
-
// overridden for otherwise.
|
|
66
|
-
//
|
|
67
|
-
// To officially match the specs the developer needs to set these 60Minutes in the Subscription options!
|
|
68
|
-
export const MAX_INTERVAL_PUBLISHER_LIMIT_S = 60 * 60; /** 1 hour */
|
|
69
|
-
export const INTERNAL_INTERVAL_PUBLISHER_LIMIT_S = 3 * 60; /** 3 min */
|
|
70
|
-
export const MIN_INTERVAL_S = 2; // We do not send faster than 2 seconds
|
|
71
|
-
export const DEFAULT_RANDOMIZATION_WINDOW_S = 10; // 10 seconds
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Server options that control subscription handling.
|
|
75
|
-
*/
|
|
76
|
-
export interface ServerSubscriptionConfig {
|
|
77
|
-
/**
|
|
78
|
-
* Optional maximum subscription interval to use for sending subscription reports. It will be used if not too
|
|
79
|
-
* low and inside the range requested by the connected controller.
|
|
80
|
-
*/
|
|
81
|
-
maxIntervalSeconds: number;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Optional minimum subscription interval to use for sending subscription reports. It will be used when other
|
|
85
|
-
* calculated values are smaller than it. Use this to make sure your device hardware can handle the load and to
|
|
86
|
-
* set limits.
|
|
87
|
-
*/
|
|
88
|
-
minIntervalSeconds: number;
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Optional subscription randomization window to use for sending subscription reports. This specifies a window
|
|
92
|
-
* in seconds from which a random part is added to the calculated maximum interval to make sure that devices
|
|
93
|
-
* that get powered on in parallel not all send at the same timepoint.
|
|
94
|
-
*/
|
|
95
|
-
randomizationWindowSeconds: number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export namespace ServerSubscriptionConfig {
|
|
99
|
-
/**
|
|
100
|
-
* Validate options and set defaults.
|
|
101
|
-
*
|
|
102
|
-
* @returns the resulting options
|
|
103
|
-
*/
|
|
104
|
-
export function of(options?: Partial<ServerSubscriptionConfig>) {
|
|
105
|
-
return {
|
|
106
|
-
maxIntervalSeconds: options?.maxIntervalSeconds ?? INTERNAL_INTERVAL_PUBLISHER_LIMIT_S,
|
|
107
|
-
minIntervalSeconds: Math.max(options?.minIntervalSeconds ?? MIN_INTERVAL_S, MIN_INTERVAL_S),
|
|
108
|
-
randomizationWindowSeconds: options?.randomizationWindowSeconds ?? DEFAULT_RANDOMIZATION_WINDOW_S,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
interface AttributePathWithValueVersion<T> {
|
|
114
|
-
path: TypeFromSchema<typeof TlvAttributePath>;
|
|
115
|
-
attribute: AnyAttributeServer<T>;
|
|
116
|
-
schema: TlvSchema<T>;
|
|
117
|
-
value: T;
|
|
118
|
-
version: number;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface EventPathWithEventData<T> {
|
|
122
|
-
path: TypeFromSchema<typeof TlvEventPath>;
|
|
123
|
-
event: AnyEventServer<any, any>;
|
|
124
|
-
schema: TlvSchema<T>;
|
|
125
|
-
data: NumberedOccurrence;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Interface between {@link ServerSubscription} and the local Matter environment.
|
|
130
|
-
*/
|
|
131
|
-
export interface ServerSubscriptionContext {
|
|
132
|
-
session: SecureSession;
|
|
133
|
-
structure: InteractionEndpointStructure;
|
|
134
|
-
readAttribute(
|
|
135
|
-
path: AttributePath,
|
|
136
|
-
attribute: AnyAttributeServer<unknown>,
|
|
137
|
-
offline?: boolean,
|
|
138
|
-
): { version: number; value: unknown };
|
|
139
|
-
readEndpointAttributesForSubscription(
|
|
140
|
-
attributes: { path: AttributePath; attribute: AnyAttributeServer<unknown>; offline?: boolean }[],
|
|
141
|
-
): { path: AttributePath; attribute: AnyAttributeServer<unknown>; version: number; value: unknown }[];
|
|
142
|
-
readEvent(
|
|
143
|
-
path: EventPath,
|
|
144
|
-
event: AnyEventServer<any, any>,
|
|
145
|
-
eventFilters: TypeFromSchema<typeof TlvEventFilter>[] | undefined,
|
|
146
|
-
): Promise<NumberedOccurrence[]>;
|
|
147
|
-
initiateExchange(address: PeerAddress, protocolId: number): MessageExchange;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Implements the server side of a single subscription.
|
|
152
|
-
*/
|
|
153
|
-
export class ServerSubscription extends Subscription {
|
|
154
|
-
readonly #context: ServerSubscriptionContext;
|
|
155
|
-
readonly #structure: InteractionEndpointStructure;
|
|
156
|
-
|
|
157
|
-
#lastUpdateTimeMs = 0;
|
|
158
|
-
#updateTimer: Timer;
|
|
159
|
-
readonly #sendDelayTimer: Timer = Time.getTimer(`Subscription ${this.id} delay`, 50, () =>
|
|
160
|
-
this.#triggerSendUpdate(),
|
|
161
|
-
);
|
|
162
|
-
readonly #outstandingAttributeUpdates = new Map<string, AttributePathWithValueVersion<any>>();
|
|
163
|
-
readonly #outstandingEventUpdates = new Set<EventPathWithEventData<any>>();
|
|
164
|
-
readonly #attributeListeners = new Map<
|
|
165
|
-
string,
|
|
166
|
-
{
|
|
167
|
-
attribute: AnyAttributeServer<any>;
|
|
168
|
-
listener?: (value: any, version: number) => void;
|
|
169
|
-
}
|
|
170
|
-
>();
|
|
171
|
-
readonly #eventListeners = new Map<
|
|
172
|
-
string,
|
|
173
|
-
{
|
|
174
|
-
event: AnyEventServer<any, any>;
|
|
175
|
-
listener?: (newEvent: NumberedOccurrence) => void;
|
|
176
|
-
}
|
|
177
|
-
>();
|
|
178
|
-
#sendUpdatesActivated = false;
|
|
179
|
-
readonly #sendIntervalMs: number;
|
|
180
|
-
readonly #minIntervalFloorMs: number;
|
|
181
|
-
readonly #maxIntervalCeilingMs: number;
|
|
182
|
-
readonly #peerAddress: PeerAddress;
|
|
183
|
-
|
|
184
|
-
#sendNextUpdateImmediately = false;
|
|
185
|
-
#sendUpdateErrorCounter = 0;
|
|
186
|
-
readonly #attributeUpdatePromises = new Set<PromiseLike<void>>();
|
|
187
|
-
#currentUpdatePromise?: Promise<void>;
|
|
188
|
-
|
|
189
|
-
constructor(options: {
|
|
190
|
-
id: number;
|
|
191
|
-
context: ServerSubscriptionContext;
|
|
192
|
-
criteria: SubscriptionCriteria;
|
|
193
|
-
minIntervalFloorSeconds: number;
|
|
194
|
-
maxIntervalCeilingSeconds: number;
|
|
195
|
-
subscriptionOptions: ServerSubscriptionConfig;
|
|
196
|
-
useAsMaxInterval?: number;
|
|
197
|
-
useAsSendInterval?: number;
|
|
198
|
-
}) {
|
|
199
|
-
const {
|
|
200
|
-
id,
|
|
201
|
-
context,
|
|
202
|
-
criteria,
|
|
203
|
-
minIntervalFloorSeconds,
|
|
204
|
-
maxIntervalCeilingSeconds,
|
|
205
|
-
subscriptionOptions,
|
|
206
|
-
useAsMaxInterval,
|
|
207
|
-
useAsSendInterval,
|
|
208
|
-
} = options;
|
|
209
|
-
|
|
210
|
-
super(context.session, id, criteria);
|
|
211
|
-
this.#context = context;
|
|
212
|
-
this.#structure = context.structure;
|
|
213
|
-
|
|
214
|
-
this.#peerAddress = this.session.peerAddress;
|
|
215
|
-
this.#minIntervalFloorMs = minIntervalFloorSeconds * 1000;
|
|
216
|
-
this.#maxIntervalCeilingMs = maxIntervalCeilingSeconds * 1000;
|
|
217
|
-
|
|
218
|
-
let maxInterval: number;
|
|
219
|
-
let sendInterval: number;
|
|
220
|
-
if (useAsMaxInterval !== undefined && useAsSendInterval !== undefined) {
|
|
221
|
-
maxInterval = useAsMaxInterval * 1000;
|
|
222
|
-
sendInterval = useAsSendInterval * 1000;
|
|
223
|
-
} else {
|
|
224
|
-
({ maxInterval, sendInterval } = this.#determineSendingIntervals(
|
|
225
|
-
subscriptionOptions.minIntervalSeconds * 1000,
|
|
226
|
-
subscriptionOptions.maxIntervalSeconds * 1000,
|
|
227
|
-
subscriptionOptions.randomizationWindowSeconds * 1000,
|
|
228
|
-
));
|
|
229
|
-
}
|
|
230
|
-
this.maxIntervalMs = maxInterval;
|
|
231
|
-
this.#sendIntervalMs = sendInterval;
|
|
232
|
-
|
|
233
|
-
this.#updateTimer = Time.getTimer(`Subscription ${this.id} update`, this.#sendIntervalMs, () =>
|
|
234
|
-
this.#prepareDataUpdate(),
|
|
235
|
-
); // will be started later
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
#determineSendingIntervals(
|
|
239
|
-
subscriptionMinIntervalMs: number,
|
|
240
|
-
subscriptionMaxIntervalMs: number,
|
|
241
|
-
subscriptionRandomizationWindowMs: number,
|
|
242
|
-
): { maxInterval: number; sendInterval: number } {
|
|
243
|
-
// Max Interval is the Max interval that the controller request, unless the configured one from the developer
|
|
244
|
-
// is lower. In that case we use the configured one. But we make sure to not be smaller than the requested
|
|
245
|
-
// controller minimum. But in general never faster than minimum interval configured or 2 seconds
|
|
246
|
-
// (SUBSCRIPTION_MIN_INTERVAL_S). Additionally, we add a randomization window to the max interval to avoid all
|
|
247
|
-
// devices sending at the same time. But we make sure not to exceed the global max interval.
|
|
248
|
-
const maxInterval = Math.min(
|
|
249
|
-
Math.max(
|
|
250
|
-
subscriptionMinIntervalMs,
|
|
251
|
-
Math.max(this.#minIntervalFloorMs, Math.min(subscriptionMaxIntervalMs, this.#maxIntervalCeilingMs)),
|
|
252
|
-
) + Math.floor(subscriptionRandomizationWindowMs * Math.random()),
|
|
253
|
-
MAX_INTERVAL_PUBLISHER_LIMIT_S * 1000,
|
|
254
|
-
);
|
|
255
|
-
let sendInterval = Math.floor(maxInterval / 2); // Ideally we send at half the max interval
|
|
256
|
-
if (sendInterval < 60_000) {
|
|
257
|
-
// But if we have no chance of at least one full resubmission process we do like chip-tool.
|
|
258
|
-
// One full resubmission process takes 33-45 seconds. So 60s means we reach at least first 2 retries of a
|
|
259
|
-
// second subscription report after first failed.
|
|
260
|
-
sendInterval = Math.max(this.#minIntervalFloorMs, Math.floor(maxInterval * 0.8));
|
|
261
|
-
}
|
|
262
|
-
if (sendInterval < subscriptionMinIntervalMs) {
|
|
263
|
-
// But not faster than once every 2s
|
|
264
|
-
logger.warn(
|
|
265
|
-
`Determined subscription send interval of ${sendInterval}ms is too low. Using maxInterval (${maxInterval}ms) instead.`,
|
|
266
|
-
);
|
|
267
|
-
sendInterval = subscriptionMinIntervalMs;
|
|
268
|
-
}
|
|
269
|
-
return { maxInterval, sendInterval };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
#registerNewAttributes() {
|
|
273
|
-
const newAttributes = new Array<AttributeWithPath>();
|
|
274
|
-
const attributeErrors = new Array<TypeFromSchema<typeof TlvAttributeStatus>>();
|
|
275
|
-
const formerAttributes = new Set<string>(this.#attributeListeners.keys());
|
|
276
|
-
|
|
277
|
-
if (this.criteria.attributeRequests !== undefined) {
|
|
278
|
-
this.criteria.attributeRequests.forEach(path => {
|
|
279
|
-
const attributes = this.#structure.getAttributes([path]);
|
|
280
|
-
|
|
281
|
-
if (attributes.length === 0) {
|
|
282
|
-
// TODO: Also check nodeId
|
|
283
|
-
const { endpointId, clusterId, attributeId } = path;
|
|
284
|
-
if (endpointId === undefined || clusterId === undefined || attributeId === undefined) {
|
|
285
|
-
// Wildcard path: Just leave out values
|
|
286
|
-
logger.debug(
|
|
287
|
-
`Subscription attribute ${this.#structure.resolveAttributeName(
|
|
288
|
-
path,
|
|
289
|
-
)}: ignore non-existing attribute`,
|
|
290
|
-
);
|
|
291
|
-
} else {
|
|
292
|
-
// was a concrete path
|
|
293
|
-
try {
|
|
294
|
-
this.#structure.validateConcreteAttributePath(endpointId, clusterId, attributeId);
|
|
295
|
-
throw new InternalError(
|
|
296
|
-
"validateConcreteAttributePath check should throw StatusResponseError but did not.",
|
|
297
|
-
);
|
|
298
|
-
} catch (e) {
|
|
299
|
-
StatusResponseError.accept(e);
|
|
300
|
-
|
|
301
|
-
logger.debug(
|
|
302
|
-
`Subscription attribute ${this.#structure.resolveAttributeName(
|
|
303
|
-
path,
|
|
304
|
-
)}: unsupported path: Status=${e.code}`,
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
attributeErrors.push({ path, status: { status: e.code } });
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
attributes.forEach(({ path, attribute }) => {
|
|
314
|
-
formerAttributes.delete(attributePathToId(path));
|
|
315
|
-
|
|
316
|
-
const existingAttributeListener = this.#attributeListeners.get(attributePathToId(path));
|
|
317
|
-
if (existingAttributeListener !== undefined) {
|
|
318
|
-
const { attribute: existingAttribute, listener: existingListener } = existingAttributeListener;
|
|
319
|
-
if (existingAttribute !== attribute) {
|
|
320
|
-
if (existingListener !== undefined) {
|
|
321
|
-
existingAttribute.removeValueChangeListener(existingListener);
|
|
322
|
-
}
|
|
323
|
-
this.#attributeListeners.delete(attributePathToId(path));
|
|
324
|
-
} else {
|
|
325
|
-
return; // Attribute is already registered and unchanged
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (attribute.isSubscribable) {
|
|
329
|
-
// If subscribable register listener
|
|
330
|
-
// TODO: Move to state change listeners from behaviors to remove the dangling promise here
|
|
331
|
-
const listener = (value: any, version: number) =>
|
|
332
|
-
this.attributeChangeListener(path, attribute.schema, version, value);
|
|
333
|
-
attribute.addValueChangeListener(listener);
|
|
334
|
-
this.#attributeListeners.set(attributePathToId(path), { attribute, listener });
|
|
335
|
-
} else {
|
|
336
|
-
this.#attributeListeners.set(attributePathToId(path), { attribute });
|
|
337
|
-
}
|
|
338
|
-
newAttributes.push({ path, attribute });
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Remove all listeners to attributes that no longer match the subscription
|
|
344
|
-
this.unregisterAttributeListeners(Array.from(formerAttributes.values()));
|
|
345
|
-
return { newAttributes, attributeErrors };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
unregisterAttributeListeners(list: Array<string>) {
|
|
349
|
-
for (const pathId of list) {
|
|
350
|
-
const existingAttributeListener = this.#attributeListeners.get(pathId);
|
|
351
|
-
if (existingAttributeListener !== undefined) {
|
|
352
|
-
const { attribute, listener } = existingAttributeListener;
|
|
353
|
-
if (listener !== undefined) {
|
|
354
|
-
attribute.removeValueChangeListener(listener);
|
|
355
|
-
}
|
|
356
|
-
this.#attributeListeners.delete(pathId);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
#registerNewEvents() {
|
|
362
|
-
const newEvents = new Array<EventWithPath>();
|
|
363
|
-
const eventErrors = new Array<TypeFromSchema<typeof TlvEventStatus>>();
|
|
364
|
-
const formerEvents = new Set<string>(this.#eventListeners.keys());
|
|
365
|
-
|
|
366
|
-
if (this.criteria.eventRequests !== undefined) {
|
|
367
|
-
this.criteria.eventRequests.forEach(path => {
|
|
368
|
-
const events = this.#structure.getEvents([path]);
|
|
369
|
-
if (events.length === 0) {
|
|
370
|
-
const { endpointId, clusterId, eventId } = path;
|
|
371
|
-
if (endpointId === undefined || clusterId === undefined || eventId === undefined) {
|
|
372
|
-
// Wildcard path: Just leave out values
|
|
373
|
-
logger.debug(
|
|
374
|
-
`Subscription event ${this.#structure.resolveEventName(path)}: ignore non-existing event`,
|
|
375
|
-
);
|
|
376
|
-
} else {
|
|
377
|
-
try {
|
|
378
|
-
this.#structure.validateConcreteEventPath(endpointId, clusterId, eventId);
|
|
379
|
-
throw new InternalError(
|
|
380
|
-
"validateConcreteEventPath should throw StatusResponseError but did not.",
|
|
381
|
-
);
|
|
382
|
-
} catch (e) {
|
|
383
|
-
StatusResponseError.accept(e);
|
|
384
|
-
|
|
385
|
-
logger.debug(
|
|
386
|
-
`Subscription event ${this.#structure.resolveEventName(
|
|
387
|
-
path,
|
|
388
|
-
)}: unsupported path: Status=${e.code}`,
|
|
389
|
-
);
|
|
390
|
-
|
|
391
|
-
eventErrors.push({ path, status: { status: e.code } });
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
events.forEach(({ path, event }) => {
|
|
398
|
-
formerEvents.delete(eventPathToId(path));
|
|
399
|
-
|
|
400
|
-
const existingEventListener = this.#eventListeners.get(eventPathToId(path));
|
|
401
|
-
if (existingEventListener !== undefined) {
|
|
402
|
-
const { event: existingEvent, listener: existingListener } = existingEventListener;
|
|
403
|
-
if (existingEvent !== event) {
|
|
404
|
-
if (existingListener !== undefined) {
|
|
405
|
-
existingEvent.removeListener(existingListener);
|
|
406
|
-
}
|
|
407
|
-
this.#eventListeners.delete(eventPathToId(path));
|
|
408
|
-
} else {
|
|
409
|
-
return; // Event is already registered and unchanged
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
const listener = (newEvent: NumberedOccurrence) =>
|
|
413
|
-
this.eventChangeListener(path, event.schema, newEvent);
|
|
414
|
-
event.addListener(listener);
|
|
415
|
-
newEvents.push({ path, event });
|
|
416
|
-
this.#eventListeners.set(eventPathToId(path), { event, listener });
|
|
417
|
-
});
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Remove all listeners to events that no longer match the subscription
|
|
422
|
-
this.unregisterEventListeners(Array.from(formerEvents.values()));
|
|
423
|
-
|
|
424
|
-
return { newEvents, eventErrors };
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
unregisterEventListeners(list: Array<string>) {
|
|
428
|
-
for (const pathId of list) {
|
|
429
|
-
const existingEventListener = this.#eventListeners.get(pathId);
|
|
430
|
-
if (existingEventListener !== undefined) {
|
|
431
|
-
const { event, listener } = existingEventListener;
|
|
432
|
-
if (listener !== undefined) {
|
|
433
|
-
event.removeListener(listener);
|
|
434
|
-
}
|
|
435
|
-
this.#eventListeners.delete(pathId);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Update the session after an endpoint structure change. The method will initialize all missing new attributes and
|
|
442
|
-
* events and will remove listeners no longer needed.
|
|
443
|
-
* Newly added attributes are then treated as "changed values" and will be sent as subscription data update to the
|
|
444
|
-
* controller. The data of newly added events are not sent automatically.
|
|
445
|
-
*/
|
|
446
|
-
async updateSubscription() {
|
|
447
|
-
const { newAttributes } = this.#registerNewAttributes();
|
|
448
|
-
|
|
449
|
-
for (const { path, attribute } of newAttributes) {
|
|
450
|
-
const { version, value } = this.#context.readAttribute(path, attribute);
|
|
451
|
-
|
|
452
|
-
// We do not do any version filtering for attributes that are newly added to make sure controller gets
|
|
453
|
-
// most current state
|
|
454
|
-
|
|
455
|
-
this.#outstandingAttributeUpdates.set(attributePathToId(path), {
|
|
456
|
-
attribute,
|
|
457
|
-
path,
|
|
458
|
-
schema: attribute.schema,
|
|
459
|
-
version,
|
|
460
|
-
value,
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const { newEvents } = this.#registerNewEvents();
|
|
465
|
-
const occurrences = Array<EventPathWithEventData<any>>();
|
|
466
|
-
for (const { path, event } of newEvents) {
|
|
467
|
-
const { schema } = event;
|
|
468
|
-
let eventOccurrences = event.get(
|
|
469
|
-
this.session,
|
|
470
|
-
this.criteria.isFabricFiltered,
|
|
471
|
-
undefined,
|
|
472
|
-
this.criteria.eventFilters,
|
|
473
|
-
);
|
|
474
|
-
if (MaybePromise.is(eventOccurrences)) {
|
|
475
|
-
eventOccurrences = await eventOccurrences;
|
|
476
|
-
}
|
|
477
|
-
occurrences.push(
|
|
478
|
-
...eventOccurrences.map(data => ({
|
|
479
|
-
event,
|
|
480
|
-
schema,
|
|
481
|
-
path,
|
|
482
|
-
data,
|
|
483
|
-
})),
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
occurrences.sort((a, b) => {
|
|
488
|
-
const eventNumberA = a.data?.number ?? EventNumber(0);
|
|
489
|
-
const eventNumberB = b.data?.number ?? EventNumber(0);
|
|
490
|
-
if (eventNumberA > eventNumberB) {
|
|
491
|
-
return 1;
|
|
492
|
-
} else if (eventNumberA < eventNumberB) {
|
|
493
|
-
return -1;
|
|
494
|
-
} else {
|
|
495
|
-
return 0;
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
for (const occurrence of occurrences) {
|
|
500
|
-
this.#outstandingEventUpdates.add(occurrence);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
this.#prepareDataUpdate();
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
get sendInterval(): number {
|
|
507
|
-
return Math.ceil(this.#sendIntervalMs / 1000);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
get minIntervalFloorSeconds(): number {
|
|
511
|
-
return Math.ceil(this.#minIntervalFloorMs / 1000);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
get maxIntervalCeilingSeconds(): number {
|
|
515
|
-
return Math.ceil(this.#maxIntervalCeilingMs / 1000);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
override activate() {
|
|
519
|
-
super.activate();
|
|
520
|
-
|
|
521
|
-
// We do not need these data anymore, so we can free some memory
|
|
522
|
-
if (this.criteria.eventFilters !== undefined) this.criteria.eventFilters.length = 0;
|
|
523
|
-
if (this.criteria.dataVersionFilters !== undefined) this.criteria.dataVersionFilters.length = 0;
|
|
524
|
-
|
|
525
|
-
this.#sendUpdatesActivated = true;
|
|
526
|
-
if (this.#outstandingAttributeUpdates.size > 0 || this.#outstandingEventUpdates.size > 0) {
|
|
527
|
-
this.#triggerSendUpdate();
|
|
528
|
-
}
|
|
529
|
-
this.#updateTimer = Time.getTimer("Subscription update", this.#sendIntervalMs, () =>
|
|
530
|
-
this.#prepareDataUpdate(),
|
|
531
|
-
).start();
|
|
532
|
-
this.#structure.change.on(() => {
|
|
533
|
-
if (this.isClosed) {
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
// TODO When change is AsyncObservable can be simplified
|
|
537
|
-
this.updateSubscription().catch(error =>
|
|
538
|
-
logger.error("Error updating subscription after structure change:", error),
|
|
539
|
-
);
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Check if data should be sent straight away or delayed because the minimum interval is not reached. Delay real
|
|
545
|
-
* sending by 50ms in any case to mke sure to catch all updates.
|
|
546
|
-
*/
|
|
547
|
-
#prepareDataUpdate() {
|
|
548
|
-
if (this.#sendDelayTimer.isRunning || this.isClosed) {
|
|
549
|
-
// sending data is already scheduled, data updates go in there ... or we close down already
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (!this.#sendUpdatesActivated) {
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
this.#updateTimer.stop();
|
|
558
|
-
const now = Time.nowMs();
|
|
559
|
-
const timeSinceLastUpdateMs = now - this.#lastUpdateTimeMs;
|
|
560
|
-
if (timeSinceLastUpdateMs < this.#minIntervalFloorMs) {
|
|
561
|
-
// Respect minimum delay time between updates
|
|
562
|
-
this.#updateTimer = Time.getTimer(
|
|
563
|
-
"Subscription update",
|
|
564
|
-
this.#minIntervalFloorMs - timeSinceLastUpdateMs,
|
|
565
|
-
() => this.#prepareDataUpdate(),
|
|
566
|
-
).start();
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
this.#sendDelayTimer.start();
|
|
571
|
-
this.#updateTimer = Time.getTimer(`Subscription update ${this.id}`, this.#sendIntervalMs, () =>
|
|
572
|
-
this.#prepareDataUpdate(),
|
|
573
|
-
).start();
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
#triggerSendUpdate() {
|
|
577
|
-
if (this.#currentUpdatePromise !== undefined) {
|
|
578
|
-
logger.debug("Sending update already in progress, delaying update ...");
|
|
579
|
-
this.#sendNextUpdateImmediately = true;
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
this.#currentUpdatePromise = this.#sendUpdate()
|
|
583
|
-
.catch(error => logger.warn("Sending subscription update failed:", error))
|
|
584
|
-
.finally(() => (this.#currentUpdatePromise = undefined));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Determine all attributes that have changed since the last update and send them tout to the subscriber.
|
|
589
|
-
* Important: This method MUST NOT be called directly. Use triggerSendUpdate() instead!
|
|
590
|
-
*/
|
|
591
|
-
async #sendUpdate(onlyWithData = false) {
|
|
592
|
-
// Get all outstanding updates, make sure the order is correct per endpoint and cluster
|
|
593
|
-
const attributeUpdatesToSend = new Array<AttributePathWithValueVersion<any>>();
|
|
594
|
-
const attributeUpdates: Record<string, AttributePathWithValueVersion<any>[]> = {};
|
|
595
|
-
Array.from(this.#outstandingAttributeUpdates.values()).forEach(entry => {
|
|
596
|
-
const {
|
|
597
|
-
path: { nodeId, endpointId, clusterId },
|
|
598
|
-
} = entry;
|
|
599
|
-
const pathId = `${nodeId}-${endpointId}-${clusterId}`;
|
|
600
|
-
attributeUpdates[pathId] = attributeUpdates[pathId] ?? [];
|
|
601
|
-
attributeUpdates[pathId].push(entry);
|
|
602
|
-
});
|
|
603
|
-
this.#outstandingAttributeUpdates.clear();
|
|
604
|
-
Object.values(attributeUpdates).forEach(data =>
|
|
605
|
-
attributeUpdatesToSend.push(
|
|
606
|
-
...data.sort(({ version: versionA }, { version: versionB }) => versionA - versionB),
|
|
607
|
-
),
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
const eventUpdatesToSend = Array.from(this.#outstandingEventUpdates.values());
|
|
611
|
-
this.#outstandingEventUpdates.clear();
|
|
612
|
-
|
|
613
|
-
if (onlyWithData && attributeUpdatesToSend.length === 0 && eventUpdatesToSend.length === 0) {
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
this.#lastUpdateTimeMs = Time.nowMs();
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
await this.#sendUpdateMessage(attributeUpdatesToSend, eventUpdatesToSend);
|
|
621
|
-
this.#sendUpdateErrorCounter = 0;
|
|
622
|
-
} catch (error) {
|
|
623
|
-
if (this.isClosed) {
|
|
624
|
-
// No need to care about resubmissions when the server is closing
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
this.#sendUpdateErrorCounter++;
|
|
629
|
-
logger.info(
|
|
630
|
-
`Error sending subscription update message (error count=${this.#sendUpdateErrorCounter}):`,
|
|
631
|
-
(error instanceof MatterError && error.message) || error,
|
|
632
|
-
);
|
|
633
|
-
if (this.#sendUpdateErrorCounter <= 2) {
|
|
634
|
-
// fill the data back in the queue to resend with next try
|
|
635
|
-
const newAttributeUpdatesToSend = Array.from(this.#outstandingAttributeUpdates.values());
|
|
636
|
-
this.#outstandingAttributeUpdates.clear();
|
|
637
|
-
const newEventUpdatesToSend = Array.from(this.#outstandingEventUpdates.values());
|
|
638
|
-
this.#outstandingEventUpdates.clear();
|
|
639
|
-
[...attributeUpdatesToSend, ...newAttributeUpdatesToSend].forEach(update =>
|
|
640
|
-
this.#outstandingAttributeUpdates.set(attributePathToId(update.path), update),
|
|
641
|
-
);
|
|
642
|
-
[...eventUpdatesToSend, ...newEventUpdatesToSend].forEach(update =>
|
|
643
|
-
this.#outstandingEventUpdates.add(update),
|
|
644
|
-
);
|
|
645
|
-
} else {
|
|
646
|
-
logger.info(
|
|
647
|
-
`Sending update failed 3 times in a row, canceling subscription ${this.id} and let controller subscribe again.`,
|
|
648
|
-
);
|
|
649
|
-
this.#sendNextUpdateImmediately = false;
|
|
650
|
-
if (
|
|
651
|
-
error instanceof NoResponseTimeoutError ||
|
|
652
|
-
error instanceof NetworkError ||
|
|
653
|
-
error instanceof NoChannelError
|
|
654
|
-
) {
|
|
655
|
-
// Let's consider this subscription as dead and wait for a reconnect
|
|
656
|
-
this.isCanceledByPeer = true; // We handle this case like if the controller canceled the subscription
|
|
657
|
-
await this.destroy();
|
|
658
|
-
return;
|
|
659
|
-
} else {
|
|
660
|
-
throw error;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (this.#sendNextUpdateImmediately) {
|
|
666
|
-
logger.debug("Sending delayed update immediately after last one was sent.");
|
|
667
|
-
this.#sendNextUpdateImmediately = false;
|
|
668
|
-
await this.#sendUpdate(true); // Send but only if non-empty
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
async #collectInitialEventReportPayloads(newEvents: EventWithPath[]) {
|
|
673
|
-
let eventsFiltered = false;
|
|
674
|
-
const eventReportsPayload = new Array<EventReportPayload>();
|
|
675
|
-
for (const { path, event } of newEvents) {
|
|
676
|
-
const { schema } = event;
|
|
677
|
-
try {
|
|
678
|
-
const matchingEvents = await this.#context.readEvent(path, event, this.criteria.eventFilters);
|
|
679
|
-
if (matchingEvents.length === 0) {
|
|
680
|
-
eventsFiltered = true;
|
|
681
|
-
} else {
|
|
682
|
-
matchingEvents.forEach(({ number, priority, epochTimestamp, payload }) => {
|
|
683
|
-
eventReportsPayload.push({
|
|
684
|
-
hasFabricSensitiveData: event.hasFabricSensitiveData,
|
|
685
|
-
eventData: {
|
|
686
|
-
path,
|
|
687
|
-
eventNumber: number,
|
|
688
|
-
priority,
|
|
689
|
-
epochTimestamp,
|
|
690
|
-
payload,
|
|
691
|
-
schema,
|
|
692
|
-
},
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
} catch (error) {
|
|
697
|
-
if (StatusResponseError.is(error, StatusCode.UnsupportedAccess)) {
|
|
698
|
-
logger.warn(`Permission denied reading event ${this.#structure.resolveEventName(path)}`);
|
|
699
|
-
} else {
|
|
700
|
-
logger.warn(`Error reading event ${this.#structure.resolveEventName(path)}:`, error);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
eventReportsPayload.sort((a, b) => {
|
|
705
|
-
const eventNumberA = a.eventData?.eventNumber ?? 0;
|
|
706
|
-
const eventNumberB = b.eventData?.eventNumber ?? 0;
|
|
707
|
-
if (eventNumberA > eventNumberB) {
|
|
708
|
-
return 1;
|
|
709
|
-
} else if (eventNumberA < eventNumberB) {
|
|
710
|
-
return -1;
|
|
711
|
-
} else {
|
|
712
|
-
return 0;
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
return { eventReportsPayload, eventsFiltered };
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Returns an iterator that yields the initial subscription data to be sent to the controller.
|
|
721
|
-
* The iterator will yield all attributes and events that match the subscription criteria.
|
|
722
|
-
* A thrown exception will cancel the sending process immediately.
|
|
723
|
-
* TODO: Streamline all this with the normal Read flow to also handle Concrete Path subscriptions with errors correctly
|
|
724
|
-
*/
|
|
725
|
-
async *#iterateInitialSubscriptionData(
|
|
726
|
-
attributesToSend: {
|
|
727
|
-
newAttributes: AttributeWithPath[];
|
|
728
|
-
attributeErrors: TypeFromSchema<typeof TlvAttributeStatus>[];
|
|
729
|
-
},
|
|
730
|
-
eventsToSend: {
|
|
731
|
-
eventReportsPayload: EventReportPayload[];
|
|
732
|
-
eventsFiltered: boolean;
|
|
733
|
-
eventErrors: TypeFromSchema<typeof TlvEventStatus>[];
|
|
734
|
-
},
|
|
735
|
-
) {
|
|
736
|
-
const dataVersionFilterMap = new Map<string, number>(
|
|
737
|
-
this.criteria.dataVersionFilters?.map(({ path, dataVersion }) => [clusterPathToId(path), dataVersion]) ??
|
|
738
|
-
[],
|
|
739
|
-
);
|
|
740
|
-
|
|
741
|
-
const { newAttributes, attributeErrors } = attributesToSend;
|
|
742
|
-
const { eventReportsPayload, eventsFiltered, eventErrors } = eventsToSend;
|
|
743
|
-
|
|
744
|
-
logger.debug(
|
|
745
|
-
`Initializes Subscription with ${newAttributes.length} attributes and ${eventReportsPayload.length} events.`,
|
|
746
|
-
);
|
|
747
|
-
|
|
748
|
-
let attributesFilteredWithVersion = false;
|
|
749
|
-
|
|
750
|
-
const attributesPerCluster = new Map<EndpointNumber, AttributeWithPath[]>();
|
|
751
|
-
for (const { path, attribute } of newAttributes) {
|
|
752
|
-
const { endpointId } = path;
|
|
753
|
-
const endpointAttributes = attributesPerCluster.get(endpointId) ?? new Array<AttributeWithPath>();
|
|
754
|
-
endpointAttributes.push({ path, attribute });
|
|
755
|
-
attributesPerCluster.set(endpointId, endpointAttributes);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
let attributesCounter = 0;
|
|
759
|
-
for (const endpointId of attributesPerCluster.keys()) {
|
|
760
|
-
const endpointAttributes = attributesPerCluster.get(endpointId)!;
|
|
761
|
-
attributesPerCluster.delete(endpointId);
|
|
762
|
-
for (const { path, attribute, value, version } of this.#context.readEndpointAttributesForSubscription(
|
|
763
|
-
endpointAttributes,
|
|
764
|
-
)) {
|
|
765
|
-
if (value === undefined) continue;
|
|
766
|
-
|
|
767
|
-
const { nodeId, endpointId, clusterId } = path;
|
|
768
|
-
|
|
769
|
-
const versionFilterValue =
|
|
770
|
-
endpointId !== undefined && clusterId !== undefined
|
|
771
|
-
? dataVersionFilterMap.get(clusterPathToId({ nodeId, endpointId, clusterId }))
|
|
772
|
-
: undefined;
|
|
773
|
-
if (versionFilterValue !== undefined && versionFilterValue === version) {
|
|
774
|
-
attributesFilteredWithVersion = true;
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
attributesCounter++;
|
|
779
|
-
yield {
|
|
780
|
-
hasFabricSensitiveData: attribute.hasFabricSensitiveData,
|
|
781
|
-
attributeData: {
|
|
782
|
-
path,
|
|
783
|
-
dataVersion: version,
|
|
784
|
-
payload: value,
|
|
785
|
-
schema: attribute.schema,
|
|
786
|
-
},
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
for (const attributeStatus of attributeErrors) {
|
|
792
|
-
yield {
|
|
793
|
-
hasFabricSensitiveData: false,
|
|
794
|
-
attributeStatus,
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
if (
|
|
799
|
-
attributesCounter === 0 &&
|
|
800
|
-
!attributesFilteredWithVersion &&
|
|
801
|
-
eventReportsPayload.length === 0 &&
|
|
802
|
-
!eventsFiltered
|
|
803
|
-
) {
|
|
804
|
-
throw new StatusResponseError(
|
|
805
|
-
"Subscription failed because no attributes or events are matching the query",
|
|
806
|
-
StatusCode.InvalidAction,
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
for (const eventReport of eventReportsPayload) {
|
|
811
|
-
yield eventReport;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
for (const eventStatus of eventErrors) {
|
|
815
|
-
yield {
|
|
816
|
-
hasFabricSensitiveData: false,
|
|
817
|
-
eventStatus,
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
this.#lastUpdateTimeMs = Time.nowMs();
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
async sendInitialReport(messenger: InteractionServerMessenger) {
|
|
825
|
-
this.#updateTimer.stop();
|
|
826
|
-
|
|
827
|
-
const { newAttributes, attributeErrors } = this.#registerNewAttributes();
|
|
828
|
-
const { newEvents, eventErrors } = this.#registerNewEvents();
|
|
829
|
-
const { eventReportsPayload, eventsFiltered } = await this.#collectInitialEventReportPayloads(newEvents);
|
|
830
|
-
|
|
831
|
-
await messenger.sendDataReport(
|
|
832
|
-
{
|
|
833
|
-
suppressResponse: false, // we always need proper response for initial report
|
|
834
|
-
subscriptionId: this.id,
|
|
835
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
836
|
-
},
|
|
837
|
-
this.criteria.isFabricFiltered,
|
|
838
|
-
this.#iterateInitialSubscriptionData(
|
|
839
|
-
{ newAttributes, attributeErrors },
|
|
840
|
-
{ eventReportsPayload, eventsFiltered, eventErrors },
|
|
841
|
-
),
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
attributeChangeListener<T>(path: AttributePath, schema: TlvSchema<T>, version: number, value: T) {
|
|
846
|
-
const changeResult = this.attributeChangeHandler(path, schema, version, value);
|
|
847
|
-
if (MaybePromise.is(changeResult)) {
|
|
848
|
-
const resolver = Promise.resolve(changeResult)
|
|
849
|
-
.catch(error => logger.error(`Error handling attribute change:`, error))
|
|
850
|
-
.finally(() => this.#attributeUpdatePromises.delete(resolver));
|
|
851
|
-
this.#attributeUpdatePromises.add(resolver);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
attributeChangeHandler<T>(
|
|
856
|
-
path: AttributePath,
|
|
857
|
-
schema: TlvSchema<T>,
|
|
858
|
-
version: number,
|
|
859
|
-
value: T,
|
|
860
|
-
): MaybePromise<void> {
|
|
861
|
-
const attributeListenerData = this.#attributeListeners.get(attributePathToId(path));
|
|
862
|
-
if (attributeListenerData === undefined) return; // Ignore changes to attributes that are not subscribed to
|
|
863
|
-
|
|
864
|
-
const { attribute } = attributeListenerData;
|
|
865
|
-
if (attribute instanceof FabricScopedAttributeServer) {
|
|
866
|
-
// We cannot be sure what value we got for fabric filtered attributes (and from which fabric),
|
|
867
|
-
// so get it again for this relevant fabric. This also makes sure that fabric sensitive fields are filtered
|
|
868
|
-
// TODO: Remove this once we remove the legacy API and go away from using AttributeServers in the background
|
|
869
|
-
const { value } = this.#context.readAttribute(path, attribute, true);
|
|
870
|
-
this.#outstandingAttributeUpdates.set(attributePathToId(path), {
|
|
871
|
-
attribute,
|
|
872
|
-
path,
|
|
873
|
-
schema,
|
|
874
|
-
version,
|
|
875
|
-
value,
|
|
876
|
-
});
|
|
877
|
-
this.#prepareDataUpdate();
|
|
878
|
-
}
|
|
879
|
-
this.#outstandingAttributeUpdates.set(attributePathToId(path), { attribute, path, schema, version, value });
|
|
880
|
-
this.#prepareDataUpdate();
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
eventChangeListener<T>(path: EventPath, schema: TlvSchema<T>, newEvent: NumberedOccurrence) {
|
|
884
|
-
const eventListenerData = this.#eventListeners.get(eventPathToId(path));
|
|
885
|
-
if (eventListenerData === undefined) return; // Ignore changes to attributes that are not subscribed to
|
|
886
|
-
|
|
887
|
-
const { event } = eventListenerData;
|
|
888
|
-
if (event instanceof FabricSensitiveEventServer) {
|
|
889
|
-
const { payload } = newEvent;
|
|
890
|
-
if (
|
|
891
|
-
isObject(payload) &&
|
|
892
|
-
"fabricIndex" in payload &&
|
|
893
|
-
payload.fabricIndex !== this.session.fabric?.fabricIndex
|
|
894
|
-
) {
|
|
895
|
-
// Ignore events from different fabrics because events are kind of always fabric filtered
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
this.#outstandingEventUpdates.add({ event, path, schema, data: newEvent });
|
|
900
|
-
if (path.isUrgent) {
|
|
901
|
-
this.#prepareDataUpdate();
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
async #flush() {
|
|
906
|
-
this.#sendDelayTimer.stop();
|
|
907
|
-
if (this.#outstandingAttributeUpdates.size > 0 || this.#outstandingEventUpdates.size > 0) {
|
|
908
|
-
logger.debug(
|
|
909
|
-
`Flushing subscription ${this.id} with ${this.#outstandingAttributeUpdates.size} attributes and ${this.#outstandingEventUpdates.size} events${this.isClosed ? " (for closing)" : ""}`,
|
|
910
|
-
);
|
|
911
|
-
this.#triggerSendUpdate();
|
|
912
|
-
if (this.#currentUpdatePromise) {
|
|
913
|
-
await this.#currentUpdatePromise;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
protected override async destroy() {
|
|
919
|
-
this.#sendUpdatesActivated = false;
|
|
920
|
-
this.unregisterAttributeListeners(Array.from(this.#attributeListeners.keys()));
|
|
921
|
-
this.unregisterEventListeners(Array.from(this.#eventListeners.keys()));
|
|
922
|
-
if (this.#attributeUpdatePromises.size) {
|
|
923
|
-
const resolvers = [...this.#attributeUpdatePromises.values()];
|
|
924
|
-
this.#attributeUpdatePromises.clear();
|
|
925
|
-
await MatterAggregateError.allSettled(resolvers, "Error receiving all outstanding attribute values").catch(
|
|
926
|
-
error => logger.error(error),
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
this.#updateTimer.stop();
|
|
930
|
-
this.#sendDelayTimer.stop();
|
|
931
|
-
await super.destroy();
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
/**
|
|
935
|
-
* Closes the subscription and flushes all outstanding data updates if requested.
|
|
936
|
-
*/
|
|
937
|
-
override async close(graceful = false, cancelledByPeer = false) {
|
|
938
|
-
if (this.isClosed) {
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
if (cancelledByPeer) {
|
|
942
|
-
this.isCanceledByPeer = true;
|
|
943
|
-
}
|
|
944
|
-
await this.destroy();
|
|
945
|
-
if (graceful) {
|
|
946
|
-
await this.#flush();
|
|
947
|
-
}
|
|
948
|
-
if (this.#currentUpdatePromise) {
|
|
949
|
-
await this.#currentUpdatePromise;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
/**
|
|
954
|
-
* Iterates over all attributes and events that have changed since the last update and sends them to
|
|
955
|
-
* the controller.
|
|
956
|
-
* A thrown exception will cancel the sending process immediately.
|
|
957
|
-
*/
|
|
958
|
-
async *#iterateDataUpdate(attributes: AttributePathWithValueVersion<any>[], events: EventPathWithEventData<any>[]) {
|
|
959
|
-
for (const {
|
|
960
|
-
path,
|
|
961
|
-
schema,
|
|
962
|
-
value: payload,
|
|
963
|
-
version: dataVersion,
|
|
964
|
-
attribute: { hasFabricSensitiveData },
|
|
965
|
-
} of attributes) {
|
|
966
|
-
yield {
|
|
967
|
-
hasFabricSensitiveData,
|
|
968
|
-
attributeData: { path, dataVersion, schema, payload },
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
for (const {
|
|
973
|
-
path,
|
|
974
|
-
schema,
|
|
975
|
-
event,
|
|
976
|
-
data: { number: eventNumber, priority, epochTimestamp, payload },
|
|
977
|
-
} of events) {
|
|
978
|
-
yield {
|
|
979
|
-
hasFabricSensitiveData: event.hasFabricSensitiveData,
|
|
980
|
-
eventData: { path, eventNumber, priority, epochTimestamp, schema, payload },
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
async #sendUpdateMessage(attributes: AttributePathWithValueVersion<any>[], events: EventPathWithEventData<any>[]) {
|
|
986
|
-
const exchange = this.#context.initiateExchange(this.#peerAddress, INTERACTION_PROTOCOL_ID);
|
|
987
|
-
if (exchange === undefined) return;
|
|
988
|
-
if (attributes.length) {
|
|
989
|
-
logger.debug(
|
|
990
|
-
`Subscription attribute changes for ID ${this.id}: ${attributes
|
|
991
|
-
.map(
|
|
992
|
-
({ path, value, version }) =>
|
|
993
|
-
`${this.#structure.resolveAttributeName(path)}=${Diagnostic.json(value)} (${version})`,
|
|
994
|
-
)
|
|
995
|
-
.join(", ")}`,
|
|
996
|
-
); // TODO Format path better using endpoint structure
|
|
997
|
-
}
|
|
998
|
-
const messenger = new InteractionServerMessenger(exchange);
|
|
999
|
-
|
|
1000
|
-
try {
|
|
1001
|
-
if (attributes.length === 0 && events.length === 0) {
|
|
1002
|
-
await messenger.sendDataReport(
|
|
1003
|
-
{
|
|
1004
|
-
suppressResponse: true, // suppressResponse true for empty DataReports
|
|
1005
|
-
subscriptionId: this.id,
|
|
1006
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
1007
|
-
},
|
|
1008
|
-
this.criteria.isFabricFiltered,
|
|
1009
|
-
undefined,
|
|
1010
|
-
!this.isClosed, // Do not wait for ack when closed
|
|
1011
|
-
);
|
|
1012
|
-
} else {
|
|
1013
|
-
await messenger.sendDataReport(
|
|
1014
|
-
{
|
|
1015
|
-
suppressResponse: false, // Non-empty data reports always need to send response
|
|
1016
|
-
subscriptionId: this.id,
|
|
1017
|
-
interactionModelRevision: Specification.INTERACTION_MODEL_REVISION,
|
|
1018
|
-
},
|
|
1019
|
-
this.criteria.isFabricFiltered,
|
|
1020
|
-
this.#iterateDataUpdate(attributes, events),
|
|
1021
|
-
!this.isClosed, // Do not wait for ack when closed
|
|
1022
|
-
);
|
|
1023
|
-
}
|
|
1024
|
-
} catch (error) {
|
|
1025
|
-
if (StatusResponseError.is(error, StatusCode.InvalidSubscription, StatusCode.Failure)) {
|
|
1026
|
-
logger.info(`Subscription ${this.id} cancelled by peer.`);
|
|
1027
|
-
this.isCanceledByPeer = true;
|
|
1028
|
-
await this.close(false);
|
|
1029
|
-
} else {
|
|
1030
|
-
StatusResponseError.accept(error);
|
|
1031
|
-
logger.info(`Subscription ${this.id} update failed:`, error);
|
|
1032
|
-
await this.close(false);
|
|
1033
|
-
}
|
|
1034
|
-
} finally {
|
|
1035
|
-
await messenger.close();
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|