@bldgblocks/node-red-contrib-control 0.1.37 → 0.2.0
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/LICENSE.md +38 -5
- package/README.md +10 -0
- package/nodes/accumulate-block.html +3 -3
- package/nodes/alarm-collector.html +11 -0
- package/nodes/alarm-collector.js +31 -5
- package/nodes/alarm-config.html +11 -6
- package/nodes/alarm-config.js +34 -35
- package/nodes/alarm-service.js +2 -2
- package/nodes/and-block.js +1 -1
- package/nodes/boolean-switch-block.html +27 -14
- package/nodes/boolean-switch-block.js +22 -12
- package/nodes/call-status-block.html +83 -56
- package/nodes/call-status-block.js +335 -248
- package/nodes/changeover-block.html +30 -31
- package/nodes/changeover-block.js +287 -389
- package/nodes/contextual-label-block.js +3 -3
- package/nodes/delay-block.js +74 -13
- package/nodes/global-getter.html +173 -12
- package/nodes/global-getter.js +29 -14
- package/nodes/global-setter.html +37 -0
- package/nodes/global-setter.js +96 -14
- package/nodes/history-buffer.js +32 -27
- package/nodes/history-collector.html +3 -1
- package/nodes/history-collector.js +4 -4
- package/nodes/history-config.html +8 -2
- package/nodes/network-point-read.js +6 -1
- package/nodes/network-point-register.html +1 -1
- package/nodes/network-service-bridge.js +43 -11
- package/nodes/network-service-registry.html +236 -27
- package/nodes/network-service-registry.js +1 -1
- package/nodes/or-block.js +1 -1
- package/nodes/priority-block.js +1 -1
- package/nodes/tstat-block.html +34 -79
- package/nodes/tstat-block.js +223 -345
- package/nodes/utils.js +1 -1
- package/package.json +90 -75
package/nodes/tstat-block.js
CHANGED
|
@@ -1,26 +1,70 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Tstat Block - Thermostat Controller
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Controls heating/cooling calls based on temperature input and setpoints.
|
|
5
|
+
// Works as a companion to changeover-block: changeover selects the mode,
|
|
6
|
+
// tstat generates the actual heating/cooling call signals.
|
|
7
|
+
//
|
|
8
|
+
// Supports three algorithms:
|
|
9
|
+
// - single: one setpoint ± diff/2 defines on/off thresholds
|
|
10
|
+
// - split: separate heating/cooling setpoints with diff hysteresis
|
|
11
|
+
// - specified: explicit on/off temperatures for heating and cooling
|
|
12
|
+
//
|
|
13
|
+
// Outputs (3 ports):
|
|
14
|
+
// 1. isHeating (boolean) - current mode passthrough
|
|
15
|
+
// 2. above (boolean) - cooling call active
|
|
16
|
+
// 3. below (boolean) - heating call active
|
|
17
|
+
//
|
|
18
|
+
// Anticipator adjusts turn-off points to prevent overshoot.
|
|
19
|
+
// ignoreAnticipatorCycles disables anticipator after mode changes.
|
|
20
|
+
// All configuration via typed inputs (editor, msg, flow, global).
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
1
23
|
module.exports = function(RED) {
|
|
2
24
|
const utils = require('./utils')(RED);
|
|
3
|
-
|
|
25
|
+
|
|
26
|
+
const VALID_ALGORITHMS = ["single", "split", "specified"];
|
|
27
|
+
|
|
4
28
|
function TstatBlockNode(config) {
|
|
5
29
|
RED.nodes.createNode(this, config);
|
|
6
30
|
const node = this;
|
|
7
31
|
node.isBusy = false;
|
|
8
32
|
|
|
9
|
-
//
|
|
33
|
+
// ====================================================================
|
|
34
|
+
// Configuration — safe parseFloat that doesn't clobber zero
|
|
35
|
+
// ====================================================================
|
|
36
|
+
const num = (v, fallback) => { const n = parseFloat(v); return isNaN(n) ? fallback : n; };
|
|
37
|
+
|
|
10
38
|
node.name = config.name;
|
|
11
|
-
node.setpoint =
|
|
12
|
-
node.heatingSetpoint =
|
|
13
|
-
node.coolingSetpoint =
|
|
14
|
-
node.coolingOn =
|
|
15
|
-
node.coolingOff =
|
|
16
|
-
node.heatingOff =
|
|
17
|
-
node.heatingOn =
|
|
18
|
-
node.diff =
|
|
19
|
-
node.anticipator =
|
|
20
|
-
node.ignoreAnticipatorCycles = Math.floor(config.ignoreAnticipatorCycles);
|
|
21
|
-
node.isHeating = config.isHeating === true;
|
|
22
|
-
node.algorithm = config.algorithm;
|
|
39
|
+
node.setpoint = num(config.setpoint, 70);
|
|
40
|
+
node.heatingSetpoint = num(config.heatingSetpoint, 68);
|
|
41
|
+
node.coolingSetpoint = num(config.coolingSetpoint, 74);
|
|
42
|
+
node.coolingOn = num(config.coolingOn, 74);
|
|
43
|
+
node.coolingOff = num(config.coolingOff, 72);
|
|
44
|
+
node.heatingOff = num(config.heatingOff, 68);
|
|
45
|
+
node.heatingOn = num(config.heatingOn, 66);
|
|
46
|
+
node.diff = num(config.diff, 2);
|
|
47
|
+
node.anticipator = num(config.anticipator, 0.5);
|
|
48
|
+
node.ignoreAnticipatorCycles = Math.floor(num(config.ignoreAnticipatorCycles, 1));
|
|
49
|
+
node.isHeating = config.isHeating === true || config.isHeating === "true";
|
|
50
|
+
node.algorithm = VALID_ALGORITHMS.includes(config.algorithm) ? config.algorithm : "single";
|
|
51
|
+
|
|
52
|
+
// Startup delay: suppress above/below calls until mode has settled
|
|
53
|
+
node.startupDelay = Math.max(num(config.startupDelay, 30), 0);
|
|
54
|
+
node.startupComplete = node.startupDelay === 0;
|
|
55
|
+
node.startupTimer = null;
|
|
56
|
+
if (!node.startupComplete) {
|
|
57
|
+
utils.setStatusWarn(node, `startup delay: ${node.startupDelay}s`);
|
|
58
|
+
node.startupTimer = setTimeout(() => {
|
|
59
|
+
node.startupComplete = true;
|
|
60
|
+
node.startupTimer = null;
|
|
61
|
+
utils.setStatusOK(node, "startup delay complete");
|
|
62
|
+
}, node.startupDelay * 1000);
|
|
63
|
+
}
|
|
23
64
|
|
|
65
|
+
// ====================================================================
|
|
66
|
+
// Runtime state
|
|
67
|
+
// ====================================================================
|
|
24
68
|
let above = false;
|
|
25
69
|
let below = false;
|
|
26
70
|
let lastAbove = false;
|
|
@@ -29,6 +73,35 @@ module.exports = function(RED) {
|
|
|
29
73
|
let cyclesSinceModeChange = 0;
|
|
30
74
|
let modeChanged = false;
|
|
31
75
|
|
|
76
|
+
// ====================================================================
|
|
77
|
+
// Typed-input evaluation helpers
|
|
78
|
+
// ====================================================================
|
|
79
|
+
function evalNumeric(configValue, configType, fallback, msg) {
|
|
80
|
+
return utils.evaluateNodeProperty(configValue, configType, node, msg)
|
|
81
|
+
.then(val => { const n = parseFloat(val); return isNaN(n) ? fallback : n; })
|
|
82
|
+
.catch(() => fallback);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function evalBool(configValue, configType, fallback, msg) {
|
|
86
|
+
return utils.evaluateNodeProperty(configValue, configType, node, msg)
|
|
87
|
+
.then(val => {
|
|
88
|
+
if (typeof val === "boolean") return val;
|
|
89
|
+
if (val === "true") return true;
|
|
90
|
+
if (val === "false") return false;
|
|
91
|
+
return fallback;
|
|
92
|
+
})
|
|
93
|
+
.catch(() => fallback);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function evalEnum(configValue, configType, allowed, fallback, msg) {
|
|
97
|
+
return utils.evaluateNodeProperty(configValue, configType, node, msg)
|
|
98
|
+
.then(val => allowed.includes(val) ? val : fallback)
|
|
99
|
+
.catch(() => fallback);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ====================================================================
|
|
103
|
+
// Main input handler
|
|
104
|
+
// ====================================================================
|
|
32
105
|
node.on("input", async function(msg, send, done) {
|
|
33
106
|
send = send || function() { node.send.apply(node, arguments); };
|
|
34
107
|
|
|
@@ -38,244 +111,65 @@ module.exports = function(RED) {
|
|
|
38
111
|
return;
|
|
39
112
|
}
|
|
40
113
|
|
|
41
|
-
//
|
|
114
|
+
// ----------------------------------------------------------------
|
|
115
|
+
// 1. Evaluate typed inputs (async phase)
|
|
116
|
+
// ----------------------------------------------------------------
|
|
117
|
+
if (node.isBusy) {
|
|
118
|
+
utils.setStatusBusy(node, "busy - dropped msg");
|
|
119
|
+
if (done) done();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
node.isBusy = true;
|
|
123
|
+
|
|
42
124
|
try {
|
|
125
|
+
const results = await Promise.all([
|
|
126
|
+
evalNumeric(config.setpoint, config.setpointType, node.setpoint, msg), // 0
|
|
127
|
+
evalNumeric(config.heatingSetpoint, config.heatingSetpointType, node.heatingSetpoint, msg), // 1
|
|
128
|
+
evalNumeric(config.coolingSetpoint, config.coolingSetpointType, node.coolingSetpoint, msg), // 2
|
|
129
|
+
evalNumeric(config.coolingOn, config.coolingOnType, node.coolingOn, msg), // 3
|
|
130
|
+
evalNumeric(config.coolingOff, config.coolingOffType, node.coolingOff, msg), // 4
|
|
131
|
+
evalNumeric(config.heatingOff, config.heatingOffType, node.heatingOff, msg), // 5
|
|
132
|
+
evalNumeric(config.heatingOn, config.heatingOnType, node.heatingOn, msg), // 6
|
|
133
|
+
evalNumeric(config.diff, config.diffType, node.diff, msg), // 7
|
|
134
|
+
evalNumeric(config.anticipator, config.anticipatorType, node.anticipator, msg), // 8
|
|
135
|
+
evalNumeric(config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node.ignoreAnticipatorCycles, msg), // 9
|
|
136
|
+
evalBool(config.isHeating, config.isHeatingType, node.isHeating, msg), // 10
|
|
137
|
+
evalEnum(config.algorithm, config.algorithmType, VALID_ALGORITHMS, node.algorithm, msg), // 11
|
|
138
|
+
]);
|
|
43
139
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
140
|
+
node.setpoint = results[0];
|
|
141
|
+
node.heatingSetpoint = results[1];
|
|
142
|
+
node.coolingSetpoint = results[2];
|
|
143
|
+
node.coolingOn = results[3];
|
|
144
|
+
node.coolingOff = results[4];
|
|
145
|
+
node.heatingOff = results[5];
|
|
146
|
+
node.heatingOn = results[6];
|
|
147
|
+
node.diff = results[7];
|
|
148
|
+
node.anticipator = results[8];
|
|
149
|
+
node.ignoreAnticipatorCycles = Math.floor(results[9]);
|
|
150
|
+
node.isHeating = results[10];
|
|
151
|
+
node.algorithm = results[11];
|
|
51
152
|
|
|
52
|
-
// Lock node during evaluation
|
|
53
|
-
node.isBusy = true;
|
|
54
|
-
|
|
55
|
-
// Begin evaluations
|
|
56
|
-
const evaluations = [];
|
|
57
|
-
|
|
58
|
-
//0
|
|
59
|
-
evaluations.push(
|
|
60
|
-
utils.requiresEvaluation(config.setpointType)
|
|
61
|
-
? utils.evaluateNodeProperty(config.setpoint, config.setpointType, node, msg)
|
|
62
|
-
.then(val => parseFloat(val))
|
|
63
|
-
: Promise.resolve(node.setpoint),
|
|
64
|
-
);
|
|
65
|
-
//1
|
|
66
|
-
evaluations.push(
|
|
67
|
-
utils.requiresEvaluation(config.heatingSetpointType)
|
|
68
|
-
? utils.evaluateNodeProperty(config.heatingSetpoint, config.heatingSetpointType, node, msg)
|
|
69
|
-
.then(val => parseFloat(val))
|
|
70
|
-
: Promise.resolve(node.heatingSetpoint),
|
|
71
|
-
);
|
|
72
|
-
//2
|
|
73
|
-
evaluations.push(
|
|
74
|
-
utils.requiresEvaluation(config.coolingSetpointType)
|
|
75
|
-
? utils.evaluateNodeProperty(config.coolingSetpoint, config.coolingSetpointType, node, msg)
|
|
76
|
-
.then(val => parseFloat(val))
|
|
77
|
-
: Promise.resolve(node.coolingSetpoint),
|
|
78
|
-
);
|
|
79
|
-
//3
|
|
80
|
-
evaluations.push(
|
|
81
|
-
utils.requiresEvaluation(config.coolingOnType)
|
|
82
|
-
? utils.evaluateNodeProperty(config.coolingOn, config.coolingOnType, node, msg)
|
|
83
|
-
.then(val => parseFloat(val))
|
|
84
|
-
: Promise.resolve(node.coolingOn),
|
|
85
|
-
);
|
|
86
|
-
//4
|
|
87
|
-
evaluations.push(
|
|
88
|
-
utils.requiresEvaluation(config.coolingOffType)
|
|
89
|
-
? utils.evaluateNodeProperty(config.coolingOff, config.coolingOffType, node, msg)
|
|
90
|
-
.then(val => parseFloat(val))
|
|
91
|
-
: Promise.resolve(node.coolingOff),
|
|
92
|
-
);
|
|
93
|
-
//5
|
|
94
|
-
evaluations.push(
|
|
95
|
-
utils.requiresEvaluation(config.heatingOffType)
|
|
96
|
-
? utils.evaluateNodeProperty(config.heatingOff, config.heatingOffType, node, msg)
|
|
97
|
-
.then(val => parseFloat(val))
|
|
98
|
-
: Promise.resolve(node.heatingOff),
|
|
99
|
-
);
|
|
100
|
-
//6
|
|
101
|
-
evaluations.push(
|
|
102
|
-
utils.requiresEvaluation(config.heatingOnType)
|
|
103
|
-
? utils.evaluateNodeProperty(config.heatingOn, config.heatingOnType, node, msg)
|
|
104
|
-
.then(val => parseFloat(val))
|
|
105
|
-
: Promise.resolve(node.heatingOn),
|
|
106
|
-
);
|
|
107
|
-
//7
|
|
108
|
-
evaluations.push(
|
|
109
|
-
utils.requiresEvaluation(config.diffType)
|
|
110
|
-
? utils.evaluateNodeProperty(config.diff, config.diffType, node, msg)
|
|
111
|
-
.then(val => parseFloat(val))
|
|
112
|
-
: Promise.resolve(node.diff),
|
|
113
|
-
);
|
|
114
|
-
//8
|
|
115
|
-
evaluations.push(
|
|
116
|
-
utils.requiresEvaluation(config.anticipatorType)
|
|
117
|
-
? utils.evaluateNodeProperty(config.anticipator, config.anticipatorType, node, msg)
|
|
118
|
-
.then(val => parseFloat(val))
|
|
119
|
-
: Promise.resolve(node.anticipator),
|
|
120
|
-
);
|
|
121
|
-
//9
|
|
122
|
-
evaluations.push(
|
|
123
|
-
utils.requiresEvaluation(config.ignoreAnticipatorCyclesType)
|
|
124
|
-
? utils.evaluateNodeProperty(config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node, msg)
|
|
125
|
-
.then(val => Math.floor(val))
|
|
126
|
-
: Promise.resolve(node.ignoreAnticipatorCycles),
|
|
127
|
-
);
|
|
128
|
-
//10
|
|
129
|
-
evaluations.push(
|
|
130
|
-
utils.requiresEvaluation(config.isHeatingType)
|
|
131
|
-
? utils.evaluateNodeProperty(config.isHeating, config.isHeatingType, node, msg)
|
|
132
|
-
.then(val => val === true)
|
|
133
|
-
: Promise.resolve(node.isHeating),
|
|
134
|
-
);
|
|
135
|
-
//11
|
|
136
|
-
evaluations.push(
|
|
137
|
-
utils.requiresEvaluation(config.algorithmType)
|
|
138
|
-
? utils.evaluateNodeProperty(config.algorithm, config.algorithmType, node, msg)
|
|
139
|
-
: Promise.resolve(node.algorithm),
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const results = await Promise.all(evaluations);
|
|
143
|
-
|
|
144
|
-
// Update runtime with evaluated values
|
|
145
|
-
if (!isNaN(results[0])) node.setpoint = results[0];
|
|
146
|
-
if (!isNaN(results[1])) node.heatingSetpoint = results[1];
|
|
147
|
-
if (!isNaN(results[2])) node.coolingSetpoint = results[2];
|
|
148
|
-
if (!isNaN(results[3])) node.coolingOn = results[3];
|
|
149
|
-
if (!isNaN(results[4])) node.coolingOff = results[4];
|
|
150
|
-
if (!isNaN(results[5])) node.heatingOff = results[5];
|
|
151
|
-
if (!isNaN(results[6])) node.heatingOn = results[6];
|
|
152
|
-
if (!isNaN(results[7])) node.diff = results[7];
|
|
153
|
-
if (!isNaN(results[8])) node.anticipator = results[8];
|
|
154
|
-
if (!isNaN(results[9])) node.ignoreAnticipatorCycles = results[9];
|
|
155
|
-
if (results[10] !== null) node.isHeating = results[10];
|
|
156
|
-
if (results[11]) node.algorithm = results[11];
|
|
157
153
|
} catch (err) {
|
|
158
154
|
node.error(`Error evaluating properties: ${err.message}`);
|
|
159
155
|
if (done) done();
|
|
160
156
|
return;
|
|
161
157
|
} finally {
|
|
162
|
-
// Release, all synchronous from here on
|
|
163
158
|
node.isBusy = false;
|
|
164
159
|
}
|
|
165
160
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
switch (msg.context) {
|
|
175
|
-
case "algorithm":
|
|
176
|
-
if (["single", "split", "specified"].includes(msg.payload)) {
|
|
177
|
-
node.algorithm = msg.payload;
|
|
178
|
-
utils.setStatusOK(node, `algorithm: ${msg.payload}`);
|
|
179
|
-
} else {
|
|
180
|
-
utils.setStatusError(node, "invalid algorithm");
|
|
181
|
-
}
|
|
182
|
-
break;
|
|
183
|
-
case "setpoint":
|
|
184
|
-
if (typeof msg.payload === 'number') {
|
|
185
|
-
node.setpoint = msg.payload;
|
|
186
|
-
utils.setStatusOK(node, `setpoint: ${msg.payload.toFixed(2)}`);
|
|
187
|
-
} else {
|
|
188
|
-
utils.setStatusError(node, "invalid setpoint");
|
|
189
|
-
}
|
|
190
|
-
break;
|
|
191
|
-
case "heatingSetpoint":
|
|
192
|
-
if (typeof msg.payload === 'number') {
|
|
193
|
-
node.heatingSetpoint = msg.payload;
|
|
194
|
-
utils.setStatusOK(node, `heatingSetpoint: ${msg.payload.toFixed(2)}`);
|
|
195
|
-
} else {
|
|
196
|
-
utils.setStatusError(node, "invalid heatingSetpoint");
|
|
197
|
-
}
|
|
198
|
-
break;
|
|
199
|
-
case "coolingSetpoint":
|
|
200
|
-
if (typeof msg.payload === 'number') {
|
|
201
|
-
node.coolingSetpoint = msg.payload;
|
|
202
|
-
utils.setStatusOK(node, `coolingSetpoint: ${msg.payload.toFixed(2)}`);
|
|
203
|
-
} else {
|
|
204
|
-
utils.setStatusError(node, "invalid coolingSetpoint");
|
|
205
|
-
}
|
|
206
|
-
break;
|
|
207
|
-
case "coolingOn":
|
|
208
|
-
if (typeof msg.payload === 'number') {
|
|
209
|
-
node.coolingOn = msg.payload;
|
|
210
|
-
utils.setStatusOK(node, `coolingOn: ${msg.payload.toFixed(2)}`);
|
|
211
|
-
} else {
|
|
212
|
-
utils.setStatusError(node, "invalid coolingOn");
|
|
213
|
-
}
|
|
214
|
-
break;
|
|
215
|
-
case "coolingOff":
|
|
216
|
-
if (typeof msg.payload === 'number') {
|
|
217
|
-
node.coolingOff = msg.payload;
|
|
218
|
-
utils.setStatusOK(node, `coolingOff: ${msg.payload.toFixed(2)}`);
|
|
219
|
-
} else {
|
|
220
|
-
utils.setStatusError(node, "invalid coolingOff");
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
case "heatingOff":
|
|
224
|
-
if (typeof msg.payload === 'number') {
|
|
225
|
-
node.heatingOff = msg.payload;
|
|
226
|
-
utils.setStatusOK(node, `heatingOff: ${msg.payload.toFixed(2)}`);
|
|
227
|
-
} else {
|
|
228
|
-
utils.setStatusError(node, "invalid heatingOff");
|
|
229
|
-
}
|
|
230
|
-
break;
|
|
231
|
-
case "heatingOn":
|
|
232
|
-
if (typeof msg.payload === 'number') {
|
|
233
|
-
node.heatingOn = msg.payload;
|
|
234
|
-
utils.setStatusOK(node, `heatingOn: ${msg.payload.toFixed(2)}`);
|
|
235
|
-
} else {
|
|
236
|
-
utils.setStatusError(node, "invalid heatingOn");
|
|
237
|
-
}
|
|
238
|
-
break;
|
|
239
|
-
case "diff":
|
|
240
|
-
if (typeof msg.payload === 'number' && msg.payload >= 0.01) {
|
|
241
|
-
node.diff = msg.payload;
|
|
242
|
-
utils.setStatusOK(node, `diff: ${msg.payload.toFixed(2)}`);
|
|
243
|
-
} else {
|
|
244
|
-
utils.setStatusError(node, "invalid diff");
|
|
245
|
-
}
|
|
246
|
-
break;
|
|
247
|
-
case "anticipator":
|
|
248
|
-
if (typeof msg.payload === 'number' && msg.payload >= -2) {
|
|
249
|
-
node.anticipator = msg.payload;
|
|
250
|
-
utils.setStatusOK(node, `anticipator: ${msg.payload.toFixed(2)}`);
|
|
251
|
-
} else {
|
|
252
|
-
utils.setStatusError(node, "invalid anticipator");
|
|
253
|
-
}
|
|
254
|
-
break;
|
|
255
|
-
case "ignoreAnticipatorCycles":
|
|
256
|
-
if (typeof msg.payload === 'number' && msg.payload >= 0) {
|
|
257
|
-
node.ignoreAnticipatorCycles = Math.floor(msg.payload);
|
|
258
|
-
utils.setStatusOK(node, `ignoreAnticipatorCycles: ${Math.floor(msg.payload)}`);
|
|
259
|
-
} else {
|
|
260
|
-
utils.setStatusError(node, "invalid ignoreAnticipatorCycles");
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
case "isHeating":
|
|
264
|
-
if (typeof msg.payload === "boolean") {
|
|
265
|
-
node.isHeating = msg.payload;
|
|
266
|
-
utils.setStatusOK(node, `isHeating: ${msg.payload}`);
|
|
267
|
-
} else {
|
|
268
|
-
utils.setStatusError(node, "invalid isHeating");
|
|
269
|
-
}
|
|
270
|
-
break;
|
|
271
|
-
default:
|
|
272
|
-
utils.setStatusWarn(node, "unknown context");
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
161
|
+
// ----------------------------------------------------------------
|
|
162
|
+
// 2. Validate constraints
|
|
163
|
+
// ----------------------------------------------------------------
|
|
164
|
+
if (node.diff < 0.01) {
|
|
165
|
+
utils.setStatusError(node, "diff must be >= 0.01");
|
|
275
166
|
if (done) done();
|
|
276
167
|
return;
|
|
277
168
|
}
|
|
278
169
|
|
|
170
|
+
// ----------------------------------------------------------------
|
|
171
|
+
// 4. Read temperature from msg.payload
|
|
172
|
+
// ----------------------------------------------------------------
|
|
279
173
|
if (!msg.hasOwnProperty("payload")) {
|
|
280
174
|
utils.setStatusError(node, "missing payload");
|
|
281
175
|
if (done) done();
|
|
@@ -289,19 +183,14 @@ module.exports = function(RED) {
|
|
|
289
183
|
return;
|
|
290
184
|
}
|
|
291
185
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (done) done();
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Handle mode changes and anticipator logic
|
|
186
|
+
// ----------------------------------------------------------------
|
|
187
|
+
// 4. Anticipator mode-change logic
|
|
188
|
+
// ----------------------------------------------------------------
|
|
300
189
|
if (lastIsHeating !== null && node.isHeating !== lastIsHeating) {
|
|
301
190
|
modeChanged = true;
|
|
302
191
|
cyclesSinceModeChange = 0;
|
|
303
192
|
}
|
|
304
|
-
lastIsHeating = node.isHeating;
|
|
193
|
+
lastIsHeating = node.isHeating;
|
|
305
194
|
if ((below && !lastBelow) || (above && !lastAbove)) {
|
|
306
195
|
cyclesSinceModeChange++;
|
|
307
196
|
}
|
|
@@ -316,155 +205,144 @@ module.exports = function(RED) {
|
|
|
316
205
|
|
|
317
206
|
lastAbove = above;
|
|
318
207
|
lastBelow = below;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
let
|
|
324
|
-
let
|
|
325
|
-
let
|
|
326
|
-
|
|
327
|
-
// Main thermostat logic
|
|
328
|
-
// The Tstat node does not control heating/cooling mode, only operates heating or cooling according to the mode set and respective setpoints.
|
|
208
|
+
|
|
209
|
+
// ----------------------------------------------------------------
|
|
210
|
+
// 5. Thermostat logic — compute above/below calls
|
|
211
|
+
// ----------------------------------------------------------------
|
|
212
|
+
let activeSetpoint = 0;
|
|
213
|
+
let onThreshold = 0;
|
|
214
|
+
let offThreshold = 0;
|
|
215
|
+
|
|
329
216
|
if (node.algorithm === "single") {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
loValue = node.setpoint - delta;
|
|
338
|
-
hiOffValue = node.setpoint + effectiveAnticipator;
|
|
339
|
-
loOffValue = node.setpoint - effectiveAnticipator;
|
|
340
|
-
activeHeatingSetpoint = node.setpoint;
|
|
341
|
-
activeCoolingSetpoint = node.setpoint;
|
|
342
|
-
|
|
343
|
-
if (isHeating) {
|
|
344
|
-
if (input < loValue) {
|
|
217
|
+
const delta = node.diff / 2;
|
|
218
|
+
activeSetpoint = node.setpoint;
|
|
219
|
+
|
|
220
|
+
if (node.isHeating) {
|
|
221
|
+
onThreshold = node.setpoint - delta;
|
|
222
|
+
offThreshold = node.setpoint - effectiveAnticipator;
|
|
223
|
+
if (input < onThreshold) {
|
|
345
224
|
below = true;
|
|
346
|
-
} else if (below && input >
|
|
225
|
+
} else if (below && input > offThreshold) {
|
|
347
226
|
below = false;
|
|
348
227
|
}
|
|
349
228
|
above = false;
|
|
350
229
|
} else {
|
|
351
|
-
|
|
230
|
+
onThreshold = node.setpoint + delta;
|
|
231
|
+
offThreshold = node.setpoint + effectiveAnticipator;
|
|
232
|
+
if (input > onThreshold) {
|
|
352
233
|
above = true;
|
|
353
|
-
} else if (above && input <
|
|
234
|
+
} else if (above && input < offThreshold) {
|
|
354
235
|
above = false;
|
|
355
236
|
}
|
|
356
237
|
below = false;
|
|
357
238
|
}
|
|
358
239
|
} else if (node.algorithm === "split") {
|
|
359
|
-
activeHeatingSetpoint = node.heatingSetpoint;
|
|
360
|
-
activeCoolingSetpoint = node.coolingSetpoint;
|
|
361
240
|
if (node.isHeating) {
|
|
362
|
-
delta = node.diff / 2;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (input <
|
|
241
|
+
const delta = node.diff / 2;
|
|
242
|
+
activeSetpoint = node.heatingSetpoint;
|
|
243
|
+
onThreshold = node.heatingSetpoint - delta;
|
|
244
|
+
offThreshold = node.heatingSetpoint - effectiveAnticipator;
|
|
245
|
+
if (input < onThreshold) {
|
|
367
246
|
below = true;
|
|
368
|
-
} else if (below && input >
|
|
247
|
+
} else if (below && input > offThreshold) {
|
|
369
248
|
below = false;
|
|
370
249
|
}
|
|
371
250
|
above = false;
|
|
372
251
|
} else {
|
|
373
|
-
delta = node.diff / 2;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (input >
|
|
252
|
+
const delta = node.diff / 2;
|
|
253
|
+
activeSetpoint = node.coolingSetpoint;
|
|
254
|
+
onThreshold = node.coolingSetpoint + delta;
|
|
255
|
+
offThreshold = node.coolingSetpoint + effectiveAnticipator;
|
|
256
|
+
if (input > onThreshold) {
|
|
378
257
|
above = true;
|
|
379
|
-
} else if (above && input <
|
|
258
|
+
} else if (above && input < offThreshold) {
|
|
380
259
|
above = false;
|
|
381
260
|
}
|
|
382
261
|
below = false;
|
|
383
262
|
}
|
|
384
263
|
} else if (node.algorithm === "specified") {
|
|
385
|
-
activeHeatingSetpoint = node.heatingOn;
|
|
386
|
-
activeCoolingSetpoint = node.coolingOn;
|
|
387
264
|
if (node.isHeating) {
|
|
388
|
-
|
|
265
|
+
activeSetpoint = node.heatingOn;
|
|
266
|
+
onThreshold = node.heatingOn;
|
|
267
|
+
offThreshold = node.heatingOff - effectiveAnticipator;
|
|
268
|
+
if (input < onThreshold) {
|
|
389
269
|
below = true;
|
|
390
|
-
} else if (below && input >
|
|
270
|
+
} else if (below && input > offThreshold) {
|
|
391
271
|
below = false;
|
|
392
272
|
}
|
|
393
273
|
above = false;
|
|
394
274
|
} else {
|
|
395
|
-
|
|
275
|
+
activeSetpoint = node.coolingOn;
|
|
276
|
+
onThreshold = node.coolingOn;
|
|
277
|
+
offThreshold = node.coolingOff + effectiveAnticipator;
|
|
278
|
+
if (input > onThreshold) {
|
|
396
279
|
above = true;
|
|
397
|
-
} else if (above && input <
|
|
280
|
+
} else if (above && input < offThreshold) {
|
|
398
281
|
above = false;
|
|
399
282
|
}
|
|
400
283
|
below = false;
|
|
401
284
|
}
|
|
402
285
|
}
|
|
403
|
-
|
|
404
|
-
//
|
|
286
|
+
|
|
287
|
+
// ----------------------------------------------------------------
|
|
288
|
+
// 6. Startup suppression
|
|
289
|
+
// ----------------------------------------------------------------
|
|
290
|
+
const outputAbove = node.startupComplete ? above : false;
|
|
291
|
+
const outputBelow = node.startupComplete ? below : false;
|
|
292
|
+
|
|
293
|
+
// ----------------------------------------------------------------
|
|
294
|
+
// 7. Build and send outputs
|
|
295
|
+
// ----------------------------------------------------------------
|
|
405
296
|
const statusInfo = {
|
|
406
297
|
algorithm: node.algorithm,
|
|
407
|
-
input
|
|
298
|
+
input,
|
|
408
299
|
isHeating: node.isHeating,
|
|
409
|
-
above:
|
|
410
|
-
below:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
300
|
+
above: outputAbove,
|
|
301
|
+
below: outputBelow,
|
|
302
|
+
activeSetpoint,
|
|
303
|
+
onThreshold,
|
|
304
|
+
offThreshold,
|
|
305
|
+
diff: node.diff,
|
|
306
|
+
anticipator: node.anticipator,
|
|
307
|
+
effectiveAnticipator,
|
|
308
|
+
modeChanged,
|
|
309
|
+
cyclesSinceModeChange
|
|
414
310
|
};
|
|
415
311
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
statusInfo.loValue = loValue;
|
|
422
|
-
statusInfo.hiValue = hiValue;
|
|
423
|
-
statusInfo.loOffValue = loOffValue;
|
|
424
|
-
statusInfo.hiOffValue = hiOffValue;
|
|
312
|
+
send([
|
|
313
|
+
{ payload: node.isHeating, status: statusInfo },
|
|
314
|
+
{ payload: outputAbove, status: statusInfo },
|
|
315
|
+
{ payload: outputBelow, status: statusInfo }
|
|
316
|
+
]);
|
|
425
317
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
statusInfo.anticipator = node.anticipator;
|
|
437
|
-
}
|
|
318
|
+
// ----------------------------------------------------------------
|
|
319
|
+
// 8. Status display
|
|
320
|
+
// ----------------------------------------------------------------
|
|
321
|
+
const mode = node.isHeating ? "heat" : "cool";
|
|
322
|
+
const call = node.isHeating ? outputBelow : outputAbove;
|
|
323
|
+
const threshold = node.isHeating
|
|
324
|
+
? `<${onThreshold.toFixed(1)}`
|
|
325
|
+
: `>${onThreshold.toFixed(1)}`;
|
|
326
|
+
const suffix = !node.startupComplete ? " [startup]" : "";
|
|
327
|
+
const text = `${input.toFixed(1)}° ${threshold} [${mode}] call:${call}${suffix}`;
|
|
438
328
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
{
|
|
442
|
-
payload: node.isHeating,
|
|
443
|
-
context: "isHeating",
|
|
444
|
-
status: statusInfo
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
payload: above,
|
|
448
|
-
status: statusInfo
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
payload: below,
|
|
452
|
-
status: statusInfo
|
|
453
|
-
}
|
|
454
|
-
];
|
|
455
|
-
|
|
456
|
-
send(outputs);
|
|
457
|
-
|
|
458
|
-
if (above === lastAbove && below === lastBelow) {
|
|
459
|
-
utils.setStatusUnchanged(node, `in: ${input.toFixed(2)}, out: ${node.isHeating ? "heating" : "cooling"}, above: ${above}, below: ${below}`);
|
|
329
|
+
if (outputAbove === lastAbove && outputBelow === lastBelow) {
|
|
330
|
+
utils.setStatusUnchanged(node, text);
|
|
460
331
|
} else {
|
|
461
|
-
utils.setStatusChanged(node,
|
|
332
|
+
utils.setStatusChanged(node, text);
|
|
462
333
|
}
|
|
463
334
|
|
|
464
335
|
if (done) done();
|
|
465
336
|
});
|
|
466
337
|
|
|
338
|
+
// ====================================================================
|
|
339
|
+
// Cleanup
|
|
340
|
+
// ====================================================================
|
|
467
341
|
node.on("close", function(done) {
|
|
342
|
+
if (node.startupTimer) {
|
|
343
|
+
clearTimeout(node.startupTimer);
|
|
344
|
+
node.startupTimer = null;
|
|
345
|
+
}
|
|
468
346
|
done();
|
|
469
347
|
});
|
|
470
348
|
}
|