@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
|
@@ -4,14 +4,1498 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { ActionContext } from "#behavior/context/ActionContext.js";
|
|
8
|
+
import { ValueSupervisor } from "#behavior/supervision/ValueSupervisor.js";
|
|
9
|
+
import { OccupancySensingServer } from "#behaviors/occupancy-sensing";
|
|
10
|
+
import { TemperatureMeasurementServer } from "#behaviors/temperature-measurement";
|
|
11
|
+
import { OccupancySensing } from "#clusters/occupancy-sensing";
|
|
12
|
+
import { Thermostat } from "#clusters/thermostat";
|
|
13
|
+
import { Endpoint } from "#endpoint/Endpoint.js";
|
|
14
|
+
import {
|
|
15
|
+
Bytes,
|
|
16
|
+
cropValueRange,
|
|
17
|
+
deepCopy,
|
|
18
|
+
Entropy,
|
|
19
|
+
ImplementationError,
|
|
20
|
+
InternalError,
|
|
21
|
+
Logger,
|
|
22
|
+
Observable,
|
|
23
|
+
} from "#general";
|
|
24
|
+
import { FieldElement } from "#model";
|
|
25
|
+
import { hasLocalActor, Val } from "#protocol";
|
|
26
|
+
import { ClusterType, StatusResponse, TypeFromPartialBitSchema } from "#types";
|
|
27
|
+
import { AtomicWriteHandler } from "./AtomicWriteHandler.js";
|
|
9
28
|
import { ThermostatBehavior } from "./ThermostatBehavior.js";
|
|
10
29
|
|
|
30
|
+
const logger = Logger.get("ThermostatServer");
|
|
31
|
+
|
|
32
|
+
// Enable some features we need for implementation, they will be reset at the end again
|
|
33
|
+
const ThermostatBehaviorLogicBase = ThermostatBehavior.with(
|
|
34
|
+
Thermostat.Feature.Heating,
|
|
35
|
+
Thermostat.Feature.Cooling,
|
|
36
|
+
Thermostat.Feature.Occupancy,
|
|
37
|
+
Thermostat.Feature.AutoMode,
|
|
38
|
+
Thermostat.Feature.Presets,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Enhance Schema to define conformance for some of the additional state attributes
|
|
42
|
+
const schema = ThermostatBehaviorLogicBase.schema!.extend({
|
|
43
|
+
children: [
|
|
44
|
+
FieldElement({
|
|
45
|
+
name: "PersistedPresets",
|
|
46
|
+
type: "list",
|
|
47
|
+
conformance: "[PRES]",
|
|
48
|
+
quality: "N",
|
|
49
|
+
children: [FieldElement({ name: "entry", type: "PresetStruct" })],
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
|
|
11
54
|
/**
|
|
12
55
|
* This is the default server implementation of {@link ThermostatBehavior}.
|
|
13
56
|
*
|
|
14
57
|
* The Matter specification requires the Thermostat cluster to support features we do not enable by default. You should
|
|
15
58
|
* use {@link ThermostatServer.with} to specialize the class for the features your implementation supports.
|
|
59
|
+
* We implement all features beside the following:
|
|
60
|
+
* * MatterScheduleConfiguration: This feature is provisional.
|
|
61
|
+
* * ScheduleConfiguration: This feature is deprecated and not allowed to be enabled.
|
|
62
|
+
* * Setback: This feature is considered deprecated.
|
|
63
|
+
* * The use of the "setpointHoldExpiryTimestamp" attribute is currently not supported.
|
|
64
|
+
*
|
|
65
|
+
* This implementation mainly provides all validation and base logic required by the Matter specification.
|
|
66
|
+
* It implements some thermostat logic, partially beyond Matter specification definition, notably:
|
|
67
|
+
* * Adjust the setpoints when a preset is activated
|
|
68
|
+
* If this behavior is not desired, you can override the setActivePresetRequest method but should call
|
|
69
|
+
* handleSetActivePresetRequest() to ensure compliance with the specification.
|
|
70
|
+
*
|
|
71
|
+
* The implementation also adds enhanced system mode logic that can be enabled by setting the state field
|
|
72
|
+
* useAutomaticModeManagement to true. When enabled, the thermostat will:
|
|
73
|
+
* * Adjust the thermostat running mode when in Auto system mode and the Setback feature is also supported
|
|
74
|
+
* * Determine the system mode and/or running mode based on temperature changes
|
|
75
|
+
*
|
|
76
|
+
* For local temperature or occupancy values we check if there is a local cluster available on the same endpoint and use
|
|
77
|
+
* them, alternatively raw measurements can be set in the states externalMeasuredIndoorTemperature and
|
|
78
|
+
* externallyMeasuredOccupancy. The OutdoorTemperature can be set directly on the attribute if supported.
|
|
79
|
+
* The RemoteSensing attribute need to be set correctly as needed by the developer to identify the measurement source.
|
|
80
|
+
*
|
|
81
|
+
* The following custom events are provided:
|
|
82
|
+
* * calibratedTemperature$Changed: Emitted when the measured local temperature changes including any calibration applied. This event is mainly useful when the localTemperatureNotExposed feature is used.
|
|
83
|
+
*
|
|
84
|
+
* Important note: To access the current local temperature (including all calibrations applied) please use
|
|
85
|
+
* this.internal.localTemperature because the localTemperature attribute in state might be null depending on the
|
|
86
|
+
* configured features.
|
|
87
|
+
*
|
|
88
|
+
* TODO: Currently the general purpose "atomic write" Matter feature is only implemented in this specific cluster because
|
|
89
|
+
* only used here so far. Also see information in AtomicWriteHandler.ts.
|
|
16
90
|
*/
|
|
17
|
-
export class
|
|
91
|
+
export class ThermostatBaseServer extends ThermostatBehaviorLogicBase {
|
|
92
|
+
declare protected internal: ThermostatBaseServer.Internal;
|
|
93
|
+
declare state: ThermostatBaseServer.State;
|
|
94
|
+
declare events: ThermostatBaseServer.Events;
|
|
95
|
+
static override readonly schema = schema;
|
|
96
|
+
|
|
97
|
+
override async initialize() {
|
|
98
|
+
if (this.features.scheduleConfiguration) {
|
|
99
|
+
throw new ImplementationError("ScheduleConfiguration features is deprecated and not allowed to be enabled");
|
|
100
|
+
}
|
|
101
|
+
if (this.features.setback) {
|
|
102
|
+
throw new ImplementationError("Setback feature is deprecated and not allowed to be enabled");
|
|
103
|
+
}
|
|
104
|
+
if (this.features.matterScheduleConfiguration) {
|
|
105
|
+
logger.warn("MatterScheduleConfiguration feature is not yet implemented. Please do not activate it");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Initialize persisted presets from defaults if not already set
|
|
109
|
+
const options = this.endpoint.behaviors.optionsFor(ThermostatBaseServer) as
|
|
110
|
+
| {
|
|
111
|
+
presets: Thermostat.Preset[] | undefined;
|
|
112
|
+
}
|
|
113
|
+
| undefined;
|
|
114
|
+
if (this.features.presets && this.state.persistedPresets === undefined) {
|
|
115
|
+
this.state.persistedPresets = options?.presets ?? [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add this check because we currently do not have a max in Schema and might have old invalid max values
|
|
119
|
+
if (this.state.minSetpointDeadBand > 127) {
|
|
120
|
+
this.state.minSetpointDeadBand = 20;
|
|
121
|
+
}
|
|
122
|
+
if (this.state.minSetpointDeadBand < 0 || this.state.minSetpointDeadBand > 127) {
|
|
123
|
+
throw new ImplementationError("minSetpointDeadBand is out of valid range 0..127");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Initialize all the validation and logic handling
|
|
127
|
+
this.#setupValidations();
|
|
128
|
+
this.#setupTemperatureMeasurementIntegration();
|
|
129
|
+
this.#setupOccupancyIntegration();
|
|
130
|
+
this.#setupModeHandling();
|
|
131
|
+
this.#setupThermostatLogic();
|
|
132
|
+
this.#setupPresets();
|
|
133
|
+
|
|
134
|
+
// We store these values internally because we need to restore them after any write try
|
|
135
|
+
this.internal.minSetpointDeadBand = this.state.minSetpointDeadBand;
|
|
136
|
+
this.internal.controlSequenceOfOperation = this.state.controlSequenceOfOperation;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The default implementation of the SetpointRaiseLower command. It handles all validation and setpoint adjustments
|
|
141
|
+
* required by the Matter specification. This method only changes the Occupied setpoints.
|
|
142
|
+
*/
|
|
143
|
+
override setpointRaiseLower({ mode, amount }: Thermostat.SetpointRaiseLowerRequest) {
|
|
144
|
+
if (mode === Thermostat.SetpointRaiseLowerMode.Heat && !this.features.heating) {
|
|
145
|
+
throw new StatusResponse.InvalidCommandError(
|
|
146
|
+
"Heating feature is not supported but Heat mode was requested",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (mode === Thermostat.SetpointRaiseLowerMode.Cool && !this.features.cooling) {
|
|
150
|
+
throw new StatusResponse.InvalidCommandError(
|
|
151
|
+
"Cooling feature is not supported but Cool mode was requested",
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
amount *= 10; // Convert to same base as the setpoints
|
|
156
|
+
|
|
157
|
+
// We only care about Occupied setpoints as by SDK implementation
|
|
158
|
+
if (mode === Thermostat.SetpointRaiseLowerMode.Both) {
|
|
159
|
+
if (this.features.heating && this.features.cooling) {
|
|
160
|
+
let desiredCoolingSetpoint = this.state.occupiedCoolingSetpoint + amount;
|
|
161
|
+
const coolLimit = desiredCoolingSetpoint - this.#clampSetpointToLimits("Cool", desiredCoolingSetpoint);
|
|
162
|
+
let desiredHeatingSetpoint = this.state.occupiedHeatingSetpoint + amount;
|
|
163
|
+
const heatLimit = desiredHeatingSetpoint - this.#clampSetpointToLimits("Heat", desiredHeatingSetpoint);
|
|
164
|
+
if (coolLimit !== 0 || heatLimit !== 0) {
|
|
165
|
+
if (Math.abs(coolLimit) <= Math.abs(heatLimit)) {
|
|
166
|
+
// We are limited by the Heating Limit
|
|
167
|
+
desiredHeatingSetpoint = desiredHeatingSetpoint - heatLimit;
|
|
168
|
+
desiredCoolingSetpoint = desiredCoolingSetpoint - heatLimit;
|
|
169
|
+
} else {
|
|
170
|
+
// We are limited by Cooling Limit
|
|
171
|
+
desiredHeatingSetpoint = desiredHeatingSetpoint - coolLimit;
|
|
172
|
+
desiredCoolingSetpoint = desiredCoolingSetpoint - coolLimit;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.state.occupiedCoolingSetpoint = desiredCoolingSetpoint;
|
|
176
|
+
this.state.occupiedHeatingSetpoint = desiredHeatingSetpoint;
|
|
177
|
+
} else if (this.features.cooling) {
|
|
178
|
+
this.state.occupiedCoolingSetpoint = this.#clampSetpointToLimits(
|
|
179
|
+
"Cool",
|
|
180
|
+
this.state.occupiedCoolingSetpoint + amount,
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
this.state.occupiedHeatingSetpoint = this.#clampSetpointToLimits(
|
|
184
|
+
"Heat",
|
|
185
|
+
this.state.occupiedHeatingSetpoint + amount,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (mode === Thermostat.SetpointRaiseLowerMode.Cool) {
|
|
192
|
+
const desiredCoolingSetpoint = this.#clampSetpointToLimits(
|
|
193
|
+
"Cool",
|
|
194
|
+
this.state.occupiedCoolingSetpoint + amount,
|
|
195
|
+
);
|
|
196
|
+
if (this.features.autoMode) {
|
|
197
|
+
let heatingSetpoint = this.state.occupiedHeatingSetpoint;
|
|
198
|
+
if (desiredCoolingSetpoint - heatingSetpoint < this.setpointDeadBand) {
|
|
199
|
+
// We are limited by the Heating Setpoint
|
|
200
|
+
heatingSetpoint = desiredCoolingSetpoint - this.setpointDeadBand;
|
|
201
|
+
if (heatingSetpoint === this.#clampSetpointToLimits("Heat", heatingSetpoint)) {
|
|
202
|
+
// Desired cooling setpoint is enforceable
|
|
203
|
+
// Set the new cooling and heating setpoints
|
|
204
|
+
this.state.occupiedHeatingSetpoint = heatingSetpoint;
|
|
205
|
+
} else {
|
|
206
|
+
throw new StatusResponse.InvalidCommandError(
|
|
207
|
+
"Could Not adjust heating setpoint to maintain dead band!",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
this.state.occupiedCoolingSetpoint = desiredCoolingSetpoint;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (mode === Thermostat.SetpointRaiseLowerMode.Heat) {
|
|
217
|
+
const desiredHeatingSetpoint = this.#clampSetpointToLimits(
|
|
218
|
+
"Heat",
|
|
219
|
+
this.state.occupiedHeatingSetpoint + amount,
|
|
220
|
+
);
|
|
221
|
+
if (this.features.autoMode) {
|
|
222
|
+
let coolingSetpoint = this.state.occupiedCoolingSetpoint;
|
|
223
|
+
if (coolingSetpoint - desiredHeatingSetpoint < this.setpointDeadBand) {
|
|
224
|
+
// We are limited by the Cooling Setpoint
|
|
225
|
+
coolingSetpoint = desiredHeatingSetpoint + this.setpointDeadBand;
|
|
226
|
+
if (coolingSetpoint === this.#clampSetpointToLimits("Cool", coolingSetpoint)) {
|
|
227
|
+
// Desired cooling setpoint is enforceable
|
|
228
|
+
// Set the new cooling and heating setpoints
|
|
229
|
+
this.state.occupiedCoolingSetpoint = coolingSetpoint;
|
|
230
|
+
} else {
|
|
231
|
+
throw new StatusResponse.InvalidCommandError(
|
|
232
|
+
"Could Not adjust cooling setpoint to maintain dead band!",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
this.state.occupiedHeatingSetpoint = desiredHeatingSetpoint;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
throw new StatusResponse.InvalidCommandError(`Unsupported SetpointRaiseLowerMode ${mode}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Performs basic validation and sets the active preset handle when valid.
|
|
246
|
+
* This fulfills the basic requirements of the SetActivePresetRequest matter command. Use this method if you need
|
|
247
|
+
* to override setActivePresetRequest to ensure compliance.
|
|
248
|
+
*/
|
|
249
|
+
protected handleSetActivePresetRequest({ presetHandle }: Thermostat.SetActivePresetRequest) {
|
|
250
|
+
let preset: Thermostat.Preset | undefined = undefined;
|
|
251
|
+
if (presetHandle !== null) {
|
|
252
|
+
preset = this.state.persistedPresets?.find(
|
|
253
|
+
p => p.presetHandle !== null && Bytes.areEqual(p.presetHandle, presetHandle),
|
|
254
|
+
);
|
|
255
|
+
if (preset === undefined) {
|
|
256
|
+
throw new StatusResponse.InvalidCommandError("Requested PresetHandle not found");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
logger.info(`Setting active preset handle to`, presetHandle);
|
|
260
|
+
this.state.activePresetHandle = presetHandle;
|
|
261
|
+
|
|
262
|
+
return preset;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* This default implementation of the SetActivePresetRequest command handler sets the active preset and
|
|
267
|
+
* (additionally to specification requirements!) adjusts the occupied setpoints to the preset values if defined.
|
|
268
|
+
*
|
|
269
|
+
* If you do not want this behavior, you can override this method but should call handleSetActivePresetRequest to
|
|
270
|
+
* ensure compliance with the specification.
|
|
271
|
+
*/
|
|
272
|
+
override setActivePresetRequest({ presetHandle }: Thermostat.SetActivePresetRequest) {
|
|
273
|
+
const preset = this.handleSetActivePresetRequest({ presetHandle });
|
|
274
|
+
|
|
275
|
+
if (preset !== undefined) {
|
|
276
|
+
const { heatingSetpoint, coolingSetpoint } = preset;
|
|
277
|
+
if (this.features.heating && heatingSetpoint !== null && heatingSetpoint !== undefined) {
|
|
278
|
+
this.state.occupiedHeatingSetpoint = this.#clampSetpointToLimits("Heat", heatingSetpoint);
|
|
279
|
+
}
|
|
280
|
+
if (this.features.cooling && coolingSetpoint !== null && coolingSetpoint !== undefined) {
|
|
281
|
+
this.state.occupiedCoolingSetpoint = this.#clampSetpointToLimits("Cool", coolingSetpoint);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Determines if the given context is from a command */
|
|
287
|
+
#isCommandContext(context: ActionContext) {
|
|
288
|
+
return "command" in context && context.command;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Whether the thermostat is currently considered occupied
|
|
293
|
+
* Uses the occupancy state if the feature is supported, otherwise always true
|
|
294
|
+
*/
|
|
295
|
+
protected get occupied() {
|
|
296
|
+
return this.features.occupancy ? (this.state.occupancy?.occupied ?? true) : true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The current heating setpoint depending on occupancy */
|
|
300
|
+
protected get heatingSetpoint() {
|
|
301
|
+
if (this.occupied) {
|
|
302
|
+
return this.state.occupiedHeatingSetpoint;
|
|
303
|
+
}
|
|
304
|
+
return this.state.unoccupiedHeatingSetpoint;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The current cooling setpoint depending on occupancy */
|
|
308
|
+
protected get coolingSetpoint() {
|
|
309
|
+
if (this.occupied) {
|
|
310
|
+
return this.state.occupiedCoolingSetpoint;
|
|
311
|
+
}
|
|
312
|
+
return this.state.unoccupiedCoolingSetpoint;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Setup basic Thermostat state and logic */
|
|
316
|
+
#setupThermostatLogic() {
|
|
317
|
+
if (this.state.temperatureSetpointHold !== undefined) {
|
|
318
|
+
// When we support temperature setpoint hold, ensure related states are initialized
|
|
319
|
+
if (this.state.temperatureSetpointHoldDuration === undefined) {
|
|
320
|
+
this.state.temperatureSetpointHoldDuration = null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// TODO Add support for correct Time handling, leave disabled for now
|
|
324
|
+
if (this.state.setpointHoldExpiryTimestamp === undefined) {
|
|
325
|
+
//this.state.setpointHoldExpiryTimestamp = null;
|
|
326
|
+
} else {
|
|
327
|
+
logger.warn(
|
|
328
|
+
"Handling for setpointHoldExpiryTimestamp is not yet implemented. To use this attribute you need to install the needed logic yourself",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
//this.maybeReactTo(this.events.temperatureSetpointHold$Changed, this.#handleTemperatureSetpointHoldChange);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// TODO Add when we adjusted the epoch-s handling to be correct
|
|
336
|
+
/*#handleTemperatureSetpointHoldChange(newValue: Thermostat.TemperatureSetpointHold) {
|
|
337
|
+
if (newValue === Thermostat.TemperatureSetpointHold.SetpointHoldOn) {
|
|
338
|
+
if (
|
|
339
|
+
this.state.temperatureSetpointHoldDuration !== null &&
|
|
340
|
+
this.state.temperatureSetpointHoldDuration! > 0
|
|
341
|
+
) {
|
|
342
|
+
// TODO: convert to use of Seconds and such and real UTC time
|
|
343
|
+
// Also requires adjustment in encoding/decoding of the attribute
|
|
344
|
+
const nowUtc = Time.nowMs - 946_684_800_000; // Still not really UTC, but ok for now
|
|
345
|
+
this.state.setpointHoldExpiryTimestamp = Math.floor(
|
|
346
|
+
nowUtc / 1000 + this.state.temperatureSetpointHoldDuration! * 60,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
this.state.setpointHoldExpiryTimestamp = null;
|
|
351
|
+
}
|
|
352
|
+
}*/
|
|
353
|
+
|
|
354
|
+
/** Whether heating is allowed in the current ControlSequenceOfOperation and features */
|
|
355
|
+
protected get heatingAllowed() {
|
|
356
|
+
return (
|
|
357
|
+
this.features.heating &&
|
|
358
|
+
![
|
|
359
|
+
Thermostat.ControlSequenceOfOperation.CoolingOnly,
|
|
360
|
+
Thermostat.ControlSequenceOfOperation.CoolingAndHeatingWithReheat,
|
|
361
|
+
].includes(this.internal.controlSequenceOfOperation)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Whether cooling is allowed in the current ControlSequenceOfOperation and features */
|
|
366
|
+
protected get coolingAllowed() {
|
|
367
|
+
return (
|
|
368
|
+
this.features.cooling &&
|
|
369
|
+
![
|
|
370
|
+
Thermostat.ControlSequenceOfOperation.HeatingOnly,
|
|
371
|
+
Thermostat.ControlSequenceOfOperation.HeatingWithReheat,
|
|
372
|
+
].includes(this.internal.controlSequenceOfOperation)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Adjust the running mode of the thermostat based on the new system mode when the thermostatRunningMode is supported
|
|
378
|
+
*/
|
|
379
|
+
protected adjustRunningMode(newState: Thermostat.ThermostatRunningMode) {
|
|
380
|
+
if (this.state.thermostatRunningMode === undefined) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
switch (newState) {
|
|
384
|
+
case Thermostat.ThermostatRunningMode.Heat:
|
|
385
|
+
if (!this.heatingAllowed) {
|
|
386
|
+
throw new ImplementationError("Heating is not allowed in the current ControlSequenceOfOperation");
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
case Thermostat.ThermostatRunningMode.Cool:
|
|
390
|
+
if (!this.coolingAllowed) {
|
|
391
|
+
throw new ImplementationError("Cooling is not allowed in the current ControlSequenceOfOperation");
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
this.state.thermostatRunningMode = newState;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Setup integration with TemperatureMeasurement cluster or external temperature state and intialize internal
|
|
400
|
+
* localTemperature state.
|
|
401
|
+
*/
|
|
402
|
+
#setupTemperatureMeasurementIntegration() {
|
|
403
|
+
const preferRemoteTemperature = !!this.state.remoteSensing?.localTemperature;
|
|
404
|
+
if (this.features.localTemperatureNotExposed) {
|
|
405
|
+
if (preferRemoteTemperature) {
|
|
406
|
+
throw new ImplementationError(
|
|
407
|
+
"RemoteSensing cannot be set to LocalTemperature when LocalTemperatureNotExposed feature is enabled",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
logger.debug("LocalTemperatureNotExposed feature is enabled, ignoring local temperature measurement");
|
|
411
|
+
this.state.localTemperature = null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let localTemperature = null;
|
|
415
|
+
if (!preferRemoteTemperature && this.agent.has(TemperatureMeasurementServer)) {
|
|
416
|
+
logger.debug(
|
|
417
|
+
"Using existing TemperatureMeasurement cluster on same endpoint for local temperature measurement",
|
|
418
|
+
);
|
|
419
|
+
if (this.state.externalMeasuredIndoorTemperature !== undefined) {
|
|
420
|
+
logger.warn(
|
|
421
|
+
"Both local TemperatureMeasurement cluster and externalMeasuredIndoorTemperature state are set, using local cluster",
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
this.reactTo(
|
|
425
|
+
this.agent.get(TemperatureMeasurementServer).events.measuredValue$Changed,
|
|
426
|
+
this.#handleMeasuredTemperatureChange,
|
|
427
|
+
);
|
|
428
|
+
localTemperature = this.endpoint.stateOf(TemperatureMeasurementServer).measuredValue;
|
|
429
|
+
} else {
|
|
430
|
+
if (this.state.externalMeasuredIndoorTemperature === undefined) {
|
|
431
|
+
logger.warn(
|
|
432
|
+
"No local TemperatureMeasurement cluster available and externalMeasuredIndoorTemperature state not set. Setting localTemperature to null",
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
logger.info("Using measured temperature via externalMeasuredIndoorTemperature state");
|
|
436
|
+
localTemperature = this.state.externalMeasuredIndoorTemperature ?? null;
|
|
437
|
+
}
|
|
438
|
+
this.reactTo(this.events.externalMeasuredIndoorTemperature$Changed, this.#handleMeasuredTemperatureChange);
|
|
439
|
+
}
|
|
440
|
+
this.#handleMeasuredTemperatureChange(localTemperature); // and initialize
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Handles changes to the measured temperature, applies calibration and update internal and official state.
|
|
445
|
+
*/
|
|
446
|
+
#handleMeasuredTemperatureChange(temperature: number | null) {
|
|
447
|
+
if (temperature !== null && this.state.localTemperatureCalibration !== undefined) {
|
|
448
|
+
temperature += this.state.localTemperatureCalibration * 10;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// When localTemperatureNotExposed feature is enabled, we do not update the attribute because it needs to stay null
|
|
452
|
+
if (!this.features.localTemperatureNotExposed) {
|
|
453
|
+
this.state.localTemperature = temperature;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// When the temperature changes, we always update the internal localTemperature and emit event
|
|
457
|
+
const oldTemperature = this.internal.localTemperature;
|
|
458
|
+
if (temperature !== null && oldTemperature !== temperature) {
|
|
459
|
+
this.internal.localTemperature = temperature;
|
|
460
|
+
this.events.calibratedTemperature$Changed.emit(temperature, oldTemperature, this.context);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Setup integration with OccupancySensing cluster or external occupancy state and initialize internal occupancy
|
|
466
|
+
* state.
|
|
467
|
+
*/
|
|
468
|
+
#setupOccupancyIntegration() {
|
|
469
|
+
if (!this.features.occupancy) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
let currentOccupancy: boolean;
|
|
473
|
+
const preferRemoteOccupancy = !!this.state.remoteSensing?.occupancy;
|
|
474
|
+
if (!preferRemoteOccupancy && this.agent.has(OccupancySensingServer)) {
|
|
475
|
+
logger.debug("Using existing OccupancySensing cluster on same endpoint for local occupancy sensing");
|
|
476
|
+
if (this.state.externallyMeasuredOccupancy !== undefined) {
|
|
477
|
+
logger.warn(
|
|
478
|
+
"Both local OccupancySensing cluster and externallyMeasuredOccupancy state are set, using local cluster",
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
this.reactTo(this.agent.get(OccupancySensingServer).events.occupancy$Changed, this.#handleOccupancyChange);
|
|
482
|
+
currentOccupancy = !!this.endpoint.stateOf(OccupancySensingServer).occupancy.occupied;
|
|
483
|
+
} else {
|
|
484
|
+
if (this.state.externallyMeasuredOccupancy === undefined) {
|
|
485
|
+
currentOccupancy = true;
|
|
486
|
+
logger.warn(
|
|
487
|
+
"No local OccupancySensing cluster available and externallyMeasuredOccupancy state not set",
|
|
488
|
+
);
|
|
489
|
+
} else {
|
|
490
|
+
logger.info("Using occupancy via externallyMeasuredOccupancy state");
|
|
491
|
+
currentOccupancy = this.state.externallyMeasuredOccupancy;
|
|
492
|
+
}
|
|
493
|
+
this.reactTo(this.events.externallyMeasuredOccupancy$Changed, this.#handleExternalOccupancyChange);
|
|
494
|
+
}
|
|
495
|
+
this.#handleExternalOccupancyChange(currentOccupancy); // and initialize
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#handleExternalOccupancyChange(newValue: boolean) {
|
|
499
|
+
this.state.occupancy = { occupied: newValue };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
#handleOccupancyChange(newValue: TypeFromPartialBitSchema<typeof OccupancySensing.Occupancy>) {
|
|
503
|
+
this.state.occupancy = newValue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Setup all validations for the Thermostat behavior */
|
|
507
|
+
#setupValidations() {
|
|
508
|
+
// Validate existing values to match the constraints at initialization
|
|
509
|
+
this.#assertUserSetpointLimits("HeatSetpointLimit");
|
|
510
|
+
this.#assertUserSetpointLimits("CoolSetpointLimit");
|
|
511
|
+
this.#clampSetpointToLimits("Heat", this.state.occupiedHeatingSetpoint);
|
|
512
|
+
this.#clampSetpointToLimits("Heat", this.state.unoccupiedHeatingSetpoint);
|
|
513
|
+
this.#clampSetpointToLimits("Cool", this.state.occupiedCoolingSetpoint);
|
|
514
|
+
this.#clampSetpointToLimits("Cool", this.state.unoccupiedCoolingSetpoint);
|
|
515
|
+
|
|
516
|
+
// Setup reactions for validations on changes
|
|
517
|
+
this.maybeReactTo(this.events.absMinHeatSetpointLimit$Changing, this.#assertAbsMinHeatSetpointLimitChanging);
|
|
518
|
+
this.maybeReactTo(this.events.minHeatSetpointLimit$Changing, this.#assertMinHeatSetpointLimitChanging);
|
|
519
|
+
this.maybeReactTo(this.events.maxHeatSetpointLimit$Changing, this.#assertMaxHeatSetpointLimitChanging);
|
|
520
|
+
this.maybeReactTo(this.events.absMaxHeatSetpointLimit$Changing, this.#assertAbsMaxHeatSetpointLimitChanging);
|
|
521
|
+
this.maybeReactTo(this.events.absMinCoolSetpointLimit$Changing, this.#assertAbsMinCoolSetpointLimitChanging);
|
|
522
|
+
this.maybeReactTo(this.events.minCoolSetpointLimit$Changing, this.#assertMinCoolSetpointLimitChanging);
|
|
523
|
+
this.maybeReactTo(this.events.maxCoolSetpointLimit$Changing, this.#assertMaxCoolSetpointLimitChanging);
|
|
524
|
+
this.maybeReactTo(this.events.absMaxCoolSetpointLimit$Changing, this.#assertAbsMaxCoolSetpointLimitChanging);
|
|
525
|
+
this.maybeReactTo(this.events.occupiedHeatingSetpoint$Changing, this.#assertOccupiedHeatingSetpointChanging);
|
|
526
|
+
this.maybeReactTo(
|
|
527
|
+
this.events.unoccupiedHeatingSetpoint$Changing,
|
|
528
|
+
this.#assertUnoccupiedHeatingSetpointChanging,
|
|
529
|
+
);
|
|
530
|
+
this.maybeReactTo(this.events.occupiedCoolingSetpoint$Changing, this.#assertOccupiedCoolingSetpointChanging);
|
|
531
|
+
this.maybeReactTo(
|
|
532
|
+
this.events.unoccupiedCoolingSetpoint$Changing,
|
|
533
|
+
this.#assertUnoccupiedCoolingSetpointChanging,
|
|
534
|
+
);
|
|
535
|
+
this.maybeReactTo(this.events.remoteSensing$Changing, this.#assertRemoteSensingChanging);
|
|
536
|
+
|
|
537
|
+
// For backwards compatibility, this attributes is optionally writeable. However, any
|
|
538
|
+
// writes to this attribute SHALL be silently ignored. So we just revert any changes.
|
|
539
|
+
this.maybeReactTo(this.events.minSetpointDeadBand$Changing, this.#ensureMinSetpointDeadBandNotWritable);
|
|
540
|
+
this.reactTo(
|
|
541
|
+
this.events.controlSequenceOfOperation$Changing,
|
|
542
|
+
this.#ensureControlSequenceOfOperationNotWritable,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
this.reactTo(this.events.systemMode$Changing, this.#assertSystemModeChanging);
|
|
546
|
+
this.maybeReactTo(this.events.thermostatRunningMode$Changing, this.#assertThermostatRunningModeChanging);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#assertThermostatRunningModeChanging(newRunningMode: Thermostat.ThermostatRunningMode) {
|
|
550
|
+
const forbiddenRunningModes = new Array<Thermostat.ThermostatRunningMode>();
|
|
551
|
+
// We use the internal value here to ensure not a temporarily invalid state during writes
|
|
552
|
+
switch (this.internal.controlSequenceOfOperation) {
|
|
553
|
+
case Thermostat.ControlSequenceOfOperation.CoolingOnly:
|
|
554
|
+
case Thermostat.ControlSequenceOfOperation.CoolingAndHeatingWithReheat:
|
|
555
|
+
forbiddenRunningModes.push(Thermostat.ThermostatRunningMode.Heat);
|
|
556
|
+
break;
|
|
557
|
+
case Thermostat.ControlSequenceOfOperation.HeatingOnly:
|
|
558
|
+
case Thermostat.ControlSequenceOfOperation.HeatingWithReheat:
|
|
559
|
+
forbiddenRunningModes.push(Thermostat.ThermostatRunningMode.Cool);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
if (forbiddenRunningModes.includes(newRunningMode)) {
|
|
563
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
564
|
+
`ThermostatRunningMode ${Thermostat.ThermostatRunningMode[newRunningMode]} is not allowed with ControlSequenceOfOperation ${
|
|
565
|
+
Thermostat.ControlSequenceOfOperation[this.internal.controlSequenceOfOperation]
|
|
566
|
+
}`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#assertSystemModeChanging(newMode: Thermostat.SystemMode) {
|
|
572
|
+
const forbiddenModes = new Array<Thermostat.SystemMode>();
|
|
573
|
+
switch (this.internal.controlSequenceOfOperation) {
|
|
574
|
+
case Thermostat.ControlSequenceOfOperation.CoolingOnly:
|
|
575
|
+
case Thermostat.ControlSequenceOfOperation.CoolingAndHeatingWithReheat:
|
|
576
|
+
forbiddenModes.push(Thermostat.SystemMode.Heat, Thermostat.SystemMode.EmergencyHeat);
|
|
577
|
+
break;
|
|
578
|
+
case Thermostat.ControlSequenceOfOperation.HeatingOnly:
|
|
579
|
+
case Thermostat.ControlSequenceOfOperation.HeatingWithReheat:
|
|
580
|
+
forbiddenModes.push(Thermostat.SystemMode.Cool, Thermostat.SystemMode.Precooling);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
if (forbiddenModes.includes(newMode)) {
|
|
584
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
585
|
+
`SystemMode ${Thermostat.SystemMode[newMode]} is not allowed with ControlSequenceOfOperation ${
|
|
586
|
+
Thermostat.ControlSequenceOfOperation[this.internal.controlSequenceOfOperation]
|
|
587
|
+
}`,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Attribute is not writable, revert any changes */
|
|
593
|
+
#ensureControlSequenceOfOperationNotWritable() {
|
|
594
|
+
this.state.controlSequenceOfOperation = this.internal.controlSequenceOfOperation;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Attribute is not writable, revert any changes, but also ensure proper errors when write try was invalid */
|
|
598
|
+
#ensureMinSetpointDeadBandNotWritable(value: number) {
|
|
599
|
+
if (value < 0 || value > 127) {
|
|
600
|
+
throw new StatusResponse.ConstraintErrorError("MinSetpointDeadBand is out of valid range 0..127");
|
|
601
|
+
}
|
|
602
|
+
this.state.minSetpointDeadBand = this.internal.minSetpointDeadBand;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
#assertRemoteSensingChanging(remoteSensing: TypeFromPartialBitSchema<typeof Thermostat.RemoteSensing>) {
|
|
606
|
+
if (this.features.localTemperatureNotExposed && remoteSensing.localTemperature) {
|
|
607
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
608
|
+
"LocalTemperature is not exposed, so RemoteSensing cannot be set to LocalTemperature",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#assertUnoccupiedCoolingSetpointChanging(setpoint: number, _old: number, context: ActionContext) {
|
|
614
|
+
this.#assertSetpointWithinLimits("Cool", "Unoccupied", setpoint);
|
|
615
|
+
this.#assertSetpointDeadband("Cooling", setpoint);
|
|
616
|
+
// Only ensure Deadband and preset adjustment when the value is written directly (not changed via a command)
|
|
617
|
+
if (!this.#isCommandContext(context)) {
|
|
618
|
+
this.#ensureSetpointDeadband("Cooling", "unoccupied", setpoint);
|
|
619
|
+
|
|
620
|
+
if (this.features.presets && this.state.activePresetHandle !== null && !this.occupied) {
|
|
621
|
+
this.agent.asLocalActor(() => {
|
|
622
|
+
this.state.activePresetHandle = null;
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
#assertUnoccupiedHeatingSetpointChanging(setpoint: number, _old: number, context: ActionContext) {
|
|
629
|
+
this.#assertSetpointWithinLimits("Heat", "Unoccupied", setpoint);
|
|
630
|
+
this.#assertSetpointDeadband("Heating", setpoint);
|
|
631
|
+
// Only ensure Deadband and preset adjustment when the value is written directly (not changed via a command)
|
|
632
|
+
if (!this.#isCommandContext(context)) {
|
|
633
|
+
this.#ensureSetpointDeadband("Heating", "unoccupied", setpoint);
|
|
634
|
+
|
|
635
|
+
if (this.features.presets && this.state.activePresetHandle !== null && !this.occupied) {
|
|
636
|
+
this.agent.asLocalActor(() => {
|
|
637
|
+
this.state.activePresetHandle = null;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#assertAbsMaxCoolSetpointLimitChanging(absMax: number) {
|
|
644
|
+
this.#assertUserSetpointLimits("CoolSetpointLimit", { absMax });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#assertMaxCoolSetpointLimitChanging(max: number) {
|
|
648
|
+
this.#assertUserSetpointLimits("CoolSetpointLimit", { max });
|
|
649
|
+
if (this.features.autoMode) {
|
|
650
|
+
if (max < this.heatSetpointMaximum + this.setpointDeadBand) {
|
|
651
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
652
|
+
`maxCoolSetpointLimit (${max}) must be greater than or equal to maxHeatSetpointLimit (${this.heatSetpointMaximum}) plus minSetpointDeadBand (${this.setpointDeadBand})`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#assertMinCoolSetpointLimitChanging(min: number) {
|
|
659
|
+
this.#assertUserSetpointLimits("CoolSetpointLimit", { min });
|
|
660
|
+
if (this.features.autoMode) {
|
|
661
|
+
if (min < this.heatSetpointMinimum + this.setpointDeadBand) {
|
|
662
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
663
|
+
`minCoolSetpointLimit (${min}) must be greater than or equal to minHeatSetpointLimit (${this.heatSetpointMinimum}) plus minSetpointDeadBand (${this.setpointDeadBand})`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#assertAbsMinCoolSetpointLimitChanging(absMin: number) {
|
|
670
|
+
this.#assertUserSetpointLimits("CoolSetpointLimit", { absMin });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
#assertAbsMaxHeatSetpointLimitChanging(absMax: number) {
|
|
674
|
+
this.#assertUserSetpointLimits("HeatSetpointLimit", { absMax });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#assertMaxHeatSetpointLimitChanging(max: number) {
|
|
678
|
+
this.#assertUserSetpointLimits("HeatSetpointLimit", { max });
|
|
679
|
+
if (this.features.autoMode) {
|
|
680
|
+
if (max > this.coolSetpointMaximum - this.setpointDeadBand) {
|
|
681
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
682
|
+
`maxHeatSetpointLimit (${max}) must be less than or equal to maxCoolSetpointLimit (${this.coolSetpointMaximum}) minus minSetpointDeadBand (${this.setpointDeadBand})`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
#assertMinHeatSetpointLimitChanging(min: number) {
|
|
689
|
+
this.#assertUserSetpointLimits("HeatSetpointLimit", { min });
|
|
690
|
+
if (this.features.autoMode) {
|
|
691
|
+
if (min > this.coolSetpointMinimum - this.setpointDeadBand) {
|
|
692
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
693
|
+
`minHeatSetpointLimit (${min}) must be less than or equal to minCoolSetpointLimit (${this.state.minCoolSetpointLimit}) minus minSetpointDeadBand (${this.setpointDeadBand})`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#assertAbsMinHeatSetpointLimitChanging(absMin: number) {
|
|
700
|
+
this.#assertUserSetpointLimits("HeatSetpointLimit", { absMin });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#assertOccupiedCoolingSetpointChanging(setpoint: number, _old: number, context: ActionContext) {
|
|
704
|
+
this.#assertSetpointWithinLimits("Cool", "Occupied", setpoint);
|
|
705
|
+
this.#assertSetpointDeadband("Cooling", setpoint);
|
|
706
|
+
// Only ensure Deadband and preset adjustment when the value is written directly (not changed via a command)
|
|
707
|
+
if (!this.#isCommandContext(context)) {
|
|
708
|
+
this.#ensureSetpointDeadband("Cooling", "occupied", setpoint);
|
|
709
|
+
|
|
710
|
+
if (this.features.presets && this.state.activePresetHandle !== null && this.occupied) {
|
|
711
|
+
this.agent.asLocalActor(() => {
|
|
712
|
+
this.state.activePresetHandle = null;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#assertOccupiedHeatingSetpointChanging(setpoint: number, _old: number, context: ActionContext) {
|
|
719
|
+
this.#assertSetpointWithinLimits("Heat", "Occupied", setpoint);
|
|
720
|
+
this.#assertSetpointDeadband("Heating", setpoint);
|
|
721
|
+
// Only ensure Deadband and preset adjustment when the value is written directly (not changed via a command)
|
|
722
|
+
if (!this.#isCommandContext(context)) {
|
|
723
|
+
this.#ensureSetpointDeadband("Heating", "occupied", setpoint);
|
|
724
|
+
|
|
725
|
+
if (this.features.presets && this.state.activePresetHandle !== null && this.occupied) {
|
|
726
|
+
this.agent.asLocalActor(() => {
|
|
727
|
+
this.state.activePresetHandle = null;
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* The current mode the thermostat is considered to be in based on local temperature and setpoints
|
|
735
|
+
*/
|
|
736
|
+
protected get temperatureConsideration(): "belowTarget" | "onTarget" | "aboveTarget" | undefined {
|
|
737
|
+
const localTemp = this.internal.localTemperature;
|
|
738
|
+
if (localTemp === null) {
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
const minSetPointDeadband = this.setpointDeadBand;
|
|
742
|
+
const heatingSetpoint = this.heatingSetpoint;
|
|
743
|
+
const coolingSetpoint = this.coolingSetpoint;
|
|
744
|
+
switch (this.state.systemMode) {
|
|
745
|
+
case Thermostat.SystemMode.Heat:
|
|
746
|
+
if (localTemp < heatingSetpoint) {
|
|
747
|
+
return "belowTarget";
|
|
748
|
+
}
|
|
749
|
+
if (localTemp > coolingSetpoint) {
|
|
750
|
+
return "onTarget";
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
case Thermostat.SystemMode.Cool:
|
|
754
|
+
if (localTemp < heatingSetpoint) {
|
|
755
|
+
return "onTarget";
|
|
756
|
+
}
|
|
757
|
+
if (localTemp > coolingSetpoint) {
|
|
758
|
+
return "aboveTarget";
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
case Thermostat.SystemMode.Auto:
|
|
762
|
+
if (localTemp < heatingSetpoint - minSetPointDeadband) {
|
|
763
|
+
return "belowTarget";
|
|
764
|
+
}
|
|
765
|
+
if (localTemp > coolingSetpoint + minSetPointDeadband) {
|
|
766
|
+
return "aboveTarget";
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
return "onTarget";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
get #heatDefaults() {
|
|
774
|
+
return {
|
|
775
|
+
absMin: 700,
|
|
776
|
+
absMax: 3000,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
get #coolDefaults() {
|
|
781
|
+
return {
|
|
782
|
+
absMin: 1600,
|
|
783
|
+
absMax: 3200,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Used to validate generically that user configurable limits must be within device limits follow:
|
|
789
|
+
* * AbsMinHeatSetpointLimit <= MinHeatSetpointLimit <= MaxHeatSetpointLimit <= AbsMaxHeatSetpointLimit
|
|
790
|
+
* * AbsMinCoolSetpointLimit <= MinCoolSetpointLimit <= MaxCoolSetpointLimit <= AbsMaxCoolSetpointLimit
|
|
791
|
+
* Values not provided are taken from the state
|
|
792
|
+
*/
|
|
793
|
+
#assertUserSetpointLimits(
|
|
794
|
+
scope: "HeatSetpointLimit" | "CoolSetpointLimit",
|
|
795
|
+
details: { absMin?: number; min?: number; max?: number; absMax?: number } = {},
|
|
796
|
+
) {
|
|
797
|
+
const defaults = scope === "HeatSetpointLimit" ? this.#heatDefaults : this.#coolDefaults;
|
|
798
|
+
const {
|
|
799
|
+
absMin = this.state[`absMin${scope}`] ?? defaults.absMin,
|
|
800
|
+
min = this.state[`min${scope}`] ?? defaults.absMin,
|
|
801
|
+
max = this.state[`max${scope}`] ?? defaults.absMax,
|
|
802
|
+
absMax = this.state[`absMax${scope}`] ?? defaults.absMax,
|
|
803
|
+
} = details;
|
|
804
|
+
logger.debug(
|
|
805
|
+
`Validating user setpoint limits for ${scope}: absMin=${absMin}, min=${min}, max=${max}, absMax=${absMax}`,
|
|
806
|
+
);
|
|
807
|
+
if (absMin > min) {
|
|
808
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
809
|
+
`absMin${scope} (${absMin}) must be less than or equal to min${scope} (${min})`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
if (min > max) {
|
|
813
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
814
|
+
`min${scope} (${min}) must be less than or equal to max${scope} (${max})`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (max > absMax) {
|
|
818
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
819
|
+
`max${scope} (${max}) must be less than or equal to absMax${scope} (${absMax})`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
get heatSetpointMinimum() {
|
|
825
|
+
const absMin = this.state.absMinHeatSetpointLimit ?? this.#heatDefaults.absMin;
|
|
826
|
+
const min = this.state.minHeatSetpointLimit ?? this.#heatDefaults.absMin;
|
|
827
|
+
return Math.max(min, absMin);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
get heatSetpointMaximum() {
|
|
831
|
+
const absMax = this.state.absMaxHeatSetpointLimit ?? this.#heatDefaults.absMax;
|
|
832
|
+
const max = this.state.maxHeatSetpointLimit ?? this.#heatDefaults.absMax;
|
|
833
|
+
return Math.min(max, absMax);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
get coolSetpointMinimum() {
|
|
837
|
+
const absMin = this.state.absMinCoolSetpointLimit ?? this.#coolDefaults.absMin;
|
|
838
|
+
const min = this.state.minCoolSetpointLimit ?? this.#coolDefaults.absMin;
|
|
839
|
+
return Math.max(min, absMin);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
get coolSetpointMaximum() {
|
|
843
|
+
const absMax = this.state.absMaxCoolSetpointLimit ?? this.#coolDefaults.absMax;
|
|
844
|
+
const max = this.state.maxCoolSetpointLimit ?? this.#coolDefaults.absMax;
|
|
845
|
+
return Math.min(max, absMax);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
get setpointDeadBand() {
|
|
849
|
+
return this.features.autoMode ? this.internal.minSetpointDeadBand * 10 : 0;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
#clampSetpointToLimits(scope: "Heat" | "Cool", setpoint: number): number {
|
|
853
|
+
const limitMin = scope === "Heat" ? this.heatSetpointMinimum : this.coolSetpointMinimum;
|
|
854
|
+
const limitMax = scope === "Heat" ? this.heatSetpointMaximum : this.coolSetpointMaximum;
|
|
855
|
+
const result = cropValueRange(setpoint, limitMin, limitMax);
|
|
856
|
+
if (result !== setpoint) {
|
|
857
|
+
logger.debug(
|
|
858
|
+
`${scope} setpoint (${setpoint}) is out of limits [${limitMin}, ${limitMax}], clamping to ${result}`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
return result;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Used to validate that Setpoints must be within user configurable limits
|
|
866
|
+
*/
|
|
867
|
+
#assertSetpointWithinLimits(scope: "Heat" | "Cool", type: "Occupied" | "Unoccupied", setpoint: number) {
|
|
868
|
+
const limitMin = scope === "Heat" ? this.heatSetpointMinimum : this.coolSetpointMinimum;
|
|
869
|
+
const limitMax = scope === "Heat" ? this.heatSetpointMaximum : this.coolSetpointMaximum;
|
|
870
|
+
if (limitMin !== undefined && setpoint < limitMin) {
|
|
871
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
872
|
+
`${scope}${type}Setpoint (${setpoint}) must be greater than or equal to min${scope}SetpointLimit (${limitMin})`,
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
if (limitMax !== undefined && setpoint > limitMax) {
|
|
876
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
877
|
+
`${scope}${type}Setpoint (${setpoint}) must be less than or equal to max${scope}SetpointLimit (${limitMax})`,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Attempts to ensure that a change to the heating/cooling setpoint maintains the deadband with the cooling/heating
|
|
884
|
+
* setpoint by adjusting the cooling setpoint
|
|
885
|
+
*/
|
|
886
|
+
#ensureSetpointDeadband(scope: "Heating" | "Cooling", type: "occupied" | "unoccupied", value: number) {
|
|
887
|
+
if (!this.features.autoMode) {
|
|
888
|
+
// Only validated when AutoMode feature is enabled
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const otherType = scope === "Heating" ? "Cooling" : "Heating";
|
|
893
|
+
const deadband = this.setpointDeadBand;
|
|
894
|
+
const otherSetpoint = otherType === "Heating" ? this.heatingSetpoint : this.coolingSetpoint; // current
|
|
895
|
+
const otherLimit = otherType === "Heating" ? this.heatSetpointMinimum : this.coolSetpointMaximum;
|
|
896
|
+
if (otherType === "Cooling") {
|
|
897
|
+
const minValidSetpoint = value + deadband;
|
|
898
|
+
logger.debug(
|
|
899
|
+
`Ensuring deadband for ${type}${otherType}Setpoint, min valid setpoint is ${minValidSetpoint}`,
|
|
900
|
+
);
|
|
901
|
+
if (otherSetpoint >= minValidSetpoint) {
|
|
902
|
+
// The current cooling setpoint doesn't violate the deadband
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (minValidSetpoint > otherLimit) {
|
|
906
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
907
|
+
`Cannot adjust cooling setpoint to maintain deadband, would exceed max cooling setpoint (${otherLimit})`,
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
logger.debug(`Adjusting ${type}${otherType}Setpoint to ${minValidSetpoint} to maintain deadband`);
|
|
911
|
+
this.state[`${type}${otherType}Setpoint`] = minValidSetpoint;
|
|
912
|
+
} else {
|
|
913
|
+
const maxValidSetpoint = value - deadband;
|
|
914
|
+
logger.debug(
|
|
915
|
+
`Ensuring deadband for ${type}${otherType}Setpoint, max valid setpoint is ${maxValidSetpoint}`,
|
|
916
|
+
);
|
|
917
|
+
if (otherSetpoint <= maxValidSetpoint) {
|
|
918
|
+
// The current heating setpoint doesn't violate the deadband
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (maxValidSetpoint < otherLimit) {
|
|
922
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
923
|
+
`Cannot adjust heating setpoint to maintain deadband, would exceed min heating setpoint (${otherLimit})`,
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
logger.debug(`Adjusting ${type}${otherType}Setpoint to ${maxValidSetpoint} to maintain deadband`);
|
|
927
|
+
this.state[`${type}${otherType}Setpoint`] = maxValidSetpoint;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Checks to see if it's possible to adjust the heating/cooling setpoint to preserve a given deadband if the
|
|
933
|
+
* cooling/heating setpoint is changed
|
|
934
|
+
*/
|
|
935
|
+
#assertSetpointDeadband(type: "Heating" | "Cooling", value: number) {
|
|
936
|
+
if (!this.features.autoMode) {
|
|
937
|
+
// Only validated when AutoMode feature is enabled
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const deadband = this.setpointDeadBand;
|
|
942
|
+
const otherValue = type === "Heating" ? this.coolSetpointMaximum : this.heatSetpointMinimum;
|
|
943
|
+
|
|
944
|
+
// No error is reported but the value is adjusted accordingly.
|
|
945
|
+
if (type === "Heating" && value + deadband > otherValue) {
|
|
946
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
947
|
+
`HeatingSetpoint (${value}) plus deadband (${deadband}) exceeds CoolingSetpoint (${otherValue})`,
|
|
948
|
+
);
|
|
949
|
+
} else if (type === "Cooling" && value - deadband < otherValue) {
|
|
950
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
951
|
+
`CoolingSetpoint (${value}) minus deadband (${deadband}) is less than HeatingSetpoint (${otherValue})`,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
#setupModeHandling() {
|
|
957
|
+
this.reactTo(this.events.systemMode$Changed, this.#handleSystemModeChange);
|
|
958
|
+
this.maybeReactTo(this.events.thermostatRunningMode$Changed, this.#handleThermostatRunningModeChange);
|
|
959
|
+
if (this.state.useAutomaticModeManagement && this.state.thermostatRunningMode !== undefined) {
|
|
960
|
+
this.reactTo(this.events.calibratedTemperature$Changed, this.#handleTemperatureChangeForMode);
|
|
961
|
+
this.#handleTemperatureChangeForMode(this.internal.localTemperature); // initialize
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
#handleSystemModeChange(newMode: Thermostat.SystemMode) {
|
|
966
|
+
if (this.state.thermostatRunningMode !== undefined && newMode !== Thermostat.SystemMode.Auto) {
|
|
967
|
+
if (newMode === Thermostat.SystemMode.Off) {
|
|
968
|
+
this.state.thermostatRunningMode = Thermostat.ThermostatRunningMode.Off;
|
|
969
|
+
} else if (newMode === Thermostat.SystemMode.Heat) {
|
|
970
|
+
this.state.thermostatRunningMode = Thermostat.ThermostatRunningMode.Heat;
|
|
971
|
+
} else if (newMode === Thermostat.SystemMode.Cool) {
|
|
972
|
+
this.state.thermostatRunningMode = Thermostat.ThermostatRunningMode.Cool;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
#handleThermostatRunningModeChange(newRunningMode: Thermostat.ThermostatRunningMode) {
|
|
978
|
+
if (this.state.piCoolingDemand !== undefined) {
|
|
979
|
+
if (
|
|
980
|
+
newRunningMode === Thermostat.ThermostatRunningMode.Off ||
|
|
981
|
+
newRunningMode === Thermostat.ThermostatRunningMode.Heat
|
|
982
|
+
) {
|
|
983
|
+
this.state.piCoolingDemand = 0;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (this.state.piHeatingDemand !== undefined) {
|
|
987
|
+
if (
|
|
988
|
+
newRunningMode === Thermostat.ThermostatRunningMode.Off ||
|
|
989
|
+
newRunningMode === Thermostat.ThermostatRunningMode.Cool
|
|
990
|
+
) {
|
|
991
|
+
this.state.piHeatingDemand = 0;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Handles temperature changes to automatically adjust the system mode based on the current temperature
|
|
998
|
+
* consideration. This logic is disabled by default and will be enabled by setting useAutomaticModeManagement to
|
|
999
|
+
* true.
|
|
1000
|
+
*/
|
|
1001
|
+
#handleTemperatureChangeForMode(temperature: number | null) {
|
|
1002
|
+
if (temperature == null) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const consideration = this.temperatureConsideration;
|
|
1006
|
+
switch (this.state.systemMode) {
|
|
1007
|
+
case Thermostat.SystemMode.Heat:
|
|
1008
|
+
switch (consideration) {
|
|
1009
|
+
case "belowTarget":
|
|
1010
|
+
this.adjustRunningMode(Thermostat.ThermostatRunningMode.Heat);
|
|
1011
|
+
break;
|
|
1012
|
+
default:
|
|
1013
|
+
this.adjustRunningMode(Thermostat.ThermostatRunningMode.Off);
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
break;
|
|
1017
|
+
|
|
1018
|
+
case Thermostat.SystemMode.Cool:
|
|
1019
|
+
switch (consideration) {
|
|
1020
|
+
case "aboveTarget":
|
|
1021
|
+
this.adjustRunningMode(Thermostat.ThermostatRunningMode.Cool);
|
|
1022
|
+
break;
|
|
1023
|
+
default:
|
|
1024
|
+
this.adjustRunningMode(Thermostat.ThermostatRunningMode.Off);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
break;
|
|
1028
|
+
|
|
1029
|
+
case Thermostat.SystemMode.Auto:
|
|
1030
|
+
switch (consideration) {
|
|
1031
|
+
case "belowTarget":
|
|
1032
|
+
this.adjustRunningMode(Thermostat.ThermostatRunningMode.Heat);
|
|
1033
|
+
break;
|
|
1034
|
+
case "aboveTarget":
|
|
1035
|
+
this.adjustRunningMode(Thermostat.ThermostatRunningMode.Cool);
|
|
1036
|
+
break;
|
|
1037
|
+
default:
|
|
1038
|
+
this.adjustRunningMode(Thermostat.ThermostatRunningMode.Off);
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
#setupPresets() {
|
|
1046
|
+
if (!this.features.presets) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
this.reactTo(this.events.presets$AtomicChanging, this.#handlePresetsChanging);
|
|
1050
|
+
this.reactTo(this.events.presets$AtomicChanged, this.#handlePresetsChanged);
|
|
1051
|
+
this.reactTo(this.events.persistedPresets$Changing, this.#handlePresetsChanging);
|
|
1052
|
+
this.reactTo(this.events.persistedPresets$Changed, this.#handlePersistedPresetsChanged);
|
|
1053
|
+
|
|
1054
|
+
this.reactTo(this.events.updatePresets, this.#updatePresets, { lock: true });
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** Handles changes to the Presets attribute and ensures persistedPresets are updated accordingly */
|
|
1058
|
+
#updatePresets(newPresets: Thermostat.Preset[]) {
|
|
1059
|
+
this.state.persistedPresets = newPresets;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Handles "In-flight" validation of newly written Presets via atomic-write and does the required validations.
|
|
1064
|
+
*/
|
|
1065
|
+
#handlePresetsChanging(newPresets: Thermostat.Preset[], oldPresets: Thermostat.Preset[]) {
|
|
1066
|
+
if (newPresets.length > this.state.numberOfPresets) {
|
|
1067
|
+
throw new StatusResponse.ResourceExhaustedError(
|
|
1068
|
+
`Number of presets (${newPresets.length}) exceeds NumberOfPresets (${this.state.numberOfPresets})`,
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const oldPresetsMap = new Map<string, Thermostat.Preset>();
|
|
1073
|
+
if (oldPresets !== undefined) {
|
|
1074
|
+
for (const preset of oldPresets) {
|
|
1075
|
+
if (preset.presetHandle !== null) {
|
|
1076
|
+
const presetHex = Bytes.toHex(preset.presetHandle);
|
|
1077
|
+
oldPresetsMap.set(presetHex, preset);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const persistedPresetsMap = new Map<string, Thermostat.Preset>();
|
|
1083
|
+
if (this.state.persistedPresets !== undefined) {
|
|
1084
|
+
for (const preset of this.state.persistedPresets) {
|
|
1085
|
+
if (preset.presetHandle === null) {
|
|
1086
|
+
throw new InternalError("Persisted preset is missing presetHandle, this should not happen");
|
|
1087
|
+
}
|
|
1088
|
+
const presetHex = Bytes.toHex(preset.presetHandle);
|
|
1089
|
+
persistedPresetsMap.set(presetHex, preset);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const presetTypeMap = new Map<Thermostat.PresetScenario, Thermostat.PresetType>();
|
|
1094
|
+
for (const type of this.state.presetTypes) {
|
|
1095
|
+
presetTypeMap.set(type.presetScenario, type);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const presetScenarioNames = new Map<Thermostat.PresetScenario, (string | null)[]>();
|
|
1099
|
+
const presetScenarioCounts = new Map<Thermostat.PresetScenario, number>();
|
|
1100
|
+
const newPresetsSet = new Set<string>();
|
|
1101
|
+
const newBuildInPresets = new Set<string>();
|
|
1102
|
+
for (const preset of newPresets) {
|
|
1103
|
+
if (preset.presetHandle !== null) {
|
|
1104
|
+
const presetHex = Bytes.toHex(preset.presetHandle);
|
|
1105
|
+
if (newPresetsSet.has(presetHex)) {
|
|
1106
|
+
throw new StatusResponse.ConstraintErrorError(`Duplicate presetHandle ${presetHex} in new Presets`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (this.state.persistedPresets !== undefined) {
|
|
1110
|
+
const persistedPreset = persistedPresetsMap.get(presetHex);
|
|
1111
|
+
if (persistedPreset === undefined) {
|
|
1112
|
+
throw new StatusResponse.NotFoundError(
|
|
1113
|
+
`Preset with presetHandle ${presetHex} does not exist in old Presets, cannot add new Presets with non-null presetHandle`,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
if (preset.builtIn !== null && persistedPreset.builtIn !== preset.builtIn) {
|
|
1117
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1118
|
+
`Cannot change built-in status of preset with presetHandle ${presetHex}`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
newPresetsSet.add(presetHex);
|
|
1124
|
+
} else if (preset.builtIn) {
|
|
1125
|
+
throw new StatusResponse.ConstraintErrorError(`Can not add a new built-in preset`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const presetType = presetTypeMap.get(preset.presetScenario);
|
|
1129
|
+
if (presetType === undefined) {
|
|
1130
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1131
|
+
`No PresetType defined for scenario ${Thermostat.PresetScenario[preset.presetScenario]}`,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (preset.name !== undefined) {
|
|
1136
|
+
const scenarioNames = presetScenarioNames.get(preset.presetScenario) ?? [];
|
|
1137
|
+
if (scenarioNames.includes(preset.name)) {
|
|
1138
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1139
|
+
`Duplicate preset name "${preset.name}" for scenario ${Thermostat.PresetScenario[preset.presetScenario]}`,
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (!presetType.presetTypeFeatures.supportsNames) {
|
|
1144
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1145
|
+
`Preset names are not supported for scenario ${Thermostat.PresetScenario[preset.presetScenario]}`,
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
scenarioNames.push(preset.name);
|
|
1150
|
+
presetScenarioNames.set(preset.presetScenario, scenarioNames);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const count = presetScenarioCounts.get(preset.presetScenario) ?? 0;
|
|
1154
|
+
if (count === presetType.numberOfPresets) {
|
|
1155
|
+
throw new StatusResponse.ResourceExhaustedError(
|
|
1156
|
+
`Number of presets (${count}) for scenario ${Thermostat.PresetScenario[preset.presetScenario]} exceeds allowed number (${presetType.numberOfPresets})`,
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
presetScenarioCounts.set(preset.presetScenario, count + 1);
|
|
1160
|
+
|
|
1161
|
+
if (this.features.cooling) {
|
|
1162
|
+
if (preset.coolingSetpoint === undefined) {
|
|
1163
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1164
|
+
`Preset for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is missing required coolingSetpoint`,
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
if (
|
|
1168
|
+
preset.coolingSetpoint < this.coolSetpointMinimum ||
|
|
1169
|
+
preset.coolingSetpoint > this.coolSetpointMaximum
|
|
1170
|
+
) {
|
|
1171
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1172
|
+
`Preset coolingSetpoint (${preset.coolingSetpoint}) for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is out of bounds [${this.coolSetpointMinimum}, ${this.coolSetpointMaximum}]`,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (this.features.heating) {
|
|
1177
|
+
if (preset.heatingSetpoint === undefined) {
|
|
1178
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1179
|
+
`Preset for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is missing required heatingSetpoint`,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
if (
|
|
1183
|
+
preset.heatingSetpoint < this.heatSetpointMinimum ||
|
|
1184
|
+
preset.heatingSetpoint > this.heatSetpointMaximum
|
|
1185
|
+
) {
|
|
1186
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1187
|
+
`Preset heatingSetpoint (${preset.heatingSetpoint}) for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is out of bounds [${this.heatSetpointMinimum}, ${this.heatSetpointMaximum}]`,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (preset.builtIn && preset.presetHandle !== null) {
|
|
1192
|
+
newBuildInPresets.add(Bytes.toHex(preset.presetHandle));
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Handles additional validation of preset changes when all chunks were written in an atomic write operation.
|
|
1199
|
+
*/
|
|
1200
|
+
#handlePresetsChanged(newPresets: Thermostat.Preset[], oldPresets: Thermostat.Preset[]) {
|
|
1201
|
+
this.#handlePersistedPresetsChanged(newPresets, oldPresets);
|
|
1202
|
+
|
|
1203
|
+
// Store old Presets for lookup convenience
|
|
1204
|
+
const oldPresetsMap = new Map<string, Thermostat.Preset>();
|
|
1205
|
+
const oldBuildInPresets = new Set<string>();
|
|
1206
|
+
if (oldPresets !== undefined) {
|
|
1207
|
+
for (const preset of oldPresets) {
|
|
1208
|
+
if (preset.presetHandle === null) {
|
|
1209
|
+
throw new InternalError("Old preset is missing presetHandle, this must not happen");
|
|
1210
|
+
}
|
|
1211
|
+
const presetHex = Bytes.toHex(preset.presetHandle);
|
|
1212
|
+
oldPresetsMap.set(presetHex, preset);
|
|
1213
|
+
if (preset.builtIn) {
|
|
1214
|
+
oldBuildInPresets.add(presetHex);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
for (const preset of newPresets) {
|
|
1220
|
+
if (preset.presetHandle === null) {
|
|
1221
|
+
if (preset.builtIn) {
|
|
1222
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1223
|
+
`Preset for scenario ${Thermostat.PresetScenario[preset.presetScenario]} is built-in and must have a non-null presetHandle`,
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Handles additional validation and required value adjustments of persistedPresets changes when all chunks were
|
|
1232
|
+
* written in an atomic write.
|
|
1233
|
+
*/
|
|
1234
|
+
#handlePersistedPresetsChanged(newPresets: Thermostat.Preset[], oldPresets: Thermostat.Preset[]) {
|
|
1235
|
+
if (oldPresets === undefined) {
|
|
1236
|
+
logger.debug(
|
|
1237
|
+
"Old presets is undefined, skipping some checks. This should only happen on setup of the behavior.",
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const entropy = this.endpoint.env.get(Entropy);
|
|
1242
|
+
let changed = false;
|
|
1243
|
+
const newPresetHandles = new Set<string>();
|
|
1244
|
+
for (const preset of newPresets) {
|
|
1245
|
+
if (preset.presetHandle === null) {
|
|
1246
|
+
logger.error("Preset is missing presetHandle, generating a new one");
|
|
1247
|
+
preset.presetHandle = entropy.randomBytes(16);
|
|
1248
|
+
changed = true;
|
|
1249
|
+
}
|
|
1250
|
+
newPresetHandles.add(Bytes.toHex(preset.presetHandle));
|
|
1251
|
+
if (oldPresets === undefined) {
|
|
1252
|
+
if (preset.builtIn === null) {
|
|
1253
|
+
preset.builtIn = false;
|
|
1254
|
+
changed = true;
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
if (preset.builtIn === null) {
|
|
1258
|
+
const oldPreset = oldPresets.find(
|
|
1259
|
+
p =>
|
|
1260
|
+
p.presetHandle &&
|
|
1261
|
+
preset.presetHandle &&
|
|
1262
|
+
Bytes.areEqual(p.presetHandle, preset.presetHandle),
|
|
1263
|
+
);
|
|
1264
|
+
if (oldPreset !== undefined) {
|
|
1265
|
+
preset.builtIn = oldPreset.builtIn;
|
|
1266
|
+
} else {
|
|
1267
|
+
preset.builtIn = false;
|
|
1268
|
+
}
|
|
1269
|
+
changed = true;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const newBuildInPresets = new Set<string>();
|
|
1275
|
+
for (const preset of newPresets) {
|
|
1276
|
+
if (preset.builtIn) {
|
|
1277
|
+
newBuildInPresets.add(Bytes.toHex(preset.presetHandle!));
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const oldBuildInPresets = new Set<string>();
|
|
1281
|
+
if (oldPresets !== undefined) {
|
|
1282
|
+
for (const preset of oldPresets) {
|
|
1283
|
+
if (preset.builtIn) {
|
|
1284
|
+
oldBuildInPresets.add(Bytes.toHex(preset.presetHandle!));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Ensure built-in presets are not removed
|
|
1290
|
+
for (const oldBuiltInPreset of oldBuildInPresets) {
|
|
1291
|
+
if (!newBuildInPresets.has(oldBuiltInPreset)) {
|
|
1292
|
+
throw new StatusResponse.ConstraintErrorError(
|
|
1293
|
+
`Cannot remove built-in preset with presetHandle ${oldBuiltInPreset}`,
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/*if (this.features.matterScheduleConfiguration) {
|
|
1299
|
+
for (const schedule of this.state.schedules) {
|
|
1300
|
+
if (schedule.presetHandle && !newPresetHandles.has(Bytes.toHex(schedule.presetHandle))) {
|
|
1301
|
+
throw new StatusResponse.InvalidInStateError(`Schedule references non-existing presetHandle`);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}*/
|
|
1305
|
+
if (
|
|
1306
|
+
this.state.activePresetHandle !== null &&
|
|
1307
|
+
!newPresetHandles.has(Bytes.toHex(this.state.activePresetHandle))
|
|
1308
|
+
) {
|
|
1309
|
+
throw new StatusResponse.InvalidInStateError(`ActivePresetHandle references non-existing presetHandle`);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (changed) {
|
|
1313
|
+
logger.error("PresetHandles or BuiltIn flags were updated, updating persistedPresets");
|
|
1314
|
+
this.state.persistedPresets = deepCopy(newPresets);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
override async [Symbol.asyncDispose]() {
|
|
1319
|
+
// Because we are basically the only user right now ensure the service is closed when we are disposed
|
|
1320
|
+
this.endpoint.env.close(AtomicWriteHandler);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** Implementation of the atomic request handling */
|
|
1324
|
+
override async atomicRequest(request: Thermostat.AtomicRequest): Promise<Thermostat.AtomicResponse> {
|
|
1325
|
+
const atomicWriteHandler = this.endpoint.env.get(AtomicWriteHandler);
|
|
1326
|
+
const { requestType } = request;
|
|
1327
|
+
switch (requestType) {
|
|
1328
|
+
case Thermostat.RequestType.BeginWrite:
|
|
1329
|
+
return atomicWriteHandler.beginWrite(request, this.context, this.endpoint, this.type);
|
|
1330
|
+
case Thermostat.RequestType.CommitWrite:
|
|
1331
|
+
return await atomicWriteHandler.commitWrite(
|
|
1332
|
+
request,
|
|
1333
|
+
this.context,
|
|
1334
|
+
this.endpoint,
|
|
1335
|
+
this.type,
|
|
1336
|
+
this.state,
|
|
1337
|
+
);
|
|
1338
|
+
case Thermostat.RequestType.RollbackWrite:
|
|
1339
|
+
return atomicWriteHandler.rollbackWrite(request, this.context, this.endpoint, this.type);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export namespace ThermostatBaseServer {
|
|
1345
|
+
export class State extends ThermostatBehaviorLogicBase.State {
|
|
1346
|
+
/**
|
|
1347
|
+
* Otherwise measured temperature in Matter format as uint16 with a factor of 100. A calibration offset is applied
|
|
1348
|
+
* additionally from localTemperatureCalibration if set.
|
|
1349
|
+
* Use this if you have an external temperature sensor that should be used for thermostat control instead of a
|
|
1350
|
+
* local temperature measurement cluster.
|
|
1351
|
+
*/
|
|
1352
|
+
externalMeasuredIndoorTemperature?: number;
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Otherwise measured occupancy as boolean.
|
|
1356
|
+
* Use this if you have an external occupancy sensor that should be used for thermostat control instead of a
|
|
1357
|
+
* internal occupancy sensing cluster.
|
|
1358
|
+
*/
|
|
1359
|
+
externallyMeasuredOccupancy?: boolean;
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Use to enable the automatic mode management, implemented by this standard implementation. This is beyond
|
|
1363
|
+
* Matter specification! It reacts to temperature changes to adjust system running mode automatically. It also
|
|
1364
|
+
* requires the Auto feature to be enabled and the ThermostatRunningMode attribute to be present.
|
|
1365
|
+
*/
|
|
1366
|
+
useAutomaticModeManagement: boolean = false;
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Persisted presets stored in the device, needed because the original "presets" is a virtual property
|
|
1370
|
+
*/
|
|
1371
|
+
persistedPresets?: Thermostat.Preset[];
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Implementation of the needed Preset attribute logic for Atomic Write handling.
|
|
1375
|
+
*/
|
|
1376
|
+
[Val.properties](endpoint: Endpoint, session: ValueSupervisor.Session) {
|
|
1377
|
+
// Only return remaining time if the attribute is defined in the endpoint
|
|
1378
|
+
const properties = {};
|
|
1379
|
+
if (
|
|
1380
|
+
(endpoint.behaviors.optionsFor(ThermostatBaseServer) as Record<string, unknown>)?.presets !==
|
|
1381
|
+
undefined ||
|
|
1382
|
+
(endpoint.behaviors.defaultsFor(ThermostatBaseServer) as Record<string, unknown>)?.presets !== undefined
|
|
1383
|
+
) {
|
|
1384
|
+
Object.defineProperty(properties, "presets", {
|
|
1385
|
+
/**
|
|
1386
|
+
* Getter will return a pending atomic write state when there is one, otherwise the stored value or
|
|
1387
|
+
* the default value.
|
|
1388
|
+
*/
|
|
1389
|
+
get(): Readonly<Thermostat.Preset[]> {
|
|
1390
|
+
// When we have a pending value for this attribute, return that instead
|
|
1391
|
+
const pendingValue = endpoint.env
|
|
1392
|
+
.get(AtomicWriteHandler)
|
|
1393
|
+
.pendingValueForAttributeAndPeer(
|
|
1394
|
+
session,
|
|
1395
|
+
endpoint,
|
|
1396
|
+
ThermostatBaseServer,
|
|
1397
|
+
Thermostat.Complete.attributes.presets.id,
|
|
1398
|
+
);
|
|
1399
|
+
if (pendingValue !== undefined) {
|
|
1400
|
+
return pendingValue as Thermostat.Preset[];
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
let value = endpoint.stateOf(ThermostatBaseServer.id).persistedPresets;
|
|
1404
|
+
if (value === undefined) {
|
|
1405
|
+
value = (endpoint.behaviors.optionsFor(ThermostatBaseServer) as Record<string, unknown>)
|
|
1406
|
+
?.presets;
|
|
1407
|
+
}
|
|
1408
|
+
return (value ?? []) as Thermostat.Preset[];
|
|
1409
|
+
},
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Setter will either emit an update event directly when in local actor context or command context,
|
|
1413
|
+
* otherwise it will go through the AtomicWriteHandler to ensure proper atomic write handling.
|
|
1414
|
+
*/
|
|
1415
|
+
set(value: Thermostat.Preset[]) {
|
|
1416
|
+
if (hasLocalActor(session) || ("command" in session && session.command)) {
|
|
1417
|
+
// Local set or command context bypass atomic write handling
|
|
1418
|
+
// We use this event to property apply state changes
|
|
1419
|
+
endpoint.eventsOf(ThermostatBaseServer.id).updatePresets!.emit(value);
|
|
1420
|
+
} else {
|
|
1421
|
+
endpoint.env
|
|
1422
|
+
.get(AtomicWriteHandler)
|
|
1423
|
+
.writeAttribute(
|
|
1424
|
+
session,
|
|
1425
|
+
endpoint,
|
|
1426
|
+
ThermostatBaseServer,
|
|
1427
|
+
Thermostat.Complete.attributes.presets.id,
|
|
1428
|
+
value,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return properties;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
export class Events extends ThermostatBehaviorLogicBase.Events {
|
|
1440
|
+
externalMeasuredIndoorTemperature$Changed =
|
|
1441
|
+
Observable<[value: number, oldValue: number, context: ActionContext]>();
|
|
1442
|
+
externallyMeasuredOccupancy$Changed = Observable<[value: boolean, oldValue: boolean, context: ActionContext]>();
|
|
1443
|
+
persistedPresets$Changed =
|
|
1444
|
+
Observable<[value: Thermostat.Preset[], oldValue: Thermostat.Preset[], context: ActionContext]>();
|
|
1445
|
+
persistedPresets$Changing =
|
|
1446
|
+
Observable<[value: Thermostat.Preset[], oldValue: Thermostat.Preset[], context: ActionContext]>();
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Custom event emitted when the calibrated temperature changes.
|
|
1450
|
+
*/
|
|
1451
|
+
calibratedTemperature$Changed =
|
|
1452
|
+
Observable<[value: number | null, oldValue: number | null, context: ActionContext]>();
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Custom event emitted when the Presets attribute is "virtually" changing as part of an atomic write operation.
|
|
1456
|
+
* Info: The events is currently needed to be a pure Observable to get errors thrown in the event handler be
|
|
1457
|
+
* reported back to the emitter.
|
|
1458
|
+
*/
|
|
1459
|
+
presets$AtomicChanging =
|
|
1460
|
+
Observable<[value: Thermostat.Preset[], oldValue: Thermostat.Preset[], context: ActionContext]>();
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Custom event emitted when the Presets attribute has "virtually" changed as part of an atomic write operation.
|
|
1464
|
+
* Info: The events is currently needed to be a pure Observable to get errors thrown in the event handler be
|
|
1465
|
+
* reported back to the emitter.
|
|
1466
|
+
*/
|
|
1467
|
+
presets$AtomicChanged =
|
|
1468
|
+
Observable<[value: Thermostat.Preset[], oldValue: Thermostat.Preset[], context: ActionContext]>();
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Custom event emitted to inform the behavior implementation of an update of the PersistedPresets attribute.
|
|
1472
|
+
*/
|
|
1473
|
+
updatePresets = Observable<[value: Thermostat.Preset[]]>();
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
export class Internal {
|
|
1477
|
+
/**
|
|
1478
|
+
* Local temperature in Matter format as uint16 with a factor of 100. It is the same value as the one reported
|
|
1479
|
+
* in the localTemperature Attribute, but also present when the LocalTemperatureNotExposed feature is enabled.
|
|
1480
|
+
* Means all logic and calculations are always done with this value.
|
|
1481
|
+
* The value will be updated on initialization and when the localTemperature Attribute changes.
|
|
1482
|
+
*/
|
|
1483
|
+
localTemperature: number | null = null;
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Storing fixed value internally to ensure it can not be modified.
|
|
1487
|
+
* This value will be initialized when the behavior is initialized and is static afterward.
|
|
1488
|
+
*/
|
|
1489
|
+
minSetpointDeadBand: number = 0;
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Storing fixed value internally to ensure it can not be modified.
|
|
1493
|
+
* This value will be initialized when the behavior is initialized and is static afterward.
|
|
1494
|
+
*/
|
|
1495
|
+
controlSequenceOfOperation!: Thermostat.ControlSequenceOfOperation;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// We had turned on some more features to provide a default implementation, but export the cluster with default
|
|
1500
|
+
// Features again.
|
|
1501
|
+
export class ThermostatServer extends ThermostatBaseServer.for(ClusterType(Thermostat.Base)) {}
|