@bldgblocks/node-red-contrib-control 0.1.27 → 0.1.29
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 +1 -1
- package/nodes/average-block.js +1 -1
- 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 +24 -8
- package/nodes/delay-block.html +1 -1
- package/nodes/frequency-block.html +3 -1
- package/nodes/frequency-block.js +64 -7
- package/nodes/global-getter.html +96 -0
- package/nodes/global-getter.js +42 -0
- package/nodes/global-setter.html +72 -0
- package/nodes/global-setter.js +43 -0
- package/nodes/hysteresis-block.js +1 -1
- package/nodes/latch-block.html +55 -0
- package/nodes/latch-block.js +77 -0
- package/nodes/on-change-block.html +0 -1
- package/nodes/on-change-block.js +2 -12
- package/nodes/pid-block.html +102 -80
- package/nodes/pid-block.js +120 -110
- 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 +66 -30
- package/package.json +6 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
|
|
4
|
+
function RateOfChangeNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
// Initialize runtime state
|
|
9
|
+
node.runtime = {
|
|
10
|
+
maxSamples: parseInt(config.sampleSize),
|
|
11
|
+
samples: [], // Array of {timestamp: Date, value: number}
|
|
12
|
+
units: config.units || "minutes", // minutes, seconds, hours
|
|
13
|
+
lastRate: null
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Evaluate typed-input properties
|
|
17
|
+
try {
|
|
18
|
+
node.runtime.minValid = parseFloat(RED.util.evaluateNodeProperty( config.minValid, config.minValidType, node ));
|
|
19
|
+
node.runtime.maxValid = parseFloat(RED.util.evaluateNodeProperty( config.maxValid, config.maxValidType, node ));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
node.error(`Error evaluating properties: ${err.message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
node.on("input", function(msg, send, done) {
|
|
25
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
26
|
+
|
|
27
|
+
// Guard against invalid msg
|
|
28
|
+
if (!msg) {
|
|
29
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
30
|
+
if (done) done();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Update typed-input properties if needed
|
|
35
|
+
try {
|
|
36
|
+
if (utils.requiresEvaluation(config.minValidType)) {
|
|
37
|
+
node.runtime.minValid = parseFloat(RED.util.evaluateNodeProperty( config.minValid, config.minValidType, node, msg ));
|
|
38
|
+
}
|
|
39
|
+
if (utils.requiresEvaluation(config.maxValidType)) {
|
|
40
|
+
node.runtime.maxValid = parseFloat(RED.util.evaluateNodeProperty( config.maxValid, config.maxValidType, node, msg ));
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
node.error(`Error evaluating properties: ${err.message}`);
|
|
44
|
+
if (done) done();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Acceptable fallbacks
|
|
49
|
+
if (isNaN(node.runtime.maxSamples) || node.runtime.maxSamples < 2) {
|
|
50
|
+
node.runtime.maxSamples = 10;
|
|
51
|
+
node.status({ fill: "red", shape: "ring", text: "invalid sample size, using 10" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate values
|
|
55
|
+
if (isNaN(node.runtime.maxValid) || isNaN(node.runtime.minValid) || node.runtime.maxValid <= node.runtime.minValid ) {
|
|
56
|
+
node.status({ fill: "red", shape: "ring", text: `invalid evaluated values ${node.runtime.minValid}, ${node.runtime.maxValid}` });
|
|
57
|
+
if (done) done();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle configuration messages
|
|
62
|
+
if (msg.hasOwnProperty("context")) {
|
|
63
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
64
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
65
|
+
if (done) done();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (msg.context) {
|
|
70
|
+
case "reset":
|
|
71
|
+
if (typeof msg.payload !== "boolean") {
|
|
72
|
+
node.status({ fill: "red", shape: "ring", text: "invalid reset" });
|
|
73
|
+
if (done) done();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (msg.payload === true) {
|
|
77
|
+
node.runtime.samples = [];
|
|
78
|
+
node.runtime.lastRate = null;
|
|
79
|
+
node.status({ fill: "green", shape: "dot", text: "state reset" });
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
|
|
83
|
+
case "sampleSize":
|
|
84
|
+
let newMaxSamples = parseInt(msg.payload);
|
|
85
|
+
if (isNaN(newMaxSamples) || newMaxSamples < 2) {
|
|
86
|
+
node.status({ fill: "red", shape: "ring", text: "sample size must be at least 2" });
|
|
87
|
+
if (done) done();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
node.runtime.maxSamples = newMaxSamples;
|
|
91
|
+
// Trim samples if new window is smaller
|
|
92
|
+
if (node.runtime.samples.length > newMaxSamples) {
|
|
93
|
+
node.runtime.samples = node.runtime.samples.slice(-newMaxSamples);
|
|
94
|
+
}
|
|
95
|
+
node.status({ fill: "green", shape: "dot", text: `samples: ${newMaxSamples}` });
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case "units":
|
|
99
|
+
const validUnits = ["seconds", "minutes", "hours"];
|
|
100
|
+
if (typeof msg.payload === "string" && validUnits.includes(msg.payload.toLowerCase())) {
|
|
101
|
+
node.runtime.units = msg.payload.toLowerCase();
|
|
102
|
+
node.status({ fill: "green", shape: "dot", text: `units: ${msg.payload}` });
|
|
103
|
+
} else {
|
|
104
|
+
node.status({ fill: "red", shape: "ring", text: "invalid units" });
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
default:
|
|
109
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (done) done();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for missing payload
|
|
117
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
118
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
119
|
+
if (done) done();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Process input
|
|
124
|
+
const inputValue = parseFloat(msg.payload);
|
|
125
|
+
const timestamp = msg.timestamp ? new Date(msg.timestamp) : new Date();
|
|
126
|
+
|
|
127
|
+
if (isNaN(inputValue) || inputValue < node.runtime.minValid || inputValue > node.runtime.maxValid) {
|
|
128
|
+
node.status({ fill: "yellow", shape: "ring", text: "out of range" });
|
|
129
|
+
if (done) done();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add new sample
|
|
134
|
+
node.runtime.samples.push({ timestamp: timestamp, value: inputValue });
|
|
135
|
+
|
|
136
|
+
// Maintain sample window
|
|
137
|
+
if (node.runtime.samples.length > node.runtime.maxSamples + 1) {
|
|
138
|
+
node.runtime.samples = node.runtime.samples.slice(-node.runtime.maxSamples);
|
|
139
|
+
} else if (node.runtime.samples.length > node.runtime.maxSamples) {
|
|
140
|
+
node.runtime.samples.shift();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Calculate rate of change (temperature per time unit)
|
|
144
|
+
let rate = null;
|
|
145
|
+
if (node.runtime.samples.length >= node.runtime.maxSamples) { // Need at least 3 points for good regression
|
|
146
|
+
const n = node.runtime.samples.length;
|
|
147
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
|
|
148
|
+
|
|
149
|
+
// Convert timestamps to relative time in the selected units
|
|
150
|
+
const baseTime = node.runtime.samples[0].timestamp;
|
|
151
|
+
let timeScale; // Conversion factor from ms to selected units
|
|
152
|
+
|
|
153
|
+
switch (node.runtime.units) {
|
|
154
|
+
case "seconds":
|
|
155
|
+
timeScale = 1000; // ms to seconds
|
|
156
|
+
break;
|
|
157
|
+
case "minutes":
|
|
158
|
+
timeScale = 1000 * 60; // ms to minutes
|
|
159
|
+
break;
|
|
160
|
+
case "hours":
|
|
161
|
+
timeScale = 1000 * 60 * 60; // ms to hours
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
timeScale = 1000 * 60; // default to minutes
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Calculate regression sums
|
|
168
|
+
node.runtime.samples.forEach((sample, i) => {
|
|
169
|
+
const x = (sample.timestamp - baseTime) / timeScale; // time in selected units
|
|
170
|
+
const y = sample.value;
|
|
171
|
+
|
|
172
|
+
sumX += x;
|
|
173
|
+
sumY += y;
|
|
174
|
+
sumXY += x * y;
|
|
175
|
+
sumXX += x * x;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Calculate slope (rate of change) using linear regression formula
|
|
179
|
+
const denominator = n * sumXX - sumX * sumX;
|
|
180
|
+
|
|
181
|
+
// Avoid division by zero - use original endpoint method if regression fails
|
|
182
|
+
if (Math.abs(denominator) > 1e-10) { // Small tolerance for floating point
|
|
183
|
+
rate = (n * sumXY - sumX * sumY) / denominator;
|
|
184
|
+
} else {
|
|
185
|
+
// Fallback to original endpoint method if regression is unstable
|
|
186
|
+
const firstSample = node.runtime.samples[0];
|
|
187
|
+
const lastSample = node.runtime.samples[node.runtime.samples.length - 1];
|
|
188
|
+
const timeDiff = (lastSample.timestamp - firstSample.timestamp) / timeScale;
|
|
189
|
+
rate = timeDiff > 0 ? (lastSample.value - firstSample.value) / timeDiff : 0;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const isUnchanged = rate === node.runtime.lastRate;
|
|
194
|
+
|
|
195
|
+
// Send new message
|
|
196
|
+
const unitsDisplay = {
|
|
197
|
+
seconds: "/sec",
|
|
198
|
+
minutes: "/min",
|
|
199
|
+
hours: "/hr"
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
node.status({
|
|
203
|
+
fill: "blue",
|
|
204
|
+
shape: isUnchanged ? "ring" : "dot",
|
|
205
|
+
text: `rate: ${rate !== null ? rate.toFixed(2) : "not ready"} ${unitsDisplay[node.runtime.units] || "/min"}`
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
node.runtime.lastRate = rate;
|
|
209
|
+
|
|
210
|
+
// Enhanced output with metadata
|
|
211
|
+
const outputMsg = {
|
|
212
|
+
payload: rate,
|
|
213
|
+
samples: node.runtime.samples.length,
|
|
214
|
+
units: `${unitsDisplay[node.runtime.units] || "/min"}`,
|
|
215
|
+
currentValue: inputValue,
|
|
216
|
+
timeSpan: node.runtime.samples.length >= 2 ?
|
|
217
|
+
(node.runtime.samples[node.runtime.samples.length - 1].timestamp - node.runtime.samples[0].timestamp) / 1000 : 0
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (node.runtime.samples.length >= node.runtime.maxSamples) {
|
|
221
|
+
send(outputMsg);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (done) done();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
node.on("close", function(done) {
|
|
228
|
+
done();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
RED.nodes.registerType("rate-of-change-block", RateOfChangeNode);
|
|
233
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="string-builder-block">
|
|
3
|
+
<div class="form-row">
|
|
4
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
5
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
6
|
+
</div>
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-in1"><i class="fa fa-bars"></i> in1</label>
|
|
9
|
+
<input type="text" id="node-input-in1" placeholder="">
|
|
10
|
+
<input type="hidden" id="node-input-in1Type">
|
|
11
|
+
</div>
|
|
12
|
+
<div class="form-row">
|
|
13
|
+
<label for="node-input-in2"><i class="fa fa-bars"></i> in2</label>
|
|
14
|
+
<input type="text" id="node-input-in2" placeholder="">
|
|
15
|
+
<input type="hidden" id="node-input-in2Type">
|
|
16
|
+
</div>
|
|
17
|
+
<div class="form-row">
|
|
18
|
+
<label for="node-input-in3"><i class="fa fa-bars"></i> in3</label>
|
|
19
|
+
<input type="text" id="node-input-in3" placeholder="">
|
|
20
|
+
<input type="hidden" id="node-input-in3Type">
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-row">
|
|
23
|
+
<label for="node-input-in4"><i class="fa fa-bars"></i> in4</label>
|
|
24
|
+
<input type="text" id="node-input-in4" placeholder="">
|
|
25
|
+
<input type="hidden" id="node-input-in4Type">
|
|
26
|
+
</div>
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
31
|
+
<script type="text/javascript">
|
|
32
|
+
RED.nodes.registerType("string-builder-block", {
|
|
33
|
+
category: "control",
|
|
34
|
+
color: "#301934",
|
|
35
|
+
defaults: {
|
|
36
|
+
name: { value: "" },
|
|
37
|
+
in1: { value: "", required: false },
|
|
38
|
+
in1Type: { value: "str" },
|
|
39
|
+
in2: { value: "", required: false },
|
|
40
|
+
in2Type: { value: "str" },
|
|
41
|
+
in3: { value: "", required: false },
|
|
42
|
+
in3Type: { value: "str" },
|
|
43
|
+
in4: { value: "", required: false },
|
|
44
|
+
in4Type: { value: "str" },
|
|
45
|
+
},
|
|
46
|
+
inputs: 1,
|
|
47
|
+
outputs: 1,
|
|
48
|
+
inputLabels: ["input"],
|
|
49
|
+
outputLabels: ["output"],
|
|
50
|
+
icon: "font-awesome/fa-bars",
|
|
51
|
+
paletteLabel: "string builder",
|
|
52
|
+
label: function() {
|
|
53
|
+
return this.name || "string builder";
|
|
54
|
+
},
|
|
55
|
+
oneditprepare: function() {
|
|
56
|
+
const node = this;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Initialize typed inputs
|
|
60
|
+
$("#node-input-in1").typedInput({
|
|
61
|
+
default: "str",
|
|
62
|
+
types: ["str", "msg", "flow", "global"],
|
|
63
|
+
typeField: "#node-input-in1Type"
|
|
64
|
+
}).typedInput("type", node.in1Type || "str").typedInput("value", node.in1);
|
|
65
|
+
|
|
66
|
+
$("#node-input-in2").typedInput({
|
|
67
|
+
default: "str",
|
|
68
|
+
types: ["str", "msg", "flow", "global"],
|
|
69
|
+
typeField: "#node-input-in2Type"
|
|
70
|
+
}).typedInput("type", node.in2Type || "str").typedInput("value", node.in2);
|
|
71
|
+
|
|
72
|
+
$("#node-input-in3").typedInput({
|
|
73
|
+
default: "str",
|
|
74
|
+
types: ["str", "msg", "flow", "global"],
|
|
75
|
+
typeField: "#node-input-in3Type"
|
|
76
|
+
}).typedInput("type", node.in3Type || "str").typedInput("value", node.in3);
|
|
77
|
+
|
|
78
|
+
$("#node-input-in4").typedInput({
|
|
79
|
+
default: "str",
|
|
80
|
+
types: ["str", "msg", "flow", "global"],
|
|
81
|
+
typeField: "#node-input-in4Type"
|
|
82
|
+
}).typedInput("type", node.in4Type || "str").typedInput("value", node.in4);
|
|
83
|
+
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error("Error in oneditprepare:", err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<!-- Help Section -->
|
|
92
|
+
<script type="text/markdown" data-help-name="string-builder-block">
|
|
93
|
+
Concatenates multiple string inputs into a single output string on any input.
|
|
94
|
+
|
|
95
|
+
### Inputs
|
|
96
|
+
: payload (number) : Input value to evaluate
|
|
97
|
+
: context (string) : Configure `inX` string inputs via context variables
|
|
98
|
+
|
|
99
|
+
### Outputs
|
|
100
|
+
: output (string) : Each string input concatenated in order
|
|
101
|
+
|
|
102
|
+
### Status
|
|
103
|
+
- Green (dot): Configuration update
|
|
104
|
+
- Blue (dot): State changed
|
|
105
|
+
- Blue (ring): State unchanged
|
|
106
|
+
- Red (ring): Error
|
|
107
|
+
- Yellow (ring): Warning
|
|
108
|
+
|
|
109
|
+
### References
|
|
110
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
111
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
112
|
+
</script>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
|
|
4
|
+
function StringBuilderBlockNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
node.name = config.name;
|
|
8
|
+
|
|
9
|
+
// Evaluate typed-input properties
|
|
10
|
+
try {
|
|
11
|
+
node.in1 = RED.util.evaluateNodeProperty( config.in1, config.in1Type, node );
|
|
12
|
+
node.in2 = RED.util.evaluateNodeProperty( config.in2, config.in2Type, node );
|
|
13
|
+
node.in3 = RED.util.evaluateNodeProperty( config.in3, config.in3Type, node );
|
|
14
|
+
node.in4 = RED.util.evaluateNodeProperty( config.in4, config.in4Type, node );
|
|
15
|
+
} catch (err) {
|
|
16
|
+
node.error(`Error evaluating properties: ${err.message}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
node.on("input", function(msg, send, done) {
|
|
20
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
21
|
+
|
|
22
|
+
if (!msg) {
|
|
23
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
24
|
+
if (done) done();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Update typed-input properties if needed
|
|
29
|
+
try {
|
|
30
|
+
if (utils.requiresEvaluation(config.in1Type)) {
|
|
31
|
+
node.in1 = RED.util.evaluateNodeProperty( config.in1, config.in1Type, node, msg );
|
|
32
|
+
}
|
|
33
|
+
if (utils.requiresEvaluation(config.in2Type)) {
|
|
34
|
+
node.in2 = RED.util.evaluateNodeProperty( config.in2, config.in2Type, node, msg );
|
|
35
|
+
}
|
|
36
|
+
if (utils.requiresEvaluation(config.in3Type)) {
|
|
37
|
+
node.in3 = RED.util.evaluateNodeProperty( config.in3, config.in3Type, node, msg );
|
|
38
|
+
}
|
|
39
|
+
if (utils.requiresEvaluation(config.in4Type)) {
|
|
40
|
+
node.in4 = RED.util.evaluateNodeProperty( config.in4, config.in4Type, node, msg );
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
node.error(`Error evaluating properties: ${err.message}`);
|
|
44
|
+
if (done) done();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check required properties
|
|
49
|
+
if (msg.hasOwnProperty("context")) {
|
|
50
|
+
|
|
51
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
52
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
53
|
+
if (done) done();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Process input slot
|
|
58
|
+
if (msg.context.startsWith("in")) {
|
|
59
|
+
let index = parseInt(msg.context.slice(2), 10);
|
|
60
|
+
if (!isNaN(index) && index >= 1 && index <= 4) {
|
|
61
|
+
if (config[`in${index}Type`] === "str") {
|
|
62
|
+
node[`in${index}`] = msg.payload;
|
|
63
|
+
} else {
|
|
64
|
+
node.status({ fill: "red", shape: "ring", text: `Field type is ${config[`in${index}Type`]}` });
|
|
65
|
+
if (done) done();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
node.status({ fill: "red", shape: "ring", text: `invalid input index ${index || "NaN"}` });
|
|
70
|
+
if (done) done();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const output = { payload: `${node.in1}${node.in2}${node.in3}${node.in4}` };
|
|
77
|
+
node.status({ fill: "blue", shape: "dot", text: `${ output.payload }` });
|
|
78
|
+
send(output);
|
|
79
|
+
|
|
80
|
+
if (done) done();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
node.on("close", function(done) {
|
|
84
|
+
done();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
RED.nodes.registerType("string-builder-block", StringBuilderBlockNode);
|
|
89
|
+
};
|
package/nodes/tstat-block.html
CHANGED
|
@@ -5,15 +5,12 @@
|
|
|
5
5
|
</div>
|
|
6
6
|
<div class="form-row">
|
|
7
7
|
<label for="node-input-algorithm" title="Algorithm: single setpoint with diff, split setpoints, or specified setpoints"><i class="fa fa-cog"></i> Algorithm</label>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
<option value="split">Split Setpoint</option>
|
|
11
|
-
<option value="specified">Specified Setpoint</option>
|
|
12
|
-
</select>
|
|
8
|
+
<input type="text" id="node-input-algorithm" placeholder="single">
|
|
9
|
+
<input type="hidden" id="node-input-algorithmType">
|
|
13
10
|
</div>
|
|
14
11
|
<div class="form-row single-setpoint">
|
|
15
12
|
<label for="node-input-setpoint" title="Target temperature setpoint (number from num, msg, flow, or global)"><i class="fa fa-crosshairs"></i> Setpoint</label>
|
|
16
|
-
<input type="text" id="node-input-setpoint"
|
|
13
|
+
<input type="text" id="node-input-setpoint" placeholder="70">
|
|
17
14
|
<input type="hidden" id="node-input-setpointType">
|
|
18
15
|
</div>
|
|
19
16
|
<div class="form-row split-setpoint" style="display: none;">
|
|
@@ -63,7 +60,8 @@
|
|
|
63
60
|
</div>
|
|
64
61
|
<div class="form-row">
|
|
65
62
|
<label for="node-input-isHeating" title="Heating mode (true) or cooling mode (false)"><i class="fa fa-fire"></i> Heating Mode</label>
|
|
66
|
-
<input type="
|
|
63
|
+
<input type="text" id="node-input-isHeating" style="width: auto; vertical-align: middle;">
|
|
64
|
+
<input type="hidden" id="node-input-isHeatingType">
|
|
67
65
|
</div>
|
|
68
66
|
</script>
|
|
69
67
|
|
|
@@ -74,6 +72,7 @@
|
|
|
74
72
|
defaults: {
|
|
75
73
|
name: { value: "" },
|
|
76
74
|
algorithm: { value: "single" },
|
|
75
|
+
algorithmType: { value: "dropdown" },
|
|
77
76
|
setpoint: { value: "70" },
|
|
78
77
|
setpointType: { value: "num" },
|
|
79
78
|
heatingSetpoint: { value: "68" },
|
|
@@ -94,7 +93,8 @@
|
|
|
94
93
|
anticipatorType: { value: "num" },
|
|
95
94
|
ignoreAnticipatorCycles: { value: "1" },
|
|
96
95
|
ignoreAnticipatorCyclesType: { value: "num" },
|
|
97
|
-
isHeating: { value: false }
|
|
96
|
+
isHeating: { value: false },
|
|
97
|
+
isHeatingType: { value: "bool" }
|
|
98
98
|
},
|
|
99
99
|
inputs: 1,
|
|
100
100
|
outputs: 3,
|
|
@@ -112,41 +112,85 @@
|
|
|
112
112
|
const $singleFields = $(".single-setpoint");
|
|
113
113
|
const $splitFields = $(".split-setpoint");
|
|
114
114
|
const $specifiedFields = $(".specified-setpoint");
|
|
115
|
+
|
|
116
|
+
$("#node-input-algorithm").typedInput({
|
|
117
|
+
default: "dropdown",
|
|
118
|
+
types: [{
|
|
119
|
+
value: "dropdown",
|
|
120
|
+
options: [
|
|
121
|
+
{ value: "single", label: "Single"},
|
|
122
|
+
{ value: "split", label: "Split"},
|
|
123
|
+
{ value: "specified", label: "Specified"},
|
|
124
|
+
]
|
|
125
|
+
}, "msg", "flow", "global"],
|
|
126
|
+
typeField: "#node-input-algorithmType"
|
|
127
|
+
}).typedInput("type", node.algorithmType).typedInput("value", node.algorithm);
|
|
115
128
|
|
|
116
|
-
$("#node-input-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
$("#node-input-setpoint").typedInput({
|
|
130
|
+
default: "num",
|
|
131
|
+
types: ["num", "msg", "flow", "global"],
|
|
132
|
+
typeField: "#node-input-setpointType"
|
|
133
|
+
}).typedInput("type", node.setpointType || "num").typedInput("value", node.setpoint);
|
|
119
134
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{ id: "node-input-coolingOff", typeId: "node-input-coolingOffType", defaultValue: "72", defaultType: "num" },
|
|
126
|
-
{ id: "node-input-heatingOff", typeId: "node-input-heatingOffType", defaultValue: "68", defaultType: "num" },
|
|
127
|
-
{ id: "node-input-heatingOn", typeId: "node-input-heatingOnType", defaultValue: "66", defaultType: "num" },
|
|
128
|
-
{ id: "node-input-diff", typeId: "node-input-diffType", defaultValue: "2", defaultType: "num" },
|
|
129
|
-
{ id: "node-input-anticipator", typeId: "node-input-anticipatorType", defaultValue: "0.5", defaultType: "num" },
|
|
130
|
-
{ id: "node-input-ignoreAnticipatorCycles", typeId: "node-input-ignoreAnticipatorCyclesType", defaultValue: "1", defaultType: "num" }
|
|
131
|
-
];
|
|
135
|
+
$("#node-input-heatingSetpoint").typedInput({
|
|
136
|
+
default: "num",
|
|
137
|
+
types: ["num", "msg", "flow", "global"],
|
|
138
|
+
typeField: "#node-input-heatingSetpointType"
|
|
139
|
+
}).typedInput("type", node.heatingSetpointType || "num").typedInput("value", node.heatingSetpoint);
|
|
132
140
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
$("#node-input-coolingSetpoint").typedInput({
|
|
142
|
+
default: "num",
|
|
143
|
+
types: ["num", "msg", "flow", "global"],
|
|
144
|
+
typeField: "#node-input-coolingSetpointType"
|
|
145
|
+
}).typedInput("type", node.coolingSetpointType || "num").typedInput("value", node.coolingSetpoint);
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
$("#node-input-coolingOn").typedInput({
|
|
148
|
+
default: "num",
|
|
149
|
+
types: ["num", "msg", "flow", "global"],
|
|
150
|
+
typeField: "#node-input-coolingOnType"
|
|
151
|
+
}).typedInput("type", node.coolingOnType || "num").typedInput("value", node.coolingOn);
|
|
152
|
+
|
|
153
|
+
$("#node-input-coolingOff").typedInput({
|
|
154
|
+
default: "num",
|
|
155
|
+
types: ["num", "msg", "flow", "global"],
|
|
156
|
+
typeField: "#node-input-coolingOffType"
|
|
157
|
+
}).typedInput("type", node.coolingOffType || "num").typedInput("value", node.coolingOff);
|
|
158
|
+
|
|
159
|
+
$("#node-input-heatingOff").typedInput({
|
|
160
|
+
default: "num",
|
|
161
|
+
types: ["num", "msg", "flow", "global"],
|
|
162
|
+
typeField: "#node-input-heatingOffType"
|
|
163
|
+
}).typedInput("type", node.heatingOffType || "num").typedInput("value", node.heatingOff);
|
|
149
164
|
|
|
165
|
+
$("#node-input-heatingOn").typedInput({
|
|
166
|
+
default: "num",
|
|
167
|
+
types: ["num", "msg", "flow", "global"],
|
|
168
|
+
typeField: "#node-input-heatingOnType"
|
|
169
|
+
}).typedInput("type", node.heatingOnType || "num").typedInput("value", node.heatingOn);
|
|
170
|
+
|
|
171
|
+
$("#node-input-diff").typedInput({
|
|
172
|
+
default: "num",
|
|
173
|
+
types: ["num", "msg", "flow", "global"],
|
|
174
|
+
typeField: "#node-input-diffType"
|
|
175
|
+
}).typedInput("type", node.diffType || "num").typedInput("value", node.diff);
|
|
176
|
+
|
|
177
|
+
$("#node-input-anticipator").typedInput({
|
|
178
|
+
default: "num",
|
|
179
|
+
types: ["num", "msg", "flow", "global"],
|
|
180
|
+
typeField: "#node-input-anticipatorType"
|
|
181
|
+
}).typedInput("type", node.anticipatorType || "num").typedInput("value", node.anticipator);
|
|
182
|
+
|
|
183
|
+
$("#node-input-ignoreAnticipatorCycles").typedInput({
|
|
184
|
+
default: "num",
|
|
185
|
+
types: ["num", "msg", "flow", "global"],
|
|
186
|
+
typeField: "#node-input-ignoreAnticipatorCyclesType"
|
|
187
|
+
}).typedInput("type", node.ignoreAnticipatorCyclesType || "num").typedInput("value", node.ignoreAnticipatorCycles);
|
|
188
|
+
|
|
189
|
+
$("#node-input-isHeating").typedInput({
|
|
190
|
+
default: "bool",
|
|
191
|
+
types: ["bool", "msg", "flow", "global"],
|
|
192
|
+
typeField: "#node-input-isHeatingType"
|
|
193
|
+
}).typedInput("type", node.isHeatingType || "bool").typedInput("value", node.isHeating);
|
|
150
194
|
|
|
151
195
|
function toggleFields() {
|
|
152
196
|
const algorithm = $algorithm.val();
|
|
@@ -201,7 +245,7 @@ All output messages include a `msg.status` object containing runtime information
|
|
|
201
245
|
- `effectiveAnticipator`: Current anticipator value after mode change adjustments
|
|
202
246
|
|
|
203
247
|
### Status Monitoring
|
|
204
|
-
|
|
248
|
+
All outputs include comprehensive status information in `msg.status`. Example:
|
|
205
249
|
```json
|
|
206
250
|
{
|
|
207
251
|
"status": {
|
|
@@ -218,6 +262,8 @@ Instead of a dedicated status output, all outputs include comprehensive status i
|
|
|
218
262
|
"effectiveAnticipator": 0.5
|
|
219
263
|
}
|
|
220
264
|
}
|
|
265
|
+
```
|
|
266
|
+
|
|
221
267
|
### Algorithms
|
|
222
268
|
- **Single Setpoint**:
|
|
223
269
|
- Uses `setpoint`, `diff`, and `anticipator`.
|