@bldgblocks/node-red-contrib-control 0.1.17 → 0.1.19
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/README.md +2 -0
- package/nodes/average-block.js +22 -22
- package/nodes/changeover-block.js +38 -32
- package/nodes/delay-block.js +23 -23
- package/nodes/frequency-block.js +1 -1
- package/nodes/hysteresis-block.js +30 -30
- package/nodes/interpolate-block.js +1 -1
- package/nodes/max-block.js +18 -18
- package/nodes/memory-block.js +6 -6
- package/nodes/min-block.js +18 -18
- package/nodes/minmax-block.js +24 -24
- package/nodes/on-change-block.js +18 -19
- package/nodes/scale-range-block.js +2 -2
- package/nodes/tstat-block.html +327 -32
- package/nodes/tstat-block.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@ Sedona-inspired control nodes for stateful logic.
|
|
|
3
3
|
|
|
4
4
|
This is a rather large node collection. Contributions are appreciated.
|
|
5
5
|
|
|
6
|
+
*** If you are reading this, the package was posted very recently and changes will be flowing as I get examples updated. ***
|
|
7
|
+
|
|
6
8
|
## Intro
|
|
7
9
|
This is intended for HVAC usage but the logic applies to anything.
|
|
8
10
|
|
package/nodes/average-block.js
CHANGED
|
@@ -10,6 +10,28 @@ module.exports = function(RED) {
|
|
|
10
10
|
lastAvg: null
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// Evaluate all properties
|
|
14
|
+
try {
|
|
15
|
+
node.runtime.minValid = RED.util.evaluateNodeProperty(
|
|
16
|
+
config.minValid, config.minValidType, node
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
node.runtime.maxValid = RED.util.evaluateNodeProperty(
|
|
20
|
+
config.maxValid, config.maxValidType, node
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Validate values
|
|
24
|
+
if (isNaN(node.runtime.maxValid) || isNaN(node.runtime.minValid) || node.runtime.maxValid <= node.runtime.minValid ) {
|
|
25
|
+
node.status({ fill: "red", shape: "ring", text: `invalid evaluated values ${node.runtime.minValid}, ${node.runtime.maxValid}` });
|
|
26
|
+
if (done) done();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
} catch(err) {
|
|
30
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
31
|
+
if (done) done(err);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
// Validate initial config
|
|
14
36
|
if (isNaN(node.runtime.maxValues) || node.runtime.maxValues < 1) {
|
|
15
37
|
node.runtime.maxValues = 10;
|
|
@@ -28,28 +50,6 @@ module.exports = function(RED) {
|
|
|
28
50
|
return;
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
// Evaluate all properties
|
|
32
|
-
try {
|
|
33
|
-
node.runtime.minValid = RED.util.evaluateNodeProperty(
|
|
34
|
-
config.minValid, config.minValidType, node, msg
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
node.runtime.maxValid = RED.util.evaluateNodeProperty(
|
|
38
|
-
config.maxValid, config.maxValidType, node, msg
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
// Validate values
|
|
42
|
-
if (isNaN(node.runtime.maxValid) || isNaN(node.runtime.minValid) || node.runtime.maxValid <= node.runtime.minValid ) {
|
|
43
|
-
node.status({ fill: "red", shape: "ring", text: `invalid evaluated values ${node.runtime.minValid}, ${node.runtime.maxValid}` });
|
|
44
|
-
if (done) done();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
} catch(err) {
|
|
48
|
-
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
49
|
-
if (done) done(err);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
53
|
// Handle configuration messages
|
|
54
54
|
if (msg.hasOwnProperty("context")) {
|
|
55
55
|
if (!msg.hasOwnProperty("payload")) {
|
|
@@ -18,6 +18,38 @@ module.exports = function(RED) {
|
|
|
18
18
|
lastModeChange: 0
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
// Resolve typed inputs
|
|
22
|
+
let minTemp = node.runtime.minTempSetpoint;
|
|
23
|
+
let maxTemp = node.runtime.maxTempSetpoint;
|
|
24
|
+
|
|
25
|
+
if (node.runtime.algorithm === "single") {
|
|
26
|
+
node.runtime.setpoint = RED.util.evaluateNodeProperty(
|
|
27
|
+
config.setpoint, config.setpointType, node
|
|
28
|
+
);
|
|
29
|
+
node.runtime.setpoint = parseFloat(node.runtime.setpoint);
|
|
30
|
+
} else {
|
|
31
|
+
node.runtime.heatingSetpoint = RED.util.evaluateNodeProperty(
|
|
32
|
+
config.heatingSetpoint, config.heatingSetpointType, node
|
|
33
|
+
);
|
|
34
|
+
node.runtime.heatingSetpoint = parseFloat(node.runtime.heatingSetpoint);
|
|
35
|
+
|
|
36
|
+
node.runtime.coolingSetpoint = RED.util.evaluateNodeProperty(
|
|
37
|
+
config.coolingSetpoint, config.coolingSetpointType, node
|
|
38
|
+
);
|
|
39
|
+
node.runtime.coolingSetpoint = parseFloat(node.runtime.coolingSetpoint);
|
|
40
|
+
|
|
41
|
+
// Validate
|
|
42
|
+
if (node.runtime.coolingSetpoint < node.runtime.heatingSetpoint) {
|
|
43
|
+
node.runtime.coolingSetpoint = node.runtime.heatingSetpoint + 4;
|
|
44
|
+
node.status({ fill: "red", shape: "ring", text: "invalid setpoints, using fallback" });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
node.runtime.swapTime = RED.util.evaluateNodeProperty(
|
|
49
|
+
config.swapTime, config.swapTimeType, node
|
|
50
|
+
);
|
|
51
|
+
node.runtime.swapTime = parseFloat(node.runtime.swapTime);
|
|
52
|
+
|
|
21
53
|
// Initialize state
|
|
22
54
|
let initComplete = false;
|
|
23
55
|
let conditionStartTime = null;
|
|
@@ -32,44 +64,18 @@ module.exports = function(RED) {
|
|
|
32
64
|
if (done) done();
|
|
33
65
|
return;
|
|
34
66
|
}
|
|
35
|
-
|
|
36
|
-
// Resolve typed inputs
|
|
37
|
-
let minTemp = node.runtime.minTempSetpoint;
|
|
38
|
-
let maxTemp = node.runtime.maxTempSetpoint;
|
|
39
|
-
|
|
40
|
-
if (node.runtime.algorithm === "single") {
|
|
41
|
-
node.runtime.setpoint = RED.util.evaluateNodeProperty(
|
|
42
|
-
config.setpoint, config.setpointType, node, msg
|
|
43
|
-
);
|
|
44
|
-
node.runtime.setpoint = parseFloat(node.runtime.setpoint);
|
|
45
|
-
} else {
|
|
46
|
-
node.runtime.heatingSetpoint = RED.util.evaluateNodeProperty(
|
|
47
|
-
config.heatingSetpoint, config.heatingSetpointType, node, msg
|
|
48
|
-
);
|
|
49
|
-
node.runtime.heatingSetpoint = parseFloat(node.runtime.heatingSetpoint);
|
|
50
|
-
|
|
51
|
-
node.runtime.coolingSetpoint = RED.util.evaluateNodeProperty(
|
|
52
|
-
config.coolingSetpoint, config.coolingSetpointType, node, msg
|
|
53
|
-
);
|
|
54
|
-
node.runtime.coolingSetpoint = parseFloat(node.runtime.coolingSetpoint);
|
|
55
|
-
|
|
56
|
-
// Validate
|
|
57
|
-
if (node.runtime.coolingSetpoint < node.runtime.heatingSetpoint) {
|
|
58
|
-
node.runtime.coolingSetpoint = node.runtime.heatingSetpoint + 4;
|
|
59
|
-
node.status({ fill: "red", shape: "ring", text: "invalid setpoints, using fallback" });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
config.swapTime, config.swapTimeType, node, msg
|
|
65
|
-
);
|
|
66
|
-
node.runtime.swapTime = parseFloat(node.runtime.swapTime);
|
|
67
|
-
|
|
68
|
+
// Validate
|
|
68
69
|
if (node.runtime.swapTime < 60) {
|
|
69
70
|
node.runtime.swapTime = 60;
|
|
70
71
|
node.status({ fill: "red", shape: "ring", text: "swapTime below 60s, using 60" });
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
if (node.runtime.coolingSetpoint < node.runtime.heatingSetpoint) {
|
|
75
|
+
node.runtime.coolingSetpoint = node.runtime.heatingSetpoint + 4;
|
|
76
|
+
node.status({ fill: "red", shape: "ring", text: "invalid setpoints, using fallback" });
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
if (msg.hasOwnProperty("context")) {
|
|
74
80
|
if (!msg.hasOwnProperty("payload")) {
|
|
75
81
|
node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
|
package/nodes/delay-block.js
CHANGED
|
@@ -9,6 +9,29 @@ module.exports = function(RED) {
|
|
|
9
9
|
desired: false
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// Evaluate typed-inputs
|
|
13
|
+
try {
|
|
14
|
+
node.runtime.delayOn = RED.util.evaluateNodeProperty(
|
|
15
|
+
config.delayOn, config.delayOnType, node
|
|
16
|
+
);
|
|
17
|
+
node.runtime.delayOn = (parseFloat(node.runtime.delayOn)) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
|
|
18
|
+
|
|
19
|
+
node.runtime.delayOff = RED.util.evaluateNodeProperty(
|
|
20
|
+
config.delayOff, config.delayOffType, node
|
|
21
|
+
);
|
|
22
|
+
node.runtime.delayOff = (parseFloat(node.runtime.delayOff)) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
|
|
23
|
+
|
|
24
|
+
node.period = parseFloat(node.period);
|
|
25
|
+
if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
|
|
26
|
+
node.period = 1000;
|
|
27
|
+
node.status({ fill: "yellow", shape: "ring", text: "invalid period, using 1000ms" });
|
|
28
|
+
}
|
|
29
|
+
} catch(err) {
|
|
30
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
31
|
+
if (done) done(err);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
let timeoutId = null;
|
|
13
36
|
|
|
14
37
|
node.on("input", function(msg, send, done) {
|
|
@@ -18,29 +41,6 @@ module.exports = function(RED) {
|
|
|
18
41
|
return;
|
|
19
42
|
}
|
|
20
43
|
|
|
21
|
-
// Evaluate typed-inputs
|
|
22
|
-
try {
|
|
23
|
-
node.runtime.delayOn = RED.util.evaluateNodeProperty(
|
|
24
|
-
config.delayOn, config.delayOnType, node, msg
|
|
25
|
-
);
|
|
26
|
-
node.runtime.delayOn = (parseFloat(node.runtime.delayOn)) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
|
|
27
|
-
|
|
28
|
-
node.runtime.delayOff = RED.util.evaluateNodeProperty(
|
|
29
|
-
config.delayOff, config.delayOffType, node, msg
|
|
30
|
-
);
|
|
31
|
-
node.runtime.delayOff = (parseFloat(node.runtime.delayOff)) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
|
|
32
|
-
|
|
33
|
-
node.period = parseFloat(node.period);
|
|
34
|
-
if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
|
|
35
|
-
node.period = 1000;
|
|
36
|
-
node.status({ fill: "yellow", shape: "ring", text: "invalid period, using 1000ms" });
|
|
37
|
-
}
|
|
38
|
-
} catch(err) {
|
|
39
|
-
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
40
|
-
if (done) done(err);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
44
|
if (isNaN(node.runtime.delayOn) || node.runtime.delayOn < 0) {
|
|
45
45
|
node.runtime.delayOn = 1000;
|
|
46
46
|
node.status({ fill: "red", shape: "ring", text: "invalid delayOn" });
|
package/nodes/frequency-block.js
CHANGED
|
@@ -114,7 +114,7 @@ module.exports = function(RED) {
|
|
|
114
114
|
node.status({
|
|
115
115
|
fill: "blue",
|
|
116
116
|
shape: "dot",
|
|
117
|
-
text: `ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}`
|
|
117
|
+
text: `input: ${inputValue}, ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}`
|
|
118
118
|
});
|
|
119
119
|
send({ payload: output });
|
|
120
120
|
} else {
|
|
@@ -5,6 +5,36 @@ module.exports = function(RED) {
|
|
|
5
5
|
node.name = config.name;
|
|
6
6
|
node.state = "within";
|
|
7
7
|
|
|
8
|
+
// Evaluate typed-inputs
|
|
9
|
+
try {
|
|
10
|
+
node.upperLimit = RED.util.evaluateNodeProperty(
|
|
11
|
+
config.upperLimit, config.upperLimitType, node
|
|
12
|
+
);
|
|
13
|
+
node.lowerLimit = RED.util.evaluateNodeProperty(
|
|
14
|
+
config.lowerLimit, config.lowerLimitType, node
|
|
15
|
+
);
|
|
16
|
+
node.upperLimitThreshold = RED.util.evaluateNodeProperty(
|
|
17
|
+
config.upperLimitThreshold, config.upperLimitThresholdType, node
|
|
18
|
+
);
|
|
19
|
+
node.lowerLimitThreshold = RED.util.evaluateNodeProperty(
|
|
20
|
+
config.lowerLimitThreshold, config.lowerLimitThresholdType, node
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Validate values
|
|
24
|
+
if (isNaN(node.upperLimit) || isNaN(node.lowerLimit) ||
|
|
25
|
+
isNaN(node.upperLimitThreshold) || isNaN(node.lowerLimitThreshold) ||
|
|
26
|
+
node.upperLimit <= node.lowerLimit ||
|
|
27
|
+
node.upperLimitThreshold < 0 || node.lowerLimitThreshold < 0) {
|
|
28
|
+
node.status({ fill: "red", shape: "ring", text: "invalid evaluated values" });
|
|
29
|
+
if (done) done();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
} catch(err) {
|
|
33
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
34
|
+
if (done) done(err);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
8
38
|
node.on("input", function(msg, send, done) {
|
|
9
39
|
send = send || function() { node.send.apply(node, arguments); };
|
|
10
40
|
|
|
@@ -14,36 +44,6 @@ module.exports = function(RED) {
|
|
|
14
44
|
return;
|
|
15
45
|
}
|
|
16
46
|
|
|
17
|
-
// Evaluate typed-inputs
|
|
18
|
-
try {
|
|
19
|
-
node.upperLimit = RED.util.evaluateNodeProperty(
|
|
20
|
-
config.upperLimit, config.upperLimitType, node, msg
|
|
21
|
-
);
|
|
22
|
-
node.lowerLimit = RED.util.evaluateNodeProperty(
|
|
23
|
-
config.lowerLimit, config.lowerLimitType, node, msg
|
|
24
|
-
);
|
|
25
|
-
node.upperLimitThreshold = RED.util.evaluateNodeProperty(
|
|
26
|
-
config.upperLimitThreshold, config.upperLimitThresholdType, node, msg
|
|
27
|
-
);
|
|
28
|
-
node.lowerLimitThreshold = RED.util.evaluateNodeProperty(
|
|
29
|
-
config.lowerLimitThreshold, config.lowerLimitThresholdType, node, msg
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
// Validate values
|
|
33
|
-
if (isNaN(node.upperLimit) || isNaN(node.lowerLimit) ||
|
|
34
|
-
isNaN(node.upperLimitThreshold) || isNaN(node.lowerLimitThreshold) ||
|
|
35
|
-
node.upperLimit <= node.lowerLimit ||
|
|
36
|
-
node.upperLimitThreshold < 0 || node.lowerLimitThreshold < 0) {
|
|
37
|
-
node.status({ fill: "red", shape: "ring", text: "invalid evaluated values" });
|
|
38
|
-
if (done) done();
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
} catch(err) {
|
|
42
|
-
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
43
|
-
if (done) done(err);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
47
|
if (msg.hasOwnProperty("context")) {
|
|
48
48
|
if (msg.context === "upperLimitThreshold") {
|
|
49
49
|
const value = parseFloat(msg.payload);
|
package/nodes/max-block.js
CHANGED
|
@@ -8,30 +8,30 @@ module.exports = function(RED) {
|
|
|
8
8
|
name: config.name
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Evaluate typed-inputs
|
|
12
|
+
try {
|
|
13
|
+
node.runtime.max = RED.util.evaluateNodeProperty(
|
|
14
|
+
config.max, config.maxType, node
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Validate values
|
|
18
|
+
if (isNaN(node.runtime.max)) {
|
|
19
|
+
node.status({ fill: "red", shape: "ring", text: "invalid evaluated values" });
|
|
20
|
+
if (done) done();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
} catch(err) {
|
|
24
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
25
|
+
if (done) done(err);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
// Store last output value for status
|
|
12
30
|
let lastOutput = null;
|
|
13
31
|
|
|
14
32
|
node.on("input", function(msg, send, done) {
|
|
15
33
|
send = send || function() { node.send.apply(node, arguments); };
|
|
16
34
|
|
|
17
|
-
// Evaluate typed-inputs
|
|
18
|
-
try {
|
|
19
|
-
node.runtime.max = RED.util.evaluateNodeProperty(
|
|
20
|
-
config.max, config.maxType, node, msg
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
// Validate values
|
|
24
|
-
if (isNaN(node.runtime.max)) {
|
|
25
|
-
node.status({ fill: "red", shape: "ring", text: "invalid evaluated values" });
|
|
26
|
-
if (done) done();
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
} catch(err) {
|
|
30
|
-
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
31
|
-
if (done) done(err);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
35
|
// Guard against invalid message
|
|
36
36
|
if (!msg) {
|
|
37
37
|
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
package/nodes/memory-block.js
CHANGED
|
@@ -17,6 +17,12 @@ module.exports = function(RED) {
|
|
|
17
17
|
storedMsg: null
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// Resolve typed inputs
|
|
21
|
+
node.runtime.writePeriod = RED.util.evaluateNodeProperty(
|
|
22
|
+
config.writePeriod, config.writePeriodType, node
|
|
23
|
+
);
|
|
24
|
+
node.runtime.writePeriod = parseFloat(node.runtime.writePeriod);
|
|
25
|
+
|
|
20
26
|
// File path for persistent storage
|
|
21
27
|
const filePath = path.join(RED.settings.userDir, `memory-${node.id}.json`);
|
|
22
28
|
|
|
@@ -81,12 +87,6 @@ module.exports = function(RED) {
|
|
|
81
87
|
return;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
// Resolve typed inputs
|
|
85
|
-
node.runtime.writePeriod = RED.util.evaluateNodeProperty(
|
|
86
|
-
config.writePeriod, config.writePeriodType, node, msg
|
|
87
|
-
);
|
|
88
|
-
node.runtime.writePeriod = parseFloat(node.runtime.writePeriod);
|
|
89
|
-
|
|
90
90
|
// Initialize output array: [Output 1, Output 2]
|
|
91
91
|
const output = [null, null];
|
|
92
92
|
|
package/nodes/min-block.js
CHANGED
|
@@ -8,30 +8,30 @@ module.exports = function(RED) {
|
|
|
8
8
|
name: config.name,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Evaluate typed-inputs
|
|
12
|
+
try {
|
|
13
|
+
node.runtime.min = RED.util.evaluateNodeProperty(
|
|
14
|
+
config.min, config.minType, node
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Validate values
|
|
18
|
+
if (isNaN(node.runtime.min)) {
|
|
19
|
+
node.status({ fill: "red", shape: "ring", text: "invalid evaluated values" });
|
|
20
|
+
if (done) done();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
} catch(err) {
|
|
24
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
25
|
+
if (done) done(err);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
// Store last output value for status
|
|
12
30
|
let lastOutput = null;
|
|
13
31
|
|
|
14
32
|
node.on("input", function(msg, send, done) {
|
|
15
33
|
send = send || function() { node.send.apply(node, arguments); };
|
|
16
34
|
|
|
17
|
-
// Evaluate typed-inputs
|
|
18
|
-
try {
|
|
19
|
-
node.runtime.min = RED.util.evaluateNodeProperty(
|
|
20
|
-
config.min, config.minType, node, msg
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
// Validate values
|
|
24
|
-
if (isNaN(node.runtime.min)) {
|
|
25
|
-
node.status({ fill: "red", shape: "ring", text: "invalid evaluated values" });
|
|
26
|
-
if (done) done();
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
} catch(err) {
|
|
30
|
-
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
31
|
-
if (done) done(err);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
35
|
// Guard against invalid message
|
|
36
36
|
if (!msg) {
|
|
37
37
|
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
package/nodes/minmax-block.js
CHANGED
|
@@ -8,35 +8,35 @@ module.exports = function(RED) {
|
|
|
8
8
|
name: config.name,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Evaluate typed-inputs
|
|
12
|
+
try {
|
|
13
|
+
node.runtime.min = RED.util.evaluateNodeProperty(
|
|
14
|
+
config.min, config.minType, node
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
node.runtime.max = RED.util.evaluateNodeProperty(
|
|
18
|
+
config.max, config.maxType, node
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// Validate min and max at startup
|
|
23
|
+
if (isNaN(node.runtime.min) || isNaN(node.runtime.max) || node.runtime.min > node.runtime.max) {
|
|
24
|
+
node.status({ fill: "red", shape: "dot", text: `invalid min/max` });
|
|
25
|
+
if (done) done();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
} catch(err) {
|
|
29
|
+
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
30
|
+
if (done) done(err);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
11
34
|
// Store last output value for status
|
|
12
35
|
let lastOutput = null;
|
|
13
36
|
|
|
14
37
|
node.on("input", function(msg, send, done) {
|
|
15
38
|
send = send || function() { node.send.apply(node, arguments); };
|
|
16
39
|
|
|
17
|
-
// Evaluate typed-inputs
|
|
18
|
-
try {
|
|
19
|
-
node.runtime.min = RED.util.evaluateNodeProperty(
|
|
20
|
-
config.min, config.minType, node, msg
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
node.runtime.max = RED.util.evaluateNodeProperty(
|
|
24
|
-
config.max, config.maxType, node, msg
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Validate min and max at startup
|
|
29
|
-
if (isNaN(node.runtime.min) || isNaN(node.runtime.max) || node.runtime.min > node.runtime.max) {
|
|
30
|
-
node.status({ fill: "red", shape: "dot", text: `invalid min/max` });
|
|
31
|
-
if (done) done();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
} catch(err) {
|
|
35
|
-
node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
|
|
36
|
-
if (done) done(err);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
40
|
// Guard against invalid message
|
|
41
41
|
if (!msg) {
|
|
42
42
|
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
@@ -65,7 +65,7 @@ module.exports = function(RED) {
|
|
|
65
65
|
node.status({ fill: "yellow", shape: "dot", text: `Context update aborted. Payload more than max` });
|
|
66
66
|
}
|
|
67
67
|
} else if (msg.context === "max") {
|
|
68
|
-
if (value > node.runtime.
|
|
68
|
+
if (value > node.runtime.min) {
|
|
69
69
|
node.runtime.max = value;
|
|
70
70
|
node.status({ fill: "green", shape: "dot", text: `max: ${node.runtime.max}` });
|
|
71
71
|
} else {
|
package/nodes/on-change-block.js
CHANGED
|
@@ -14,6 +14,24 @@ module.exports = function(RED) {
|
|
|
14
14
|
pendingMsg: null
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// Get period
|
|
18
|
+
let period;
|
|
19
|
+
try {
|
|
20
|
+
period = RED.util.evaluateNodeProperty(
|
|
21
|
+
node.runtime.period,
|
|
22
|
+
node.runtime.periodType,
|
|
23
|
+
node
|
|
24
|
+
);
|
|
25
|
+
if (isNaN(period) || period < 0) {
|
|
26
|
+
throw new Error("invalid period");
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
node.status({ fill: "red", shape: "ring", text: "invalid period" });
|
|
30
|
+
send(msg);
|
|
31
|
+
if (done) done();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
// Validate initial config
|
|
18
36
|
if (isNaN(node.runtime.period) || node.runtime.period < 0) {
|
|
19
37
|
node.runtime.period = 0;
|
|
@@ -78,25 +96,6 @@ module.exports = function(RED) {
|
|
|
78
96
|
return;
|
|
79
97
|
}
|
|
80
98
|
|
|
81
|
-
// Get period
|
|
82
|
-
let period;
|
|
83
|
-
try {
|
|
84
|
-
period = RED.util.evaluateNodeProperty(
|
|
85
|
-
node.runtime.period,
|
|
86
|
-
node.runtime.periodType,
|
|
87
|
-
node,
|
|
88
|
-
msg
|
|
89
|
-
);
|
|
90
|
-
if (isNaN(period) || period < 0) {
|
|
91
|
-
throw new Error("invalid period");
|
|
92
|
-
}
|
|
93
|
-
} catch (err) {
|
|
94
|
-
node.status({ fill: "red", shape: "ring", text: "invalid period" });
|
|
95
|
-
send(msg);
|
|
96
|
-
if (done) done();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
99
|
const currentValue = msg.payload;
|
|
101
100
|
|
|
102
101
|
// Deep comparison function
|
|
@@ -86,7 +86,7 @@ module.exports = function(RED) {
|
|
|
86
86
|
if (shouldOutput) {
|
|
87
87
|
const out = calculate(node.runtime.lastInput, node.runtime.inMin, node.runtime.inMax, node.runtime.outMin, node.runtime.outMax, node.runtime.clamp);
|
|
88
88
|
msg.payload = out;
|
|
89
|
-
node.status({ fill: "blue", shape: "dot", text: `
|
|
89
|
+
node.status({ fill: "blue", shape: "dot", text: `in: ${node.runtime.lastInput.toFixed(2)}, out: ${out.toFixed(2)}` });
|
|
90
90
|
send(msg);
|
|
91
91
|
}
|
|
92
92
|
if (done) done();
|
|
@@ -115,7 +115,7 @@ module.exports = function(RED) {
|
|
|
115
115
|
node.runtime.lastInput = inputValue;
|
|
116
116
|
const out = calculate(inputValue, node.runtime.inMin, node.runtime.inMax, node.runtime.outMin, node.runtime.outMax, node.runtime.clamp);
|
|
117
117
|
msg.payload = out;
|
|
118
|
-
node.status({ fill: "blue", shape: "dot", text: `
|
|
118
|
+
node.status({ fill: "blue", shape: "dot", text: `in: ${inputValue.toFixed(2)}, out: ${out.toFixed(2)}` });
|
|
119
119
|
send(msg);
|
|
120
120
|
|
|
121
121
|
if (done) done();
|
package/nodes/tstat-block.html
CHANGED
|
@@ -198,22 +198,310 @@
|
|
|
198
198
|
validateSpecifiedSetpoints();
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
-
|
|
201
|
+
|
|
202
|
+
function validateInputs() {
|
|
203
|
+
const algorithm = $algorithm.val();
|
|
204
|
+
let isValid = true;
|
|
205
|
+
let errorMsg = "";
|
|
206
|
+
|
|
207
|
+
// Clear previous error states
|
|
208
|
+
$("#node-input-anticipator").removeClass("input-error");
|
|
209
|
+
$("#node-input-diff").removeClass("input-error");
|
|
210
|
+
$("#node-input-ignoreAnticipatorCycles").removeClass("input-error");
|
|
211
|
+
$("#node-input-heatingSetpoint").removeClass("input-error");
|
|
212
|
+
$("#node-input-coolingSetpoint").removeClass("input-error");
|
|
213
|
+
$("#node-input-coolingOn").removeClass("input-error");
|
|
214
|
+
$("#node-input-coolingOff").removeClass("input-error");
|
|
215
|
+
$("#node-input-heatingOff").removeClass("input-error");
|
|
216
|
+
$("#node-input-heatingOn").removeClass("input-error");
|
|
217
|
+
$("#split-setpoint-warning").hide();
|
|
218
|
+
$("#specified-setpoint-warning").hide();
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// In editor, we can only validate numeric values directly
|
|
222
|
+
const anticipatorInput = $("#node-input-anticipator").typedInput("value");
|
|
223
|
+
const anticipatorValue = typeof anticipatorInput === 'number' ? anticipatorInput : parseFloat(anticipatorInput);
|
|
224
|
+
|
|
225
|
+
if (typeof anticipatorInput === 'string' || (typeof anticipatorValue === 'number' && !isNaN(anticipatorValue) && anticipatorValue < -2)) {
|
|
226
|
+
$("#node-input-anticipator").addClass("input-error");
|
|
227
|
+
errorMsg = "Invalid anticipator";
|
|
228
|
+
isValid = false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (algorithm === "single") {
|
|
232
|
+
const diffInput = $("#node-input-diff").typedInput("value");
|
|
233
|
+
const diffValue = typeof diffInput === 'number' ? diffInput : parseFloat(diffInput);
|
|
234
|
+
|
|
235
|
+
if (typeof diffInput === 'string' || (typeof diffValue === 'number' && !isNaN(diffValue) && diffValue <= 0)) {
|
|
236
|
+
$("#node-input-diff").addClass("input-error");
|
|
237
|
+
errorMsg = "Invalid differential (must be positive)";
|
|
238
|
+
isValid = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const ignoreCyclesInput = $("#node-input-ignoreAnticipatorCycles").typedInput("value");
|
|
243
|
+
const ignoreCyclesValue = typeof ignoreCyclesInput === 'number' ? ignoreCyclesInput : parseInt(ignoreCyclesInput);
|
|
244
|
+
|
|
245
|
+
if (typeof ignoreCyclesInput === 'string' || (typeof ignoreCyclesValue === 'number' && !isNaN(ignoreCyclesValue) && ignoreCyclesValue < 0)) {
|
|
246
|
+
$("#node-input-ignoreAnticipatorCycles").addClass("input-error");
|
|
247
|
+
errorMsg = "Invalid ignore anticipator cycles (must be non-negative)";
|
|
248
|
+
isValid = false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (algorithm === "split") {
|
|
252
|
+
const heatingSetpointInput = $("#node-input-heatingSetpoint").typedInput("value");
|
|
253
|
+
const heatingSetpointValue = typeof heatingSetpointInput === 'number' ? heatingSetpointInput : parseFloat(heatingSetpointInput);
|
|
254
|
+
|
|
255
|
+
const coolingSetpointInput = $("#node-input-coolingSetpoint").typedInput("value");
|
|
256
|
+
const coolingSetpointValue = typeof coolingSetpointInput === 'number' ? coolingSetpointInput : parseFloat(coolingSetpointInput);
|
|
257
|
+
|
|
258
|
+
if ((typeof heatingSetpointInput !== 'string' && typeof coolingSetpointInput !== 'string') &&
|
|
259
|
+
(typeof heatingSetpointValue === 'number' && typeof coolingSetpointValue === 'number' &&
|
|
260
|
+
!isNaN(heatingSetpointValue) && !isNaN(coolingSetpointValue) && coolingSetpointValue <= heatingSetpointValue)) {
|
|
261
|
+
$("#node-input-heatingSetpoint").addClass("input-error");
|
|
262
|
+
$("#node-input-coolingSetpoint").addClass("input-error");
|
|
263
|
+
$("#split-setpoint-warning").show();
|
|
264
|
+
errorMsg = "Cooling setpoint must be greater than heating setpoint";
|
|
265
|
+
isValid = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (algorithm === "specified") {
|
|
270
|
+
const coolingOnInput = $("#node-input-coolingOn").typedInput("value");
|
|
271
|
+
const coolingOnValue = typeof coolingOnInput === 'number' ? coolingOnInput : parseFloat(coolingOnInput);
|
|
272
|
+
|
|
273
|
+
const coolingOffInput = $("#node-input-coolingOff").typedInput("value");
|
|
274
|
+
const coolingOffValue = typeof coolingOffInput === 'number' ? coolingOffInput : parseFloat(coolingOffInput);
|
|
275
|
+
|
|
276
|
+
const heatingOffInput = $("#node-input-heatingOff").typedInput("value");
|
|
277
|
+
const heatingOffValue = typeof heatingOffInput === 'number' ? heatingOffInput : parseFloat(heatingOffInput);
|
|
278
|
+
|
|
279
|
+
const heatingOnInput = $("#node-input-heatingOn").typedInput("value");
|
|
280
|
+
const heatingOnValue = typeof heatingOnInput === 'number' ? heatingOnInput : parseFloat(heatingOnInput);
|
|
281
|
+
|
|
282
|
+
if ((typeof coolingOnInput !== 'string' && typeof coolingOffInput !== 'string' &&
|
|
283
|
+
typeof heatingOffInput !== 'string' && typeof heatingOnInput !== 'string') &&
|
|
284
|
+
(typeof coolingOnValue === 'number' && typeof coolingOffValue === 'number' &&
|
|
285
|
+
typeof heatingOffValue === 'number' && typeof heatingOnValue === 'number' &&
|
|
286
|
+
!isNaN(coolingOnValue) && !isNaN(coolingOffValue) && !isNaN(heatingOffValue) && !isNaN(heatingOnValue) &&
|
|
287
|
+
(coolingOnValue < coolingOffValue || coolingOffValue < heatingOffValue || heatingOffValue < heatingOnValue))) {
|
|
288
|
+
$("#node-input-coolingOn").addClass("input-error");
|
|
289
|
+
$("#node-input-coolingOff").addClass("input-error");
|
|
290
|
+
$("#node-input-heatingOff").addClass("input-error");
|
|
291
|
+
$("#node-input-heatingOn").addClass("input-error");
|
|
292
|
+
$("#specified-setpoint-warning").show();
|
|
293
|
+
errorMsg = "Invalid setpoints (coolingOn >= coolingOff >= heatingOff >= heatingOn)";
|
|
294
|
+
isValid = false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error("Error validating inputs:", err);
|
|
300
|
+
isValid = false;
|
|
301
|
+
errorMsg = "Validation error";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
$("#node-runtime-changes").text(isValid ? "Effective Setpoints: Valid" : `Effective Setpoints: ${errorMsg}`);
|
|
305
|
+
return isValid;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
202
310
|
$algorithm.on("change", function() {
|
|
203
311
|
toggleFields();
|
|
312
|
+
validateInputs();
|
|
204
313
|
});
|
|
205
314
|
|
|
206
315
|
toggleFields();
|
|
316
|
+
|
|
317
|
+
$("#node-input-diff").on("change input", function() {
|
|
318
|
+
validateInputs();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
$("#node-input-anticipator").on("change input", function() {
|
|
322
|
+
validateInputs();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
$("#node-input-ignoreAnticipatorCycles").on("change input", function() {
|
|
326
|
+
validateInputs();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
$("#node-input-heatingSetpoint").on("change", validateSplitSetpoints);
|
|
330
|
+
$("#node-input-coolingSetpoint").on("change", validateSplitSetpoints);
|
|
331
|
+
$("#node-input-coolingOn").on("change", validateSpecifiedSetpoints);
|
|
332
|
+
$("#node-input-coolingOff").on("change", validateSpecifiedSetpoints);
|
|
333
|
+
$("#node-input-heatingOff").on("change", validateSpecifiedSetpoints);
|
|
334
|
+
$("#node-input-heatingOn").on("change", validateSpecifiedSetpoints);
|
|
335
|
+
|
|
336
|
+
function validateSplitSetpoints() {
|
|
337
|
+
validateInputs();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function validateSpecifiedSetpoints() {
|
|
341
|
+
validateInputs();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function updateEffectiveSetpoints(runtime = {}) {
|
|
345
|
+
const changes = [];
|
|
346
|
+
const algorithm = runtime.algorithm || $("#node-input-algorithm").val() || "single";
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
let diffValue, anticipatorValue;
|
|
350
|
+
|
|
351
|
+
// Get values from runtime if available
|
|
352
|
+
if (runtime.diff !== undefined) {
|
|
353
|
+
diffValue = runtime.diff;
|
|
354
|
+
} else {
|
|
355
|
+
// In editor, we can only parse numeric values directly
|
|
356
|
+
const diffInput = $("#node-input-diff").typedInput("value");
|
|
357
|
+
diffValue = typeof diffInput === 'number' ? diffInput : parseFloat(diffInput) || 2;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (runtime.anticipator !== undefined) {
|
|
361
|
+
anticipatorValue = runtime.anticipator;
|
|
362
|
+
} else {
|
|
363
|
+
const anticipatorInput = $("#node-input-anticipator").typedInput("value");
|
|
364
|
+
anticipatorValue = typeof anticipatorInput === 'number' ? anticipatorInput : parseFloat(anticipatorInput) || 0.5;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (algorithm === "single") {
|
|
368
|
+
let setpointValue;
|
|
369
|
+
if (runtime.setpoint !== undefined) {
|
|
370
|
+
setpointValue = runtime.setpoint;
|
|
371
|
+
} else {
|
|
372
|
+
const setpointInput = $("#node-input-setpoint").typedInput("value");
|
|
373
|
+
setpointValue = typeof setpointInput === 'number' ? setpointInput : parseFloat(setpointInput) || 70;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const heatingOn = setpointValue - diffValue / 2;
|
|
377
|
+
const heatingOff = setpointValue - anticipatorValue;
|
|
378
|
+
const coolingOn = setpointValue + diffValue / 2;
|
|
379
|
+
const coolingOff = setpointValue + anticipatorValue;
|
|
380
|
+
changes.push(`effectiveHeatingOn: ${heatingOn.toFixed(1)}`);
|
|
381
|
+
changes.push(`effectiveHeatingOff: ${heatingOff.toFixed(1)}`);
|
|
382
|
+
changes.push(`effectiveCoolingOn: ${coolingOn.toFixed(1)}`);
|
|
383
|
+
changes.push(`effectiveCoolingOff: ${coolingOff.toFixed(1)}`);
|
|
384
|
+
} else if (algorithm === "split") {
|
|
385
|
+
let heatingSetpointValue, coolingSetpointValue;
|
|
386
|
+
|
|
387
|
+
if (runtime.heatingSetpoint !== undefined) {
|
|
388
|
+
heatingSetpointValue = runtime.heatingSetpoint;
|
|
389
|
+
} else {
|
|
390
|
+
const heatingSetpointInput = $("#node-input-heatingSetpoint").typedInput("value");
|
|
391
|
+
heatingSetpointValue = typeof heatingSetpointInput === 'number' ? heatingSetpointInput : parseFloat(heatingSetpointInput) || 68;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (runtime.coolingSetpoint !== undefined) {
|
|
395
|
+
coolingSetpointValue = runtime.coolingSetpoint;
|
|
396
|
+
} else {
|
|
397
|
+
const coolingSetpointInput = $("#node-input-coolingSetpoint").typedInput("value");
|
|
398
|
+
coolingSetpointValue = typeof coolingSetpointInput === 'number' ? coolingSetpointInput : parseFloat(coolingSetpointInput) || 74;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const heatingOn = heatingSetpointValue - diffValue / 2;
|
|
402
|
+
const heatingOff = heatingSetpointValue - anticipatorValue;
|
|
403
|
+
const coolingOn = coolingSetpointValue + diffValue / 2;
|
|
404
|
+
const coolingOff = coolingSetpointValue + anticipatorValue;
|
|
405
|
+
changes.push(`effectiveHeatingOn: ${heatingOn.toFixed(1)}`);
|
|
406
|
+
changes.push(`effectiveHeatingOff: ${heatingOff.toFixed(1)}`);
|
|
407
|
+
changes.push(`effectiveCoolingOn: ${coolingOn.toFixed(1)}`);
|
|
408
|
+
changes.push(`effectiveCoolingOff: ${coolingOff.toFixed(1)}`);
|
|
409
|
+
} else {
|
|
410
|
+
let coolingOnValue, coolingOffValue, heatingOffValue, heatingOnValue;
|
|
411
|
+
|
|
412
|
+
if (runtime.coolingOn !== undefined) {
|
|
413
|
+
coolingOnValue = runtime.coolingOn;
|
|
414
|
+
} else {
|
|
415
|
+
const coolingOnInput = $("#node-input-coolingOn").typedInput("value");
|
|
416
|
+
coolingOnValue = typeof coolingOnInput === 'number' ? coolingOnInput : parseFloat(coolingOnInput) || 74;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (runtime.coolingOff !== undefined) {
|
|
420
|
+
coolingOffValue = runtime.coolingOff;
|
|
421
|
+
} else {
|
|
422
|
+
const coolingOffInput = $("#node-input-coolingOff").typedInput("value");
|
|
423
|
+
coolingOffValue = typeof coolingOffInput === 'number' ? coolingOffInput : parseFloat(coolingOffInput) || 72;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (runtime.heatingOff !== undefined) {
|
|
427
|
+
heatingOffValue = runtime.heatingOff;
|
|
428
|
+
} else {
|
|
429
|
+
const heatingOffInput = $("#node-input-heatingOff").typedInput("value");
|
|
430
|
+
heatingOffValue = typeof heatingOffInput === 'number' ? heatingOffInput : parseFloat(heatingOffInput) || 68;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (runtime.heatingOn !== undefined) {
|
|
434
|
+
heatingOnValue = runtime.heatingOn;
|
|
435
|
+
} else {
|
|
436
|
+
const heatingOnInput = $("#node-input-heatingOn").typedInput("value");
|
|
437
|
+
heatingOnValue = typeof heatingOnInput === 'number' ? heatingOnInput : parseFloat(heatingOnInput) || 66;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
changes.push(`effectiveHeatingOn: ${heatingOnValue.toFixed(1)}`);
|
|
441
|
+
changes.push(`effectiveHeatingOff: ${(heatingOffValue - anticipatorValue).toFixed(1)}`);
|
|
442
|
+
changes.push(`effectiveCoolingOn: ${coolingOnValue.toFixed(1)}`);
|
|
443
|
+
changes.push(`effectiveCoolingOff: ${(coolingOffValue + anticipatorValue).toFixed(1)}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.error("Error in updateEffectiveSetpoints:", err);
|
|
448
|
+
changes.push("Error calculating effective setpoints");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const displayText = `Effective Setpoints:\n${changes.join("\n")}`;
|
|
452
|
+
$("#node-runtime-changes").text(displayText);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function fetchRuntimeState(attempts = 3, delay = 1000) {
|
|
456
|
+
if (!node.id) {
|
|
457
|
+
$("#node-runtime-changes").text("Effective Setpoints: Node ID missing");
|
|
458
|
+
updateEffectiveSetpoints();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Use a timeout to ensure the node is fully registered
|
|
463
|
+
setTimeout(function() {
|
|
464
|
+
$.getJSON(`/tstat-block-runtime/${node.id}`, function(data) {
|
|
465
|
+
if (data.error) {
|
|
466
|
+
if (attempts > 0) {
|
|
467
|
+
// Retry after delay if node not found yet
|
|
468
|
+
setTimeout(function() {
|
|
469
|
+
fetchRuntimeState(attempts - 1, delay);
|
|
470
|
+
}, delay);
|
|
471
|
+
} else {
|
|
472
|
+
$("#node-runtime-changes").text("Effective Setpoints: Node not ready");
|
|
473
|
+
updateEffectiveSetpoints();
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
updateEffectiveSetpoints(data);
|
|
477
|
+
}
|
|
478
|
+
}).fail(function(xhr, status, error) {
|
|
479
|
+
console.error("Failed to fetch runtime state:", status, error);
|
|
480
|
+
if (attempts > 0) {
|
|
481
|
+
// Retry after delay on failure
|
|
482
|
+
setTimeout(function() {
|
|
483
|
+
fetchRuntimeState(attempts - 1, delay);
|
|
484
|
+
}, delay);
|
|
485
|
+
} else {
|
|
486
|
+
$("#node-runtime-changes").text("Effective Setpoints: Unable to load");
|
|
487
|
+
updateEffectiveSetpoints();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}, 100); // Small delay to ensure node registration
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
fetchRuntimeState();
|
|
495
|
+
validateInputs();
|
|
496
|
+
|
|
497
|
+
$("#node-input-algorithm, #node-input-setpoint, #node-input-heatingSetpoint, #node-input-coolingSetpoint, #node-input-coolingOn, #node-input-coolingOff, #node-input-heatingOff, #node-input-heatingOn, #node-input-diff, #node-input-anticipator, #node-input-ignoreAnticipatorCycles").on("change input", function() {
|
|
498
|
+
updateEffectiveSetpoints();
|
|
499
|
+
validateInputs();
|
|
500
|
+
});
|
|
207
501
|
} catch (err) {
|
|
208
502
|
console.error("Error in tstat-block oneditprepare:", err.message, err.stack);
|
|
209
503
|
$("#node-runtime-changes").text("Effective Setpoints: Error initializing UI - check browser console");
|
|
210
504
|
}
|
|
211
|
-
},
|
|
212
|
-
oneditsave: function() {
|
|
213
|
-
// Standard config properties are automatically saved by Node-RED
|
|
214
|
-
},
|
|
215
|
-
oneditvalidate: function() {
|
|
216
|
-
return validateInputs();
|
|
217
505
|
}
|
|
218
506
|
});
|
|
219
507
|
</script>
|
|
@@ -230,15 +518,15 @@ All output messages include a `msg.status` object containing runtime information
|
|
|
230
518
|
: isHeating (boolean) : `true` for heating mode, `false` for cooling mode. Includes `msg.context = "isHeating"`.
|
|
231
519
|
: above (boolean) : `true` if input exceeds cooling on threshold.
|
|
232
520
|
: below (boolean) : `true` if input is below heating on threshold.
|
|
233
|
-
: status (object) : Contains detailed runtime information including
|
|
234
|
-
- `algorithm
|
|
235
|
-
- `input
|
|
236
|
-
- `isHeating
|
|
237
|
-
- `above/below
|
|
521
|
+
: status (object) : Contains detailed runtime information including:
|
|
522
|
+
- `algorithm`: Current algorithm in use
|
|
523
|
+
- `input`: Current temperature input value
|
|
524
|
+
- `isHeating`: Current heating mode
|
|
525
|
+
- `above/below`: Current output states
|
|
238
526
|
- Algorithm-specific setpoints and values
|
|
239
|
-
- `modeChanged
|
|
240
|
-
- `cyclesSinceModeChange
|
|
241
|
-
- `effectiveAnticipator
|
|
527
|
+
- `modeChanged`: If mode recently changed
|
|
528
|
+
- `cyclesSinceModeChange`: Count since last mode change
|
|
529
|
+
- `effectiveAnticipator`: Current anticipator value after mode change adjustments
|
|
242
530
|
|
|
243
531
|
### Status Monitoring
|
|
244
532
|
Instead of a dedicated status output, all outputs include comprehensive status information
|
|
@@ -258,52 +546,59 @@ Instead of a dedicated status output, all outputs include comprehensive status i
|
|
|
258
546
|
"effectiveAnticipator": 0.5
|
|
259
547
|
}
|
|
260
548
|
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
549
|
### Algorithms
|
|
264
|
-
- Single Setpoint
|
|
550
|
+
- **Single Setpoint**:
|
|
265
551
|
- Uses `setpoint`, `diff`, and `anticipator`.
|
|
266
552
|
- Sets `above` if `input > setpoint + diff/2`, clears when `input < setpoint + anticipator`.
|
|
267
553
|
- Sets `below` if `input < setpoint - diff/2`, clears when `input > setpoint - anticipator`.
|
|
268
|
-
- For positive `anticipator`, stops early to prevent overshoot. For negative `anticipator
|
|
554
|
+
- For positive `anticipator`, stops early to prevent overshoot. For negative `anticipator` (testing only), delays turn-off to overshoot setpoint.
|
|
555
|
+
- Example: `setpoint=70`, `diff=2`, `anticipator=-0.5`, `above` if `input > 71`, clears at `< 69.5` (overshoots); `below` if `input < 69`, clears at `> 70.5` (overshoots).
|
|
269
556
|
|
|
270
|
-
- Split Setpoint
|
|
557
|
+
- **Split Setpoint**:
|
|
271
558
|
- Uses `heatingSetpoint`, `coolingSetpoint`, `diff`, and `anticipator`.
|
|
272
|
-
- For `isHeating = true
|
|
559
|
+
- For `isHeating = true`:
|
|
273
560
|
- Sets `below` if `input < heatingSetpoint - diff/2`, clears when `input > heatingSetpoint - anticipator`.
|
|
274
561
|
- `above` is `false`.
|
|
275
|
-
- For `isHeating = false
|
|
562
|
+
- For `isHeating = false`:
|
|
276
563
|
- Sets `above` if `input > coolingSetpoint + diff/2`, clears when `input < coolingSetpoint + anticipator`.
|
|
277
564
|
- `below` is `false`.
|
|
278
565
|
- Ensures `heatingSetpoint < coolingSetpoint`.
|
|
566
|
+
- For negative `anticipator`, delays turn-off (e.g., heating off above `heatingSetpoint`).
|
|
567
|
+
- Example: `heatingSetpoint=68`, `coolingSetpoint=74`, `diff=2`, `anticipator=-0.5`, heating mode sets `below` if `input < 67`, clears at `> 68.5`; cooling mode sets `above` if `input > 75`, clears at `< 73.5`.
|
|
279
568
|
|
|
280
|
-
- Specified Setpoint
|
|
569
|
+
- **Specified Setpoint**:
|
|
281
570
|
- Uses `coolingOn`, `coolingOff`, `heatingOff`, `heatingOn`, and `anticipator`.
|
|
282
|
-
- For `isHeating = false
|
|
571
|
+
- For `isHeating = false`:
|
|
283
572
|
- Sets `above` if `input > coolingOn`, clears when `input < coolingOff + anticipator`.
|
|
284
573
|
- `below` is `false`.
|
|
285
|
-
- For `isHeating = true
|
|
574
|
+
- For `isHeating = true`:
|
|
286
575
|
- Sets `below` if `input < heatingOn`, clears when `input > heatingOff - anticipator`.
|
|
287
576
|
- `above` is `false`.
|
|
288
577
|
- Validates `coolingOn >= coolingOff >= heatingOff >= heatingOn`.
|
|
578
|
+
- For negative `anticipator`, delays turn-off (e.g., heating off above `heatingOff`).
|
|
579
|
+
- Example: `coolingOn=74`, `coolingOff=72`, `heatingOff=68`, `heatingOn=66`, `anticipator=-0.5`, cooling mode sets `above` if `input > 74`, clears at `< 71.5`; heating mode sets `below` if `input < 66`, clears at `> 68.5`.
|
|
289
580
|
|
|
290
581
|
### Details
|
|
291
582
|
Compares a numeric temperature input (`msg.payload`) against setpoints to control heating or cooling.
|
|
292
583
|
|
|
293
584
|
The `differential` (`diff`) applies to the `single` and `split` setpoint algorithms, providing hysteresis.
|
|
294
585
|
|
|
295
|
-
The `anticipator` adjusts turn-off points
|
|
586
|
+
The `anticipator` adjusts turn-off points: positive values stop early to prevent overshoot (subtracts for heating, adds for cooling);
|
|
587
|
+
negative values (allowed for testing, >= -2) delay turn-off to overshoot setpoint.
|
|
296
588
|
|
|
297
589
|
The `isHeating` flag (typically from a changeover node) sets output 1 and selects the active setpoint(s).
|
|
298
590
|
|
|
299
|
-
The `ignoreAnticipatorCycles` setting allows ignoring the anticipator for a specified number of cycles after a mode change to reduce short-cycling
|
|
591
|
+
The `ignoreAnticipatorCycles` setting allows ignoring the anticipator for a specified number of cycles after a mode change to reduce short-cycling.
|
|
592
|
+
|
|
593
|
+
All numeric inputs (`setpoint`, `heatingSetpoint`, `coolingSetpoint`, `coolingOn`, `coolingOff`, `heatingOff`, `heatingOn`, `diff`, `anticipator`,
|
|
594
|
+
`ignoreAnticipatorCycles`) support `num`, `msg`, `flow`, or `global` types via `typedInput`.
|
|
300
595
|
|
|
301
596
|
### Status
|
|
302
|
-
- Green (dot): Configuration
|
|
303
|
-
- Blue (dot):
|
|
304
|
-
- Blue (ring):
|
|
305
|
-
- Red (ring):
|
|
306
|
-
- Yellow (ring):
|
|
597
|
+
- Green (dot): Configuration updates (e.g., `setpoint: 70.0`, `anticipator: 0.5`, `isHeating: true`).
|
|
598
|
+
- Blue (dot): Outputs when state changes (e.g., `in: 65.00, out: heating, above: false, below: true`).
|
|
599
|
+
- Blue (ring): Outputs when state unchanged (e.g., `in: 66.00, out: heating, above: false, below: true`).
|
|
600
|
+
- Red (ring): Errors (e.g., `missing input`, `invalid coolingOff`, `invalid diff, using 2`).
|
|
601
|
+
- Yellow (ring): Warnings (e.g., `unknown context`).
|
|
307
602
|
|
|
308
603
|
### References
|
|
309
604
|
- [Node-RED Documentation](https://nodered.org/docs/)
|
package/nodes/tstat-block.js
CHANGED
package/package.json
CHANGED