@bldgblocks/node-red-contrib-control 0.1.18 → 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.
@@ -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
- node.runtime.swapTime = RED.util.evaluateNodeProperty(
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}` });
@@ -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" });
@@ -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);
@@ -6,7 +6,7 @@ module.exports = function(RED) {
6
6
 
7
7
  // Initialize runtime state
8
8
  node.runtime = {
9
- name: config.name || "interpolate",
9
+ name: config.name,
10
10
  points: null,
11
11
  lastOutput: null
12
12
  };
@@ -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" });
@@ -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
 
@@ -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" });
@@ -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.max) {
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 {
@@ -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: `out: ${out.toFixed(2)}, in: ${node.runtime.lastInput.toFixed(2)}` });
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: `out: ${out.toFixed(2)}, in: ${inputValue.toFixed(2)}` });
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();
@@ -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` Current algorithm in use
235
- - `input` Current temperature input value
236
- - `isHeating` Current heating mode
237
- - `above/below` Current output states
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` If mode recently changed
240
- - `cyclesSinceModeChange` Count since last mode change
241
- - `effectiveAnticipator` Current anticipator value after mode change adjustments
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`, delays turn-off to overshoot setpoint, lengthen runtime, or straddle setpoint.
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. Positive values stop early to prevent overshoot (subtracts for heating, adds for cooling);
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 due to latent heat effects.
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 update
303
- - Blue (dot): State changed
304
- - Blue (ring): State unchanged
305
- - Red (ring): Error
306
- - Yellow (ring): Warning
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/)
@@ -490,7 +490,6 @@ module.exports = function(RED) {
490
490
  });
491
491
 
492
492
  node.on("close", function(done) {
493
- node.status({});
494
493
  done();
495
494
  });
496
495
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [ "node-red", "sedona", "control", "hvac" ],
6
6
  "files": ["nodes/*.js", "nodes/*.html"],