@bldgblocks/node-red-contrib-control 0.1.26 → 0.1.28
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/nodes/analog-switch-block.html +8 -9
- package/nodes/analog-switch-block.js +0 -23
- package/nodes/average-block.html +1 -1
- package/nodes/average-block.js +9 -9
- package/nodes/boolean-switch-block.html +2 -1
- package/nodes/boolean-switch-block.js +1 -2
- package/nodes/call-status-block.html +30 -45
- package/nodes/call-status-block.js +81 -195
- package/nodes/changeover-block.html +76 -12
- package/nodes/changeover-block.js +57 -28
- package/nodes/delay-block.html +1 -1
- package/nodes/delay-block.js +8 -8
- package/nodes/enum-switch-block.html +157 -0
- package/nodes/enum-switch-block.js +101 -0
- package/nodes/hysteresis-block.html +1 -1
- package/nodes/hysteresis-block.js +17 -13
- package/nodes/latch-block.html +55 -0
- package/nodes/latch-block.js +77 -0
- package/nodes/max-block.js +2 -4
- package/nodes/memory-block.js +3 -6
- package/nodes/min-block.js +2 -4
- package/nodes/minmax-block.js +9 -9
- package/nodes/on-change-block.html +0 -1
- package/nodes/on-change-block.js +4 -16
- package/nodes/pid-block.html +102 -80
- package/nodes/pid-block.js +121 -111
- package/nodes/rate-of-change-block.html +110 -0
- package/nodes/rate-of-change-block.js +233 -0
- package/nodes/string-builder-block.html +112 -0
- package/nodes/string-builder-block.js +89 -0
- package/nodes/tstat-block.html +85 -39
- package/nodes/tstat-block.js +105 -56
- package/nodes/utils.js +1 -22
- package/package.json +5 -1
package/nodes/pid-block.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
|
|
2
4
|
function PIDBlockNode(config) {
|
|
3
5
|
RED.nodes.createNode(this, config);
|
|
6
|
+
|
|
4
7
|
const node = this;
|
|
5
8
|
|
|
6
|
-
// Initialize runtime state
|
|
9
|
+
// Initialize runtime state
|
|
7
10
|
node.runtime = {
|
|
8
|
-
name: config.name
|
|
9
|
-
|
|
10
|
-
ki: parseFloat(config.ki) || 0,
|
|
11
|
-
kd: parseFloat(config.kd) || 0,
|
|
12
|
-
setpoint: parseFloat(config.setpoint) || 0,
|
|
13
|
-
deadband: parseFloat(config.deadband) || 0,
|
|
14
|
-
dbBehavior: config.dbBehavior || "ReturnToZero",
|
|
15
|
-
outMin: config.outMin ? parseFloat(config.outMin) : null,
|
|
16
|
-
outMax: config.outMax ? parseFloat(config.outMax) : null,
|
|
17
|
-
maxChange: parseFloat(config.maxChange) || 0,
|
|
18
|
-
directAction: !!config.directAction,
|
|
19
|
-
run: config.run !== false,
|
|
11
|
+
name: config.name,
|
|
12
|
+
dbBehavior: config.dbBehavior,
|
|
20
13
|
errorSum: 0,
|
|
21
14
|
lastError: 0,
|
|
22
15
|
lastDError: 0,
|
|
@@ -25,36 +18,35 @@ module.exports = function(RED) {
|
|
|
25
18
|
tuneMode: false,
|
|
26
19
|
tuneData: { oscillations: [], lastPeak: null, lastTrough: null, Ku: 0, Tu: 0 }
|
|
27
20
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
node.
|
|
35
|
-
node.runtime.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
node.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (node.runtime.outMin != null && node.runtime.outMax != null && node.runtime.outMax <= node.runtime.outMin) {
|
|
42
|
-
node.status({ fill: "red", shape: "ring", text: "invalid output range" });
|
|
43
|
-
node.runtime.outMin = node.runtime.outMax = null;
|
|
44
|
-
}
|
|
45
|
-
if (!["ReturnToZero", "HoldLastResult"].includes(node.runtime.dbBehavior)) {
|
|
46
|
-
node.status({ fill: "red", shape: "ring", text: "invalid dbBehavior" });
|
|
47
|
-
node.runtime.dbBehavior = "ReturnToZero";
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
node.runtime.kp = parseFloat(RED.util.evaluateNodeProperty( config.kp, config.kpType, node ));
|
|
24
|
+
node.runtime.ki = parseFloat(RED.util.evaluateNodeProperty( config.ki, config.kiType, node ));
|
|
25
|
+
node.runtime.kd = parseFloat(RED.util.evaluateNodeProperty( config.kd, config.kdType, node ));
|
|
26
|
+
node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node ));
|
|
27
|
+
node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node ));
|
|
28
|
+
node.runtime.outMin = parseFloat(RED.util.evaluateNodeProperty( config.outMin, config.outMinType, node ));
|
|
29
|
+
node.runtime.outMax = parseFloat(RED.util.evaluateNodeProperty( config.outMax, config.outMaxType, node ));
|
|
30
|
+
node.runtime.maxChange = parseFloat(RED.util.evaluateNodeProperty( config.maxChange, config.maxChangeType, node ));
|
|
31
|
+
node.runtime.run = RED.util.evaluateNodeProperty( config.run, config.runType, node ) === true;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
node.error(`Error evaluating properties: ${err.message}`);
|
|
48
34
|
}
|
|
49
35
|
|
|
50
36
|
// Initialize internal variables
|
|
51
|
-
let storekp =
|
|
52
|
-
let storeki =
|
|
53
|
-
let
|
|
54
|
-
let
|
|
55
|
-
let
|
|
56
|
-
let
|
|
57
|
-
let
|
|
37
|
+
let storekp = parseFloat(config.kp) || 0;
|
|
38
|
+
let storeki = parseFloat(config.ki) || 0;
|
|
39
|
+
let storekd = parseFloat(config.kd) || 0;
|
|
40
|
+
let storesetpoint = parseFloat(config.setpoint) || 0;
|
|
41
|
+
let storedeadband = parseFloat(config.deadband) || 0;
|
|
42
|
+
let storeOutMin = config.outMin ? parseFloat(config.outMin) : null;
|
|
43
|
+
let storeOutMax = config.outMax ? parseFloat(config.outMax) : null;
|
|
44
|
+
let storemaxChange = parseFloat(config.maxChange) || 0;
|
|
45
|
+
let storerun = !!config.run; // convert to boolean
|
|
46
|
+
|
|
47
|
+
let kpkiConst = storekp * storeki;
|
|
48
|
+
let minInt = kpkiConst === 0 ? 0 : (storeOutMin || -Infinity) * kpkiConst;
|
|
49
|
+
let maxInt = kpkiConst === 0 ? 0 : (storeOutMax || Infinity) * kpkiConst;
|
|
58
50
|
let lastOutput = null;
|
|
59
51
|
|
|
60
52
|
node.on("input", function(msg, send, done) {
|
|
@@ -67,6 +59,60 @@ module.exports = function(RED) {
|
|
|
67
59
|
return;
|
|
68
60
|
}
|
|
69
61
|
|
|
62
|
+
// Evaluate typed-input properties
|
|
63
|
+
try {
|
|
64
|
+
if (utils.requiresEvaluation(config.kpType)) {
|
|
65
|
+
node.runtime.kp = parseFloat(RED.util.evaluateNodeProperty( config.kp, config.kpType, node, msg ));
|
|
66
|
+
}
|
|
67
|
+
if (utils.requiresEvaluation(config.kiType)) {
|
|
68
|
+
node.runtime.ki = parseFloat(RED.util.evaluateNodeProperty( config.ki, config.kiType, node, msg ));
|
|
69
|
+
}
|
|
70
|
+
if (utils.requiresEvaluation(config.kdType)) {
|
|
71
|
+
node.runtime.kd = parseFloat(RED.util.evaluateNodeProperty( config.kd, config.kdType, node, msg ));
|
|
72
|
+
}
|
|
73
|
+
if (utils.requiresEvaluation(config.setpointType)) {
|
|
74
|
+
node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node, msg ));
|
|
75
|
+
}
|
|
76
|
+
if (utils.requiresEvaluation(config.deadbandType)) {
|
|
77
|
+
node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node, msg ));
|
|
78
|
+
}
|
|
79
|
+
if (utils.requiresEvaluation(config.outMinType)) {
|
|
80
|
+
node.runtime.outMin = parseFloat(RED.util.evaluateNodeProperty( config.outMin, config.outMinType, node, msg ));
|
|
81
|
+
}
|
|
82
|
+
if (utils.requiresEvaluation(config.outMaxType)) {
|
|
83
|
+
node.runtime.outMax = parseFloat(RED.util.evaluateNodeProperty( config.outMax, config.outMaxType, node, msg ));
|
|
84
|
+
}
|
|
85
|
+
if (utils.requiresEvaluation(config.maxChangeType)) {
|
|
86
|
+
node.runtime.maxChange = parseFloat(RED.util.evaluateNodeProperty( config.maxChange, config.maxChangeType, node, msg ));
|
|
87
|
+
}
|
|
88
|
+
if (utils.requiresEvaluation(config.runType)) {
|
|
89
|
+
node.runtime.run = RED.util.evaluateNodeProperty( config.run, config.runType, node, msg ) === true;
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
node.error(`Error evaluating properties: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Validate config
|
|
96
|
+
if (isNaN(node.runtime.kp) || isNaN(node.runtime.ki) || isNaN(node.runtime.kd) ||
|
|
97
|
+
isNaN(node.runtime.setpoint) || isNaN(node.runtime.deadband) || isNaN(node.runtime.maxChange) ||
|
|
98
|
+
!isFinite(node.runtime.kp) || !isFinite(node.runtime.ki) || !isFinite(node.runtime.kd) ||
|
|
99
|
+
!isFinite(node.runtime.setpoint) || !isFinite(node.runtime.deadband) || !isFinite(node.runtime.maxChange)) {
|
|
100
|
+
node.status({ fill: "red", shape: "ring", text: "invalid config" });
|
|
101
|
+
node.runtime.kp = node.runtime.ki = node.runtime.kd = node.runtime.setpoint = node.runtime.deadband = node.runtime.maxChange = 0;
|
|
102
|
+
}
|
|
103
|
+
if (node.runtime.deadband < 0 || node.runtime.maxChange < 0) {
|
|
104
|
+
node.status({ fill: "red", shape: "ring", text: "invalid deadband or maxChange" });
|
|
105
|
+
node.runtime.deadband = node.runtime.maxChange = 0;
|
|
106
|
+
}
|
|
107
|
+
if (node.runtime.outMin != null && node.runtime.outMax != null && node.runtime.outMax <= node.runtime.outMin) {
|
|
108
|
+
node.status({ fill: "red", shape: "ring", text: "invalid output range" });
|
|
109
|
+
node.runtime.outMin = node.runtime.outMax = null;
|
|
110
|
+
}
|
|
111
|
+
if (!["ReturnToZero", "HoldLastResult"].includes(node.runtime.dbBehavior)) {
|
|
112
|
+
node.status({ fill: "red", shape: "ring", text: "invalid dbBehavior" });
|
|
113
|
+
node.runtime.dbBehavior = "ReturnToZero";
|
|
114
|
+
}
|
|
115
|
+
|
|
70
116
|
// Handle context updates
|
|
71
117
|
if (msg.hasOwnProperty("context")) {
|
|
72
118
|
if (!msg.hasOwnProperty("payload")) {
|
|
@@ -156,14 +202,14 @@ module.exports = function(RED) {
|
|
|
156
202
|
}
|
|
157
203
|
|
|
158
204
|
if (!msg.hasOwnProperty("payload")) {
|
|
159
|
-
node.status({ fill: "red", shape: "ring", text: "missing
|
|
205
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
160
206
|
if (done) done();
|
|
161
207
|
return;
|
|
162
208
|
}
|
|
163
209
|
|
|
164
210
|
const input = parseFloat(msg.payload);
|
|
165
211
|
if (isNaN(input) || !isFinite(input)) {
|
|
166
|
-
node.status({ fill: "red", shape: "ring", text: "invalid
|
|
212
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
167
213
|
if (done) done();
|
|
168
214
|
return;
|
|
169
215
|
}
|
|
@@ -173,7 +219,18 @@ module.exports = function(RED) {
|
|
|
173
219
|
let interval = (currentTime - node.runtime.lastTime) / 1000; // Seconds
|
|
174
220
|
node.runtime.lastTime = currentTime;
|
|
175
221
|
|
|
176
|
-
let outputMsg = { payload: 0
|
|
222
|
+
let outputMsg = { payload: 0 };
|
|
223
|
+
outputMsg.diagnostics = {
|
|
224
|
+
setpoint: node.runtime.setpoint,
|
|
225
|
+
interval,
|
|
226
|
+
lastOutput,
|
|
227
|
+
run: node.runtime.run,
|
|
228
|
+
directAction: node.runtime.directAction,
|
|
229
|
+
kp: node.runtime.kp,
|
|
230
|
+
ki: node.runtime.ki,
|
|
231
|
+
kd: node.runtime.kd
|
|
232
|
+
};
|
|
233
|
+
|
|
177
234
|
if (!node.runtime.run || interval <= 0 || node.runtime.kp === 0) {
|
|
178
235
|
if (lastOutput !== 0) {
|
|
179
236
|
lastOutput = 0;
|
|
@@ -182,7 +239,6 @@ module.exports = function(RED) {
|
|
|
182
239
|
shape: "dot",
|
|
183
240
|
text: `in: ${input.toFixed(2)}, out: 0.00, setpoint: ${node.runtime.setpoint.toFixed(2)}`
|
|
184
241
|
});
|
|
185
|
-
send(outputMsg);
|
|
186
242
|
} else {
|
|
187
243
|
node.status({
|
|
188
244
|
fill: "blue",
|
|
@@ -190,6 +246,7 @@ module.exports = function(RED) {
|
|
|
190
246
|
text: `in: ${input.toFixed(2)}, out: 0.00, setpoint: ${node.runtime.setpoint.toFixed(2)}`
|
|
191
247
|
});
|
|
192
248
|
}
|
|
249
|
+
send(outputMsg);
|
|
193
250
|
if (done) done();
|
|
194
251
|
return;
|
|
195
252
|
}
|
|
@@ -218,7 +275,7 @@ module.exports = function(RED) {
|
|
|
218
275
|
}
|
|
219
276
|
|
|
220
277
|
// Update internal constraints
|
|
221
|
-
if (node.runtime.kp !== storekp || node.runtime.ki !== storeki || node.runtime.outMin !==
|
|
278
|
+
if (node.runtime.kp !== storekp || node.runtime.ki !== storeki || node.runtime.outMin !== storeOutMin || node.runtime.outMax !== storeOutMax) {
|
|
222
279
|
if (node.runtime.kp !== storekp && node.runtime.kp !== 0 && storekp !== 0) {
|
|
223
280
|
node.runtime.errorSum = node.runtime.errorSum * storekp / node.runtime.kp;
|
|
224
281
|
}
|
|
@@ -230,8 +287,8 @@ module.exports = function(RED) {
|
|
|
230
287
|
maxInt = kpkiConst === 0 ? 0 : (node.runtime.outMax || Infinity) * kpkiConst;
|
|
231
288
|
storekp = node.runtime.kp;
|
|
232
289
|
storeki = node.runtime.ki;
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
storeOutMin = node.runtime.outMin;
|
|
291
|
+
storeOutMax = node.runtime.outMax;
|
|
235
292
|
}
|
|
236
293
|
|
|
237
294
|
// Calculate error
|
|
@@ -272,6 +329,7 @@ module.exports = function(RED) {
|
|
|
272
329
|
shape: "dot",
|
|
273
330
|
text: `tune: completed, Kp=${node.runtime.kp.toFixed(2)}, Ki=${node.runtime.ki.toFixed(2)}, Kd=${node.runtime.kd.toFixed(2)}`
|
|
274
331
|
});
|
|
332
|
+
|
|
275
333
|
send(outputMsg);
|
|
276
334
|
if (done) done();
|
|
277
335
|
return;
|
|
@@ -301,8 +359,8 @@ module.exports = function(RED) {
|
|
|
301
359
|
|
|
302
360
|
// Output calculation
|
|
303
361
|
let pv = pGain + intGain + dGain;
|
|
304
|
-
if (node.runtime.directAction) pv = -pv;
|
|
305
|
-
pv = Math.min(Math.max(pv, node.runtime.outMin
|
|
362
|
+
//if (node.runtime.directAction) pv = -pv;
|
|
363
|
+
pv = Math.min(Math.max(pv, node.runtime.outMin), node.runtime.outMax);
|
|
306
364
|
|
|
307
365
|
// Rate of change limit
|
|
308
366
|
if (node.runtime.maxChange !== 0) {
|
|
@@ -316,7 +374,18 @@ module.exports = function(RED) {
|
|
|
316
374
|
}
|
|
317
375
|
|
|
318
376
|
outputMsg.payload = node.runtime.result;
|
|
319
|
-
outputMsg.diagnostics = {
|
|
377
|
+
outputMsg.diagnostics = {
|
|
378
|
+
pGain,
|
|
379
|
+
intGain,
|
|
380
|
+
dGain,
|
|
381
|
+
error,
|
|
382
|
+
errorSum: node.runtime.errorSum,
|
|
383
|
+
run: node.runtime.run,
|
|
384
|
+
directAction: node.runtime.directAction,
|
|
385
|
+
kp: node.runtime.kp,
|
|
386
|
+
ki: node.runtime.ki,
|
|
387
|
+
kd: node.runtime.kd
|
|
388
|
+
};
|
|
320
389
|
|
|
321
390
|
const outputChanged = !lastOutput || lastOutput !== outputMsg.payload;
|
|
322
391
|
if (outputChanged) {
|
|
@@ -326,7 +395,6 @@ module.exports = function(RED) {
|
|
|
326
395
|
shape: "dot",
|
|
327
396
|
text: `in: ${input.toFixed(2)}, out: ${node.runtime.result.toFixed(2)}, setpoint: ${node.runtime.setpoint.toFixed(2)}`
|
|
328
397
|
});
|
|
329
|
-
send(outputMsg);
|
|
330
398
|
} else {
|
|
331
399
|
node.status({
|
|
332
400
|
fill: "blue",
|
|
@@ -335,73 +403,15 @@ module.exports = function(RED) {
|
|
|
335
403
|
});
|
|
336
404
|
}
|
|
337
405
|
|
|
406
|
+
send(outputMsg);
|
|
407
|
+
|
|
338
408
|
if (done) done();
|
|
339
409
|
});
|
|
340
410
|
|
|
341
411
|
node.on("close", function(done) {
|
|
342
|
-
node.runtime = {
|
|
343
|
-
name: config.name || "",
|
|
344
|
-
kp: parseFloat(config.kp) || 0,
|
|
345
|
-
ki: parseFloat(config.ki) || 0,
|
|
346
|
-
kd: parseFloat(config.kd) || 0,
|
|
347
|
-
setpoint: parseFloat(config.setpoint) || 0,
|
|
348
|
-
deadband: parseFloat(config.deadband) || 0,
|
|
349
|
-
dbBehavior: config.dbBehavior || "ReturnToZero",
|
|
350
|
-
outMin: config.outMin ? parseFloat(config.outMin) : null,
|
|
351
|
-
outMax: config.outMax ? parseFloat(config.outMax) : null,
|
|
352
|
-
maxChange: parseFloat(config.maxChange) || 0,
|
|
353
|
-
directAction: !!config.directAction,
|
|
354
|
-
run: config.run !== false,
|
|
355
|
-
errorSum: 0,
|
|
356
|
-
lastError: 0,
|
|
357
|
-
lastDError: 0,
|
|
358
|
-
result: 0,
|
|
359
|
-
lastTime: Date.now(),
|
|
360
|
-
tuneMode: false,
|
|
361
|
-
tuneData: { oscillations: [], lastPeak: null, lastTrough: null, Ku: 0, Tu: 0 }
|
|
362
|
-
};
|
|
363
|
-
if (isNaN(node.runtime.kp) || isNaN(node.runtime.ki) || isNaN(node.runtime.kd) ||
|
|
364
|
-
isNaN(node.runtime.setpoint) || isNaN(node.runtime.deadband) || isNaN(node.runtime.maxChange) ||
|
|
365
|
-
!isFinite(node.runtime.kp) || !isFinite(node.runtime.ki) || !isFinite(node.runtime.kd) ||
|
|
366
|
-
!isFinite(node.runtime.setpoint) || !isFinite(node.runtime.deadband) || !isFinite(node.runtime.maxChange)) {
|
|
367
|
-
node.runtime.kp = node.runtime.ki = node.runtime.kd = node.runtime.setpoint = node.runtime.deadband = node.runtime.maxChange = 0;
|
|
368
|
-
}
|
|
369
|
-
if (node.runtime.deadband < 0 || node.runtime.maxChange < 0) {
|
|
370
|
-
node.runtime.deadband = node.runtime.maxChange = 0;
|
|
371
|
-
}
|
|
372
|
-
if (node.runtime.outMin != null && node.runtime.outMax != null && node.runtime.outMax <= node.runtime.outMin) {
|
|
373
|
-
node.runtime.outMin = node.runtime.outMax = null;
|
|
374
|
-
}
|
|
375
|
-
if (!["ReturnToZero", "HoldLastResult"].includes(node.runtime.dbBehavior)) {
|
|
376
|
-
node.runtime.dbBehavior = "ReturnToZero";
|
|
377
|
-
}
|
|
378
|
-
node.status({});
|
|
379
412
|
done();
|
|
380
413
|
});
|
|
381
414
|
}
|
|
382
415
|
|
|
383
416
|
RED.nodes.registerType("pid-block", PIDBlockNode);
|
|
384
|
-
|
|
385
|
-
// Serve runtime state for editor
|
|
386
|
-
RED.httpAdmin.get("/pid-block-runtime/:id", RED.auth.needsPermission("pid-block.read"), function(req, res) {
|
|
387
|
-
const node = RED.nodes.getNode(req.params.id);
|
|
388
|
-
if (node && node.type === "pid-block") {
|
|
389
|
-
res.json({
|
|
390
|
-
name: node.runtime.name,
|
|
391
|
-
kp: node.runtime.kp,
|
|
392
|
-
ki: node.runtime.ki,
|
|
393
|
-
kd: node.runtime.kd,
|
|
394
|
-
setpoint: node.runtime.setpoint,
|
|
395
|
-
deadband: node.runtime.deadband,
|
|
396
|
-
dbBehavior: node.runtime.dbBehavior,
|
|
397
|
-
outMin: node.runtime.outMin,
|
|
398
|
-
outMax: node.runtime.outMax,
|
|
399
|
-
maxChange: node.runtime.maxChange,
|
|
400
|
-
directAction: node.runtime.directAction,
|
|
401
|
-
run: node.runtime.run
|
|
402
|
-
});
|
|
403
|
-
} else {
|
|
404
|
-
res.status(404).json({ error: "Node not found" });
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
417
|
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="rate-of-change-block">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-sampleSize" title="Number of samples to track (minimum 2)"><i class="fa fa-list-ol"></i> Sample Size</label>
|
|
8
|
+
<input type="number" id="node-input-sampleSize" placeholder="10" min="2" step="1">
|
|
9
|
+
</div>
|
|
10
|
+
<div class="form-row">
|
|
11
|
+
<label for="node-input-units" title="Time units for rate calculation"><i class="fa fa-clock-o"></i> Rate Units</label>
|
|
12
|
+
<select id="node-input-units">
|
|
13
|
+
<option value="seconds">Seconds</option>
|
|
14
|
+
<option value="minutes" selected>Minutes</option>
|
|
15
|
+
<option value="hours">Hours</option>
|
|
16
|
+
</select>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-row">
|
|
19
|
+
<label for="node-input-minValid"><i class="fa fa-arrow-down"></i> Minimum Valid Temp</label>
|
|
20
|
+
<input type="text" id="node-input-minValid" placeholder="-40">
|
|
21
|
+
<input type="hidden" id="node-input-minValidType">
|
|
22
|
+
</div>
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<label for="node-input-maxValid"><i class="fa fa-arrow-up"></i> Maximum Valid Temp</label>
|
|
25
|
+
<input type="text" id="node-input-maxValid" placeholder="150">
|
|
26
|
+
<input type="hidden" id="node-input-maxValidType">
|
|
27
|
+
</div>
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/javascript">
|
|
31
|
+
RED.nodes.registerType("rate-of-change-block", {
|
|
32
|
+
category: "control",
|
|
33
|
+
color: "#301934",
|
|
34
|
+
defaults: {
|
|
35
|
+
name: { value: "" },
|
|
36
|
+
sampleSize: {
|
|
37
|
+
value: 10,
|
|
38
|
+
required: true,
|
|
39
|
+
validate: function(v) { return !isNaN(parseInt(v)) && parseInt(v) >= 2; }
|
|
40
|
+
},
|
|
41
|
+
units: { value: "minutes" },
|
|
42
|
+
minValid: { value: -40, required: true },
|
|
43
|
+
minValidType: { value: "num" },
|
|
44
|
+
maxValid: { value: 150, required: true },
|
|
45
|
+
maxValidType: { value: "num" }
|
|
46
|
+
},
|
|
47
|
+
inputs: 1,
|
|
48
|
+
outputs: 1,
|
|
49
|
+
inputLabels: ["input"],
|
|
50
|
+
outputLabels: ["rate"],
|
|
51
|
+
icon: "font-awesome/fa-bar-chart",
|
|
52
|
+
paletteLabel: "rate of change",
|
|
53
|
+
label: function() {
|
|
54
|
+
return this.name ? `${this.name} (${this.sampleSize} samples)` : `RoC (${this.sampleSize})`;
|
|
55
|
+
},
|
|
56
|
+
oneditprepare: function() {
|
|
57
|
+
const node = this;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Initialize typed inputs
|
|
61
|
+
$("#node-input-minValid").typedInput({
|
|
62
|
+
default: "num",
|
|
63
|
+
types: ["num", "msg", "flow", "global"],
|
|
64
|
+
typeField: "#node-input-minValidType"
|
|
65
|
+
}).typedInput("type", node.minValidType || "num").typedInput("value", node.minValid || "-40");
|
|
66
|
+
|
|
67
|
+
$("#node-input-maxValid").typedInput({
|
|
68
|
+
default: "num",
|
|
69
|
+
types: ["num", "msg", "flow", "global"],
|
|
70
|
+
typeField: "#node-input-maxValidType"
|
|
71
|
+
}).typedInput("type", node.maxValidType || "num").typedInput("value", node.maxValid || "150");
|
|
72
|
+
|
|
73
|
+
// Set units dropdown
|
|
74
|
+
$("#node-input-units").val(node.units || "minutes");
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("Error in oneditprepare:", err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<script type="text/markdown" data-help-name="rate-of-change-block">
|
|
83
|
+
Calculates the rate of temperature change over time for HVAC applications.
|
|
84
|
+
|
|
85
|
+
### Inputs
|
|
86
|
+
: context (string) : Configures reset (`"reset"`), sample size (`"sampleSize"`), or units (`"units"`).
|
|
87
|
+
: payload (number) : Temperature value for rate calculation.
|
|
88
|
+
: timestamp (optional) : Custom timestamp for the reading.
|
|
89
|
+
|
|
90
|
+
### Outputs
|
|
91
|
+
: payload (number | null) : Rate of change in temperature per time unit.
|
|
92
|
+
: samples (number) : Current number of samples in buffer.
|
|
93
|
+
: units (string) : Rate units ("°/s", "°/min", "°/hr").
|
|
94
|
+
: currentValue (number) : Most recent temperature value.
|
|
95
|
+
: timeSpan (number) : Time span of sample buffer in seconds.
|
|
96
|
+
|
|
97
|
+
### Details
|
|
98
|
+
Tracks temperature changes over a rolling window of samples. Calculates rate as (last_value - first_value) / time_difference.
|
|
99
|
+
|
|
100
|
+
Useful for detecting HVAC issues like temperature droop, defrost cycles, or rapid changes.
|
|
101
|
+
|
|
102
|
+
### Configuration
|
|
103
|
+
- Reset via `msg.context = "reset"` with `msg.payload = true`
|
|
104
|
+
- Change sample size via `msg.context = "sampleSize"` with numeric payload
|
|
105
|
+
- Change units via `msg.context = "units"` with "seconds", "minutes", or "hours"
|
|
106
|
+
|
|
107
|
+
### Status
|
|
108
|
+
- Shows current rate with units
|
|
109
|
+
- Color indicates state change
|
|
110
|
+
</script>
|