@matter/node 0.16.0-alpha.0-20251027-17770fb28 → 0.16.0-alpha.0-20251030-e9ca79f93
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/behavior/Behavior.d.ts +1 -0
- package/dist/cjs/behavior/Behavior.d.ts.map +1 -1
- package/dist/cjs/behavior/Behavior.js +5 -0
- package/dist/cjs/behavior/Behavior.js.map +1 -1
- package/dist/cjs/behavior/internal/BehaviorBacking.js +1 -1
- package/dist/cjs/behavior/internal/BehaviorBacking.js.map +1 -1
- package/dist/cjs/behavior/state/managed/Datasource.d.ts +4 -5
- package/dist/cjs/behavior/state/managed/Datasource.d.ts.map +1 -1
- package/dist/cjs/behavior/state/managed/Datasource.js +6 -2
- package/dist/cjs/behavior/state/managed/Datasource.js.map +1 -1
- package/dist/cjs/behavior/state/managed/ManagedReference.d.ts +3 -2
- package/dist/cjs/behavior/state/managed/ManagedReference.d.ts.map +1 -1
- package/dist/cjs/behavior/state/managed/ManagedReference.js +65 -20
- package/dist/cjs/behavior/state/managed/ManagedReference.js.map +1 -1
- package/dist/cjs/behavior/state/managed/values/ListManager.js +2 -1
- package/dist/cjs/behavior/state/managed/values/ListManager.js.map +1 -1
- package/dist/cjs/behavior/state/managed/values/StructManager.js +9 -1
- package/dist/cjs/behavior/state/managed/values/StructManager.js.map +1 -1
- package/dist/cjs/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/access-control/AccessControlServer.js +3 -3
- package/dist/cjs/behaviors/access-control/AccessControlServer.js.map +1 -1
- package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js +3 -9
- package/dist/cjs/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
- package/dist/cjs/behaviors/service-area/ServiceAreaServer.js +2 -2
- package/dist/cjs/behaviors/service-area/ServiceAreaServer.js.map +1 -1
- package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.d.ts +58 -0
- package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.d.ts.map +1 -0
- package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js +306 -0
- package/dist/cjs/behaviors/thermostat/AtomicWriteHandler.js.map +6 -0
- package/dist/cjs/behaviors/thermostat/AtomicWriteState.d.ts +33 -0
- package/dist/cjs/behaviors/thermostat/AtomicWriteState.d.ts.map +1 -0
- package/dist/cjs/behaviors/thermostat/AtomicWriteState.js +86 -0
- package/dist/cjs/behaviors/thermostat/AtomicWriteState.js.map +6 -0
- package/dist/cjs/behaviors/thermostat/ThermostatBehavior.d.ts +12 -0
- package/dist/cjs/behaviors/thermostat/ThermostatBehavior.d.ts.map +1 -1
- package/dist/cjs/behaviors/thermostat/ThermostatInterface.d.ts +1 -0
- package/dist/cjs/behaviors/thermostat/ThermostatInterface.d.ts.map +1 -1
- package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts +894 -3
- package/dist/cjs/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
- package/dist/cjs/behaviors/thermostat/ThermostatServer.js +1216 -1
- package/dist/cjs/behaviors/thermostat/ThermostatServer.js.map +2 -2
- package/dist/cjs/devices/water-heater.d.ts +24 -0
- package/dist/cjs/devices/water-heater.d.ts.map +1 -1
- package/dist/cjs/endpoint/Endpoint.d.ts +36 -2
- package/dist/cjs/endpoint/Endpoint.d.ts.map +1 -1
- package/dist/cjs/endpoint/Endpoint.js +17 -14
- package/dist/cjs/endpoint/Endpoint.js.map +1 -1
- package/dist/cjs/endpoint/properties/EndpointContainer.d.ts +1 -0
- package/dist/cjs/endpoint/properties/EndpointContainer.d.ts.map +1 -1
- package/dist/cjs/endpoint/properties/EndpointContainer.js +3 -0
- package/dist/cjs/endpoint/properties/EndpointContainer.js.map +1 -1
- package/dist/esm/behavior/Behavior.d.ts +1 -0
- package/dist/esm/behavior/Behavior.d.ts.map +1 -1
- package/dist/esm/behavior/Behavior.js +5 -0
- package/dist/esm/behavior/Behavior.js.map +1 -1
- package/dist/esm/behavior/internal/BehaviorBacking.js +2 -2
- package/dist/esm/behavior/internal/BehaviorBacking.js.map +1 -1
- package/dist/esm/behavior/state/managed/Datasource.d.ts +4 -5
- package/dist/esm/behavior/state/managed/Datasource.d.ts.map +1 -1
- package/dist/esm/behavior/state/managed/Datasource.js +7 -3
- package/dist/esm/behavior/state/managed/Datasource.js.map +1 -1
- package/dist/esm/behavior/state/managed/ManagedReference.d.ts +3 -2
- package/dist/esm/behavior/state/managed/ManagedReference.d.ts.map +1 -1
- package/dist/esm/behavior/state/managed/ManagedReference.js +66 -21
- package/dist/esm/behavior/state/managed/ManagedReference.js.map +1 -1
- package/dist/esm/behavior/state/managed/values/ListManager.js +2 -1
- package/dist/esm/behavior/state/managed/values/ListManager.js.map +1 -1
- package/dist/esm/behavior/state/managed/values/StructManager.js +9 -1
- package/dist/esm/behavior/state/managed/values/StructManager.js.map +1 -1
- package/dist/esm/behaviors/access-control/AccessControlServer.d.ts.map +1 -1
- package/dist/esm/behaviors/access-control/AccessControlServer.js +3 -3
- package/dist/esm/behaviors/access-control/AccessControlServer.js.map +1 -1
- package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.d.ts.map +1 -1
- package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js +3 -9
- package/dist/esm/behaviors/general-diagnostics/GeneralDiagnosticsServer.js.map +1 -1
- package/dist/esm/behaviors/service-area/ServiceAreaServer.js +2 -2
- package/dist/esm/behaviors/service-area/ServiceAreaServer.js.map +1 -1
- package/dist/esm/behaviors/thermostat/AtomicWriteHandler.d.ts +58 -0
- package/dist/esm/behaviors/thermostat/AtomicWriteHandler.d.ts.map +1 -0
- package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js +293 -0
- package/dist/esm/behaviors/thermostat/AtomicWriteHandler.js.map +6 -0
- package/dist/esm/behaviors/thermostat/AtomicWriteState.d.ts +33 -0
- package/dist/esm/behaviors/thermostat/AtomicWriteState.d.ts.map +1 -0
- package/dist/esm/behaviors/thermostat/AtomicWriteState.js +66 -0
- package/dist/esm/behaviors/thermostat/AtomicWriteState.js.map +6 -0
- package/dist/esm/behaviors/thermostat/ThermostatBehavior.d.ts +12 -0
- package/dist/esm/behaviors/thermostat/ThermostatBehavior.d.ts.map +1 -1
- package/dist/esm/behaviors/thermostat/ThermostatInterface.d.ts +1 -0
- package/dist/esm/behaviors/thermostat/ThermostatInterface.d.ts.map +1 -1
- package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts +894 -3
- package/dist/esm/behaviors/thermostat/ThermostatServer.d.ts.map +1 -1
- package/dist/esm/behaviors/thermostat/ThermostatServer.js +1225 -1
- package/dist/esm/behaviors/thermostat/ThermostatServer.js.map +2 -2
- package/dist/esm/devices/water-heater.d.ts +24 -0
- package/dist/esm/devices/water-heater.d.ts.map +1 -1
- package/dist/esm/endpoint/Endpoint.d.ts +36 -2
- package/dist/esm/endpoint/Endpoint.d.ts.map +1 -1
- package/dist/esm/endpoint/Endpoint.js +17 -14
- package/dist/esm/endpoint/Endpoint.js.map +1 -1
- package/dist/esm/endpoint/properties/EndpointContainer.d.ts +1 -0
- package/dist/esm/endpoint/properties/EndpointContainer.d.ts.map +1 -1
- package/dist/esm/endpoint/properties/EndpointContainer.js +3 -0
- package/dist/esm/endpoint/properties/EndpointContainer.js.map +1 -1
- package/package.json +7 -7
- package/src/behavior/Behavior.ts +10 -0
- package/src/behavior/internal/BehaviorBacking.ts +2 -2
- package/src/behavior/state/managed/Datasource.ts +14 -7
- package/src/behavior/state/managed/ManagedReference.ts +67 -19
- package/src/behavior/state/managed/values/ListManager.ts +1 -0
- package/src/behavior/state/managed/values/StructManager.ts +13 -3
- package/src/behaviors/access-control/AccessControlServer.ts +3 -7
- package/src/behaviors/general-diagnostics/GeneralDiagnosticsServer.ts +5 -9
- package/src/behaviors/service-area/ServiceAreaServer.ts +2 -2
- package/src/behaviors/thermostat/AtomicWriteHandler.ts +412 -0
- package/src/behaviors/thermostat/AtomicWriteState.ts +91 -0
- package/src/behaviors/thermostat/ThermostatInterface.ts +2 -0
- package/src/behaviors/thermostat/ThermostatServer.ts +1487 -3
- package/src/endpoint/Endpoint.ts +61 -5
- package/src/endpoint/properties/EndpointContainer.ts +4 -0
|
@@ -31,6 +31,7 @@ type Container = Record<string | number, Val>;
|
|
|
31
31
|
* @param id the lookup ID in the case of structs
|
|
32
32
|
* @param assertWriteOk enforces ACLs and read-only
|
|
33
33
|
* @param clone clones the container prior to write; undefined if not transactional
|
|
34
|
+
* @param session the access control session
|
|
34
35
|
*
|
|
35
36
|
* @returns a reference to the property
|
|
36
37
|
*/
|
|
@@ -41,6 +42,7 @@ export function ManagedReference(
|
|
|
41
42
|
id: number | undefined,
|
|
42
43
|
assertWriteOk: (value: Val) => void,
|
|
43
44
|
clone: (container: Val) => Val,
|
|
45
|
+
session: AccessControl.Session,
|
|
44
46
|
) {
|
|
45
47
|
let expired = false;
|
|
46
48
|
let location = {
|
|
@@ -51,10 +53,24 @@ export function ManagedReference(
|
|
|
51
53
|
const key = primaryKey === "id" ? (id ?? name) : name;
|
|
52
54
|
const altKey = primaryKey === "id" ? (key === name ? undefined : name) : id;
|
|
53
55
|
let value: unknown;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
|
|
57
|
+
let dynamicContainer: Val.Struct | undefined;
|
|
58
|
+
if ((parent.value as Val.Dynamic)[Val.properties]) {
|
|
59
|
+
dynamicContainer = (parent.value as Val.Dynamic)[Val.properties](parent.rootOwner, session);
|
|
60
|
+
if (key in (dynamicContainer as Container)) {
|
|
61
|
+
value = (dynamicContainer as Container)[key];
|
|
62
|
+
} else if (altKey !== undefined && altKey in (dynamicContainer as Container)) {
|
|
63
|
+
value = (dynamicContainer as Container)[altKey];
|
|
64
|
+
} else {
|
|
65
|
+
dynamicContainer = undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (dynamicContainer === undefined) {
|
|
69
|
+
if (key in (parent.value as Container)) {
|
|
70
|
+
value = (parent.value as Container)[key];
|
|
71
|
+
} else if (altKey !== undefined) {
|
|
72
|
+
value = (parent.value as Container)[altKey];
|
|
73
|
+
}
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
const reference: Val.Reference = {
|
|
@@ -97,9 +113,16 @@ export function ManagedReference(
|
|
|
97
113
|
|
|
98
114
|
// Now use change to complete the update
|
|
99
115
|
this.change(() => {
|
|
100
|
-
(
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
if (dynamicContainer) {
|
|
117
|
+
(dynamicContainer as Container)[key] = newValue;
|
|
118
|
+
if (altKey !== undefined && altKey in dynamicContainer) {
|
|
119
|
+
delete (dynamicContainer as Container)[altKey];
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
(parent.value as Container)[key] = newValue;
|
|
123
|
+
if (altKey !== undefined && altKey in parent.value) {
|
|
124
|
+
delete (parent.value as Container)[altKey];
|
|
125
|
+
}
|
|
103
126
|
}
|
|
104
127
|
});
|
|
105
128
|
},
|
|
@@ -108,11 +131,21 @@ export function ManagedReference(
|
|
|
108
131
|
if (!parent.original) {
|
|
109
132
|
return undefined;
|
|
110
133
|
}
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
if (dynamicContainer !== undefined) {
|
|
135
|
+
const origProperties = (parent.original as Val.Dynamic)[Val.properties](parent.rootOwner, session);
|
|
136
|
+
if (key in (origProperties as Container)) {
|
|
137
|
+
return (origProperties as Container)[key];
|
|
138
|
+
}
|
|
139
|
+
if (altKey !== undefined) {
|
|
140
|
+
return (origProperties as Container)[altKey];
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
if (key in parent.original) {
|
|
144
|
+
return (parent.original as Container)[key];
|
|
145
|
+
}
|
|
146
|
+
if (altKey !== undefined) {
|
|
147
|
+
return (parent.original as Container)[altKey];
|
|
148
|
+
}
|
|
116
149
|
}
|
|
117
150
|
},
|
|
118
151
|
|
|
@@ -125,9 +158,16 @@ export function ManagedReference(
|
|
|
125
158
|
// In transactions, clone the value if we haven't done so yet
|
|
126
159
|
if (clone && value === this.original) {
|
|
127
160
|
const newValue = clone(value);
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
if (dynamicContainer !== undefined) {
|
|
162
|
+
(dynamicContainer as Container)[key] = newValue;
|
|
163
|
+
if (altKey !== undefined && altKey in dynamicContainer) {
|
|
164
|
+
delete (dynamicContainer as Container)[altKey];
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
(parent.value as Container)[key] = newValue;
|
|
168
|
+
if (altKey !== undefined && altKey in (parent.value as Container)) {
|
|
169
|
+
delete (parent.value as Container)[altKey];
|
|
170
|
+
}
|
|
131
171
|
}
|
|
132
172
|
replaceValue(newValue);
|
|
133
173
|
}
|
|
@@ -149,10 +189,18 @@ export function ManagedReference(
|
|
|
149
189
|
}
|
|
150
190
|
|
|
151
191
|
let value;
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
if (dynamicContainer !== undefined) {
|
|
193
|
+
if (key in dynamicContainer) {
|
|
194
|
+
value = (dynamicContainer as Container)[key];
|
|
195
|
+
} else if (altKey !== undefined && altKey in dynamicContainer) {
|
|
196
|
+
value = (dynamicContainer as Container)[altKey];
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
if (key in parent.value) {
|
|
200
|
+
value = (parent.value as Container)[key];
|
|
201
|
+
} else if (altKey !== undefined && altKey in parent.value) {
|
|
202
|
+
value = (parent.value as Container)[altKey];
|
|
203
|
+
}
|
|
156
204
|
}
|
|
157
205
|
|
|
158
206
|
replaceValue(value);
|
|
@@ -279,7 +279,7 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
|
|
|
279
279
|
};
|
|
280
280
|
|
|
281
281
|
if (manage === PrimitiveManager) {
|
|
282
|
-
// For primitives we don't need a manager so just proxy reads directly
|
|
282
|
+
// For primitives, we don't need a manager so just proxy reads directly
|
|
283
283
|
descriptor.get = function (this: Struct) {
|
|
284
284
|
if (access.mayRead(this[Internal.session], this[Internal.reference].location)) {
|
|
285
285
|
const struct = this[Internal.reference].value as Val.Dynamic;
|
|
@@ -310,7 +310,7 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
|
|
|
310
310
|
}
|
|
311
311
|
};
|
|
312
312
|
} else {
|
|
313
|
-
// For collections we create a managed value
|
|
313
|
+
// For collections, we create a managed value
|
|
314
314
|
let cloneContainer: (container: Val) => Val;
|
|
315
315
|
switch (schema.effectiveMetatype) {
|
|
316
316
|
case Metatype.array:
|
|
@@ -373,6 +373,8 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
|
|
|
373
373
|
return defaultReader?.(this);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
// Value is null or a dynamic property, so just return it
|
|
377
|
+
// TODO Consider to also use Management for dynamic properties
|
|
376
378
|
if (value === null) {
|
|
377
379
|
return value;
|
|
378
380
|
}
|
|
@@ -394,7 +396,15 @@ function configureProperty(supervisor: RootSupervisor, schema: ValueModel) {
|
|
|
394
396
|
};
|
|
395
397
|
|
|
396
398
|
// Clone the container before write
|
|
397
|
-
const ref = ManagedReference(
|
|
399
|
+
const ref = ManagedReference(
|
|
400
|
+
this[Internal.reference],
|
|
401
|
+
pk,
|
|
402
|
+
name,
|
|
403
|
+
id,
|
|
404
|
+
assertWriteOk,
|
|
405
|
+
cloneContainer,
|
|
406
|
+
this[Internal.session],
|
|
407
|
+
);
|
|
398
408
|
|
|
399
409
|
ref.owner = manage(ref, this[Internal.session]);
|
|
400
410
|
|
|
@@ -52,13 +52,9 @@ export class AccessControlServer extends AccessControlBehavior.with("Extension")
|
|
|
52
52
|
override initialize(): MaybePromise {
|
|
53
53
|
this.reactTo(this.events.acl$Changing, this.#validateAccessControlListChanges); // Enhanced Validation
|
|
54
54
|
this.reactTo(this.events.acl$Changed, this.#handleAccessControlListChange); // Event handling for changes
|
|
55
|
-
if (
|
|
56
|
-
this.
|
|
57
|
-
this.events.extension$
|
|
58
|
-
this.events.extension$Changed !== undefined
|
|
59
|
-
) {
|
|
60
|
-
this.reactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges); // Enhanced Validation
|
|
61
|
-
this.reactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange); // Event handling for changes
|
|
55
|
+
if (this.state.extension !== undefined) {
|
|
56
|
+
this.maybeReactTo(this.events.extension$Changing, this.#validateAccessControlExtensionChanges); // Enhanced Validation
|
|
57
|
+
this.maybeReactTo(this.events.extension$Changed, this.#handleAccessControlExtensionChange); // Event handling for changes
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
const lifecycle = this.endpoint.lifecycle as NodeLifecycle;
|
|
@@ -85,15 +85,11 @@ export class GeneralDiagnosticsServer extends Base {
|
|
|
85
85
|
this.reactTo(lifecycle.online, this.#online, { lock: true });
|
|
86
86
|
this.reactTo(lifecycle.goingOffline, this.#goingOffline, { lock: true });
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
if (this.events.activeNetworkFaults$Changed !== undefined) {
|
|
95
|
-
this.reactTo(this.events.activeNetworkFaults$Changed, this.#triggerActiveNetworkFaultsChangedEvent);
|
|
96
|
-
}
|
|
88
|
+
this.maybeReactTo(this.events.activeHardwareFaults$Changed, this.#triggerActiveHardwareFaultsChangedEvent);
|
|
89
|
+
|
|
90
|
+
this.maybeReactTo(this.events.activeRadioFaults$Changed, this.#triggerActiveRadioFaultsChangedEvent);
|
|
91
|
+
|
|
92
|
+
this.maybeReactTo(this.events.activeNetworkFaults$Changed, this.#triggerActiveNetworkFaultsChangedEvent);
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
#validateTestEnabledKey(enableKey: Bytes) {
|
|
@@ -40,9 +40,9 @@ export class ServiceAreaBaseServer extends ServiceAreaBase {
|
|
|
40
40
|
this.reactTo(this.events.supportedMaps$Changing, this.#assertSupportedMaps);
|
|
41
41
|
this.#assertSelectedAreas(this.state.selectedAreas);
|
|
42
42
|
this.reactTo(this.events.selectedAreas$Changing, this.#assertSelectedAreas);
|
|
43
|
-
if (this.state.currentArea !== undefined
|
|
43
|
+
if (this.state.currentArea !== undefined) {
|
|
44
44
|
this.#assertCurrentArea(this.state.currentArea);
|
|
45
|
-
this.
|
|
45
|
+
this.maybeReactTo(this.events.currentArea$Changing, this.#assertCurrentArea);
|
|
46
46
|
}
|
|
47
47
|
if (this.state.progress !== undefined) {
|
|
48
48
|
this.#assertProgress(this.state.progress);
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2025 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ActionContext, Behavior, ClusterBehavior, type ClusterState, ValueSupervisor } from "#behavior/index.js";
|
|
8
|
+
import { Thermostat } from "#clusters/thermostat";
|
|
9
|
+
import { Endpoint } from "#endpoint/Endpoint.js";
|
|
10
|
+
import { BasicSet, Environment, Environmental, InternalError, Logger, ObserverGroup, serialize } from "#general";
|
|
11
|
+
import { ClusterModel, DataModelPath } from "#model";
|
|
12
|
+
import {
|
|
13
|
+
AccessControl,
|
|
14
|
+
assertRemoteActor,
|
|
15
|
+
Fabric,
|
|
16
|
+
FabricManager,
|
|
17
|
+
hasRemoteActor,
|
|
18
|
+
PeerAddress,
|
|
19
|
+
Subject,
|
|
20
|
+
Val,
|
|
21
|
+
} from "#protocol";
|
|
22
|
+
import { AttributeId, NodeId, Status, StatusResponse, StatusResponseError } from "#types";
|
|
23
|
+
import { AtomicWriteState } from "./AtomicWriteState.js";
|
|
24
|
+
|
|
25
|
+
const logger = Logger.get("AtomicWriteHandler");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handles atomic write handling according to Matter definitions.
|
|
29
|
+
* The implementation tries to be generic, but is currently only used by the Thermostat cluster, so the atomic write
|
|
30
|
+
* types are imported from there.
|
|
31
|
+
*
|
|
32
|
+
* The logic requires that the cluster behavior implements the following additional events as "pure Observable()" events,
|
|
33
|
+
* because the current implementation logic requires error thrown by the event handlers to signal validation failures to
|
|
34
|
+
* be thrown back to te emitter. This is not the case for official state events.
|
|
35
|
+
* * `${attributeName}$AtomicChanging` - emitted when an attribute is changed as part of an atomic write, before the value
|
|
36
|
+
* is actually changed. Receives the new value, the old value and the action context as parameters.
|
|
37
|
+
* * `${attributeName}$AtomicChanged` - emitted when an attribute is changed as part of an atomic write, after the value
|
|
38
|
+
* is actually changed. Receives the new value, the old value and the action context as parameters.
|
|
39
|
+
*
|
|
40
|
+
* TODO: Move out of thermostat behavior into a more generic behavior handler once used by other clusters too. Then we
|
|
41
|
+
* also need to adjust how it is handled.
|
|
42
|
+
* Proper solution might be to add the handling of the atomic Request command on interaction level and leave the
|
|
43
|
+
* transaction open until it is rolled back or committed. This might have side effects on other parts of the system though.
|
|
44
|
+
* So lets do that later when we have more clusters using it.
|
|
45
|
+
*/
|
|
46
|
+
export class AtomicWriteHandler {
|
|
47
|
+
#observers = new ObserverGroup();
|
|
48
|
+
#pendingWrites = new BasicSet<AtomicWriteState>();
|
|
49
|
+
|
|
50
|
+
constructor(fabricManager: FabricManager) {
|
|
51
|
+
this.#observers.on(fabricManager.events.deleted, fabric => this.#handleFabricRemoval(fabric));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static [Environmental.create](env: Environment) {
|
|
55
|
+
const instance = new AtomicWriteHandler(env.get(FabricManager));
|
|
56
|
+
env.set(AtomicWriteHandler, instance);
|
|
57
|
+
return instance;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
close() {
|
|
61
|
+
this.#observers.close();
|
|
62
|
+
for (const writeState of Array.from(this.#pendingWrites)) {
|
|
63
|
+
writeState.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initializes an AtomicWrite state for the given request, context, endpoint and cluster.
|
|
69
|
+
* It also implements all relevant validation according to the Matter spec.
|
|
70
|
+
*/
|
|
71
|
+
#initializeState<B extends Behavior.Type>(
|
|
72
|
+
{ requestType, attributeRequests, timeout }: Thermostat.AtomicRequest,
|
|
73
|
+
context: ActionContext,
|
|
74
|
+
endpoint: Endpoint,
|
|
75
|
+
cluster: B,
|
|
76
|
+
) {
|
|
77
|
+
if (!ClusterBehavior.is(cluster) || !cluster.schema) {
|
|
78
|
+
throw new InternalError("Cluster behavior expected for atomic write handler");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure we have a valid peer and so also associated fabric
|
|
82
|
+
const peerAddress = this.#assertValidPeer(context);
|
|
83
|
+
|
|
84
|
+
// Validate AttributeRequests
|
|
85
|
+
if (attributeRequests.length === 0) {
|
|
86
|
+
throw new StatusResponse.InvalidCommandError("No attribute requests provided");
|
|
87
|
+
}
|
|
88
|
+
const attributes = new Map<AttributeId, string>();
|
|
89
|
+
for (const attr of attributeRequests) {
|
|
90
|
+
const [attributeName, _] =
|
|
91
|
+
Object.entries((cluster as ClusterBehavior.Type).cluster.attributes).find(
|
|
92
|
+
([_, { id }]) => id === attr,
|
|
93
|
+
) ?? [];
|
|
94
|
+
if (attributeName === undefined || endpoint.stateOf(cluster.id)[attr] === undefined) {
|
|
95
|
+
throw new StatusResponse.InvalidCommandError(`Attribute ${attr} not supported by cluster`);
|
|
96
|
+
}
|
|
97
|
+
if (attributes.has(attr)) {
|
|
98
|
+
throw new StatusResponse.InvalidCommandError("Duplicate attribute in attribute requests");
|
|
99
|
+
}
|
|
100
|
+
attributes.set(attr, attributeName);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const existingState = this.#pendingWrites.find(
|
|
104
|
+
s =>
|
|
105
|
+
PeerAddress.is(s.peerAddress, peerAddress) &&
|
|
106
|
+
s.endpoint.number == endpoint.number &&
|
|
107
|
+
s.clusterId === (cluster as ClusterBehavior.Type).cluster.id,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (requestType === Thermostat.RequestType.BeginWrite) {
|
|
111
|
+
if (timeout === undefined) {
|
|
112
|
+
throw new StatusResponse.InvalidCommandError("Timeout missing for BeginWrite request");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
existingState !== undefined &&
|
|
117
|
+
existingState.attributeRequests.some(attr => attributeRequests.includes(attr))
|
|
118
|
+
) {
|
|
119
|
+
throw new StatusResponse.InvalidCommandError(
|
|
120
|
+
"An atomic write for at least one of the attributes is already in progress for this peer",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const initialValues: Val.Struct = {};
|
|
125
|
+
for (const attr of attributeRequests) {
|
|
126
|
+
initialValues[attr] = endpoint.stateOf(cluster.id)[attr];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const state = new AtomicWriteState(
|
|
130
|
+
peerAddress,
|
|
131
|
+
endpoint,
|
|
132
|
+
cluster.cluster.id,
|
|
133
|
+
attributeRequests,
|
|
134
|
+
timeout,
|
|
135
|
+
attributes,
|
|
136
|
+
initialValues,
|
|
137
|
+
);
|
|
138
|
+
this.#pendingWrites.add(state);
|
|
139
|
+
state.closed.on(() => void this.#pendingWrites.delete(state));
|
|
140
|
+
logger.debug("Added atomic write state:", state);
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
if (existingState === undefined) {
|
|
144
|
+
throw new StatusResponse.InvalidInStateError("No atomic write in progress for this peer");
|
|
145
|
+
}
|
|
146
|
+
if (
|
|
147
|
+
existingState.attributeRequests.length !== attributeRequests.length ||
|
|
148
|
+
!existingState.attributeRequests.every(attr => attributeRequests.includes(attr))
|
|
149
|
+
) {
|
|
150
|
+
throw new StatusResponse.InvalidInStateError("Attribute requests do not match existing atomic write");
|
|
151
|
+
}
|
|
152
|
+
return existingState;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Implements the begin write logic for an atomic write.
|
|
157
|
+
*/
|
|
158
|
+
beginWrite(
|
|
159
|
+
request: Thermostat.AtomicRequest,
|
|
160
|
+
context: ActionContext,
|
|
161
|
+
endpoint: Endpoint,
|
|
162
|
+
cluster: Behavior.Type,
|
|
163
|
+
): Thermostat.AtomicResponse {
|
|
164
|
+
if (!hasRemoteActor(context)) {
|
|
165
|
+
throw new StatusResponse.InvalidCommandError("AtomicRequest requires a remote actor");
|
|
166
|
+
}
|
|
167
|
+
if (!ClusterBehavior.is(cluster) || !cluster.schema) {
|
|
168
|
+
throw new InternalError("Cluster behavior expected for atomic write handler");
|
|
169
|
+
}
|
|
170
|
+
let commandStatusCode = Status.Success;
|
|
171
|
+
const attributeStatus = request.attributeRequests.map(attr => {
|
|
172
|
+
let statusCode = Status.Success;
|
|
173
|
+
const attributeModel = (cluster.schema!.conformant as ClusterModel.Conformant).attributes.for(attr);
|
|
174
|
+
if (!attributeModel?.quality.atomic) {
|
|
175
|
+
statusCode = Status.InvalidAction;
|
|
176
|
+
} else if (this.#pendingWriteStateForAttribute(endpoint, cluster, attr) !== undefined) {
|
|
177
|
+
statusCode = Status.Busy;
|
|
178
|
+
} else {
|
|
179
|
+
const { writeLevel } = cluster.supervisor.get(attributeModel).access.limits;
|
|
180
|
+
const location = {
|
|
181
|
+
path: DataModelPath.none,
|
|
182
|
+
endpoint: endpoint.number,
|
|
183
|
+
cluster: cluster.cluster.id,
|
|
184
|
+
owningFabric: context.fabric,
|
|
185
|
+
};
|
|
186
|
+
if (context.authorityAt(writeLevel, location) !== AccessControl.Authority.Granted) {
|
|
187
|
+
statusCode = Status.UnsupportedAccess;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (statusCode !== Status.Success) {
|
|
192
|
+
commandStatusCode = Status.Failure;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
attributeId: attr,
|
|
196
|
+
statusCode,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
let timeout;
|
|
201
|
+
if (commandStatusCode === Status.Success) {
|
|
202
|
+
const state = this.#initializeState(request, context, endpoint, cluster);
|
|
203
|
+
timeout = state.timeout;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
statusCode: commandStatusCode,
|
|
208
|
+
attributeStatus,
|
|
209
|
+
timeout,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Handles writing a value for an attribute as part of an ongoing atomic write.
|
|
215
|
+
* It uses the *$AtomicChanging* event to trigger validation of the partial write.
|
|
216
|
+
*/
|
|
217
|
+
writeAttribute(
|
|
218
|
+
context: ValueSupervisor.Session,
|
|
219
|
+
endpoint: Endpoint,
|
|
220
|
+
cluster: Behavior.Type,
|
|
221
|
+
attribute: AttributeId,
|
|
222
|
+
value: unknown,
|
|
223
|
+
) {
|
|
224
|
+
const state = this.#assertPendingWriteForAttributeAndPeer(context, endpoint, cluster, attribute);
|
|
225
|
+
const attributeName = state.attributeNames.get(attribute)!;
|
|
226
|
+
logger.debug(`Writing pending value for attribute ${attributeName}, ${attribute} in atomic write`, value);
|
|
227
|
+
// TODO currently we only handle this one changing, so checking other state within the event potentially use
|
|
228
|
+
// older values. We need to tweak the state for a complete solution. But ok for now!
|
|
229
|
+
endpoint
|
|
230
|
+
.eventsOf(cluster.id)
|
|
231
|
+
[
|
|
232
|
+
`${attributeName}$AtomicChanging`
|
|
233
|
+
]?.emit(value, state.pendingAttributeValues[attribute] !== undefined ? state.pendingAttributeValues[attribute] : state.initialValues[attribute], context);
|
|
234
|
+
state.pendingAttributeValues[attribute] = value;
|
|
235
|
+
logger.debug("Atomic write state after current write:", state);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Implements the commit logic for an atomic write.
|
|
240
|
+
*/
|
|
241
|
+
async commitWrite<B extends Behavior.Type>(
|
|
242
|
+
request: Thermostat.AtomicRequest,
|
|
243
|
+
context: ActionContext,
|
|
244
|
+
endpoint: Endpoint,
|
|
245
|
+
cluster: B,
|
|
246
|
+
clusterState: ClusterState.Type<any, B>,
|
|
247
|
+
): Promise<Thermostat.AtomicResponse> {
|
|
248
|
+
const state = this.#initializeState(request, context, endpoint, cluster);
|
|
249
|
+
|
|
250
|
+
let commandStatusCode = Status.Success;
|
|
251
|
+
const attributeStatus = [];
|
|
252
|
+
for (const [attr, value] of Object.entries(state.pendingAttributeValues)) {
|
|
253
|
+
let statusCode = Status.Success;
|
|
254
|
+
try {
|
|
255
|
+
const attributeName = state.attributeNames.get(AttributeId(Number(attr)))!;
|
|
256
|
+
endpoint
|
|
257
|
+
.eventsOf(cluster.id)
|
|
258
|
+
[`${attributeName}$AtomicChanging`]?.emit(value, endpoint.stateOf(cluster.id)[attr], context);
|
|
259
|
+
endpoint
|
|
260
|
+
.eventsOf(cluster.id)
|
|
261
|
+
[`${attributeName}$AtomicChanged`]?.emit(value, endpoint.stateOf(cluster.id)[attr], context);
|
|
262
|
+
(clusterState as any)[attr] = value;
|
|
263
|
+
await context.transaction?.commit();
|
|
264
|
+
} catch (error) {
|
|
265
|
+
await context.transaction?.rollback();
|
|
266
|
+
logger.info(`Failed to write attribute ${attr} during atomic write commit: ${error}`);
|
|
267
|
+
statusCode = error instanceof StatusResponseError ? error.code : Status.Failure;
|
|
268
|
+
// If one fails with ConstraintError, the whole command should return ConstraintError, otherwise Failure
|
|
269
|
+
commandStatusCode =
|
|
270
|
+
commandStatusCode === Status.Failure
|
|
271
|
+
? Status.Failure
|
|
272
|
+
: commandStatusCode === Status.ConstraintError
|
|
273
|
+
? Status.ConstraintError
|
|
274
|
+
: Status.Failure;
|
|
275
|
+
}
|
|
276
|
+
attributeStatus.push({
|
|
277
|
+
attributeId: AttributeId(Number(attr)),
|
|
278
|
+
statusCode,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
state.close(); // Irrelevant of the outcome the state is closed
|
|
282
|
+
return {
|
|
283
|
+
statusCode: commandStatusCode,
|
|
284
|
+
attributeStatus,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Implements the rollback logic for an atomic write.
|
|
290
|
+
*/
|
|
291
|
+
rollbackWrite(
|
|
292
|
+
request: Thermostat.AtomicRequest,
|
|
293
|
+
context: ActionContext,
|
|
294
|
+
endpoint: Endpoint,
|
|
295
|
+
cluster: Behavior.Type,
|
|
296
|
+
): Thermostat.AtomicResponse {
|
|
297
|
+
const state = this.#initializeState(request, context, endpoint, cluster);
|
|
298
|
+
state.close();
|
|
299
|
+
return {
|
|
300
|
+
statusCode: Status.Success,
|
|
301
|
+
attributeStatus: state.attributeRequests.map(attr => ({
|
|
302
|
+
attributeId: attr,
|
|
303
|
+
statusCode: Status.Success,
|
|
304
|
+
})),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Handles fabric removal by closing all pending atomic write states for peers on the removed fabric.
|
|
310
|
+
*/
|
|
311
|
+
#handleFabricRemoval(fabric: Fabric) {
|
|
312
|
+
const fabricIndex = fabric.fabricIndex;
|
|
313
|
+
for (const writeState of Array.from(this.#pendingWrites)) {
|
|
314
|
+
if (writeState.peerAddress.fabricIndex === fabricIndex) {
|
|
315
|
+
logger.debug(
|
|
316
|
+
`Closing atomic write state for peer ${writeState.peerAddress.toString()} on endpoint ${writeState.endpoint.id} due to fabric removal`,
|
|
317
|
+
);
|
|
318
|
+
writeState.close();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Returns the pending write state for the given attribute, if any.
|
|
325
|
+
*/
|
|
326
|
+
#pendingWriteStateForAttribute(endpoint: Endpoint, cluster: Behavior.Type, attribute: AttributeId) {
|
|
327
|
+
const writeStates = this.#pendingWrites.filter(
|
|
328
|
+
s => s.endpoint.number == endpoint.number && s.clusterId === (cluster as ClusterBehavior.Type).cluster.id,
|
|
329
|
+
);
|
|
330
|
+
if (writeStates.length === 0) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
const attrWriteStates = writeStates.filter(({ attributeRequests }) => attributeRequests.includes(attribute));
|
|
334
|
+
if (attrWriteStates.length === 0) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
if (attrWriteStates.length > 1) {
|
|
338
|
+
throw new InternalError("Multiple atomic write states found for the same attribute. Should never happen");
|
|
339
|
+
}
|
|
340
|
+
return attrWriteStates[0];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Returns the pending value for the given attribute and peer, if any.
|
|
345
|
+
*/
|
|
346
|
+
pendingValueForAttributeAndPeer(
|
|
347
|
+
context: ValueSupervisor.Session,
|
|
348
|
+
endpoint: Endpoint,
|
|
349
|
+
cluster: Behavior.Type,
|
|
350
|
+
attribute: AttributeId,
|
|
351
|
+
) {
|
|
352
|
+
const peerAddress = this.#derivePeerAddress(context);
|
|
353
|
+
if (peerAddress === undefined) {
|
|
354
|
+
// No valid peer address could be derived from the session
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
const attrWriteState = this.#pendingWriteStateForAttribute(endpoint, cluster, attribute);
|
|
358
|
+
if (attrWriteState === undefined) {
|
|
359
|
+
// No pending write for this attribute
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
if (!PeerAddress.is(attrWriteState.peerAddress, peerAddress)) {
|
|
363
|
+
// Pending state s for an other peer
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
logger.debug(
|
|
367
|
+
`Found pending value for attribute ${attribute} for peer ${peerAddress.nodeId}`,
|
|
368
|
+
serialize(attrWriteState.pendingAttributeValues[attribute]),
|
|
369
|
+
);
|
|
370
|
+
return attrWriteState.pendingAttributeValues[attribute];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#assertPendingWriteForAttributeAndPeer(
|
|
374
|
+
session: ValueSupervisor.Session,
|
|
375
|
+
endpoint: Endpoint,
|
|
376
|
+
cluster: Behavior.Type,
|
|
377
|
+
attribute: AttributeId,
|
|
378
|
+
) {
|
|
379
|
+
const attrWriteState = this.#pendingWriteStateForAttribute(endpoint, cluster, attribute);
|
|
380
|
+
if (attrWriteState === undefined) {
|
|
381
|
+
throw new StatusResponse.InvalidInStateError("There is no atomic write in progress for this attribute");
|
|
382
|
+
}
|
|
383
|
+
const peerAddress = this.#derivePeerAddress(session);
|
|
384
|
+
if (peerAddress === undefined) {
|
|
385
|
+
throw new StatusResponse.InvalidInStateError("There is no atomic write in progress for this peer");
|
|
386
|
+
}
|
|
387
|
+
if (!PeerAddress.is(attrWriteState.peerAddress, peerAddress)) {
|
|
388
|
+
throw new StatusResponse.BusyError("Attribute is part of an atomic write in progress for a different peer");
|
|
389
|
+
}
|
|
390
|
+
return attrWriteState;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#derivePeerAddress(session: ValueSupervisor.Session) {
|
|
394
|
+
if (
|
|
395
|
+
hasRemoteActor(session) &&
|
|
396
|
+
Subject.isNode(session.subject) &&
|
|
397
|
+
NodeId.isOperationalNodeId(session.subject.id)
|
|
398
|
+
) {
|
|
399
|
+
return PeerAddress({ fabricIndex: session.fabric, nodeId: NodeId(session.subject.id) });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#assertValidPeer(context: ActionContext) {
|
|
404
|
+
assertRemoteActor(context); // Also validate that we have a remote actor for processing the command
|
|
405
|
+
|
|
406
|
+
const peerAddress = this.#derivePeerAddress(context);
|
|
407
|
+
if (!context.session.associatedFabric || peerAddress === undefined) {
|
|
408
|
+
throw new StatusResponse.InvalidCommandError("AtomicRequest requires an operational session");
|
|
409
|
+
}
|
|
410
|
+
return peerAddress;
|
|
411
|
+
}
|
|
412
|
+
}
|