@bldgblocks/node-red-contrib-control 0.1.27 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/nodes/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/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 +4 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="rate-of-change-block">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-sampleSize" title="Number of samples to track (minimum 2)"><i class="fa fa-list-ol"></i> Sample Size</label>
|
|
8
|
+
<input type="number" id="node-input-sampleSize" placeholder="10" min="2" step="1">
|
|
9
|
+
</div>
|
|
10
|
+
<div class="form-row">
|
|
11
|
+
<label for="node-input-units" title="Time units for rate calculation"><i class="fa fa-clock-o"></i> Rate Units</label>
|
|
12
|
+
<select id="node-input-units">
|
|
13
|
+
<option value="seconds">Seconds</option>
|
|
14
|
+
<option value="minutes" selected>Minutes</option>
|
|
15
|
+
<option value="hours">Hours</option>
|
|
16
|
+
</select>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="form-row">
|
|
19
|
+
<label for="node-input-minValid"><i class="fa fa-arrow-down"></i> Minimum Valid Temp</label>
|
|
20
|
+
<input type="text" id="node-input-minValid" placeholder="-40">
|
|
21
|
+
<input type="hidden" id="node-input-minValidType">
|
|
22
|
+
</div>
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<label for="node-input-maxValid"><i class="fa fa-arrow-up"></i> Maximum Valid Temp</label>
|
|
25
|
+
<input type="text" id="node-input-maxValid" placeholder="150">
|
|
26
|
+
<input type="hidden" id="node-input-maxValidType">
|
|
27
|
+
</div>
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/javascript">
|
|
31
|
+
RED.nodes.registerType("rate-of-change-block", {
|
|
32
|
+
category: "control",
|
|
33
|
+
color: "#301934",
|
|
34
|
+
defaults: {
|
|
35
|
+
name: { value: "" },
|
|
36
|
+
sampleSize: {
|
|
37
|
+
value: 10,
|
|
38
|
+
required: true,
|
|
39
|
+
validate: function(v) { return !isNaN(parseInt(v)) && parseInt(v) >= 2; }
|
|
40
|
+
},
|
|
41
|
+
units: { value: "minutes" },
|
|
42
|
+
minValid: { value: -40, required: true },
|
|
43
|
+
minValidType: { value: "num" },
|
|
44
|
+
maxValid: { value: 150, required: true },
|
|
45
|
+
maxValidType: { value: "num" }
|
|
46
|
+
},
|
|
47
|
+
inputs: 1,
|
|
48
|
+
outputs: 1,
|
|
49
|
+
inputLabels: ["input"],
|
|
50
|
+
outputLabels: ["rate"],
|
|
51
|
+
icon: "font-awesome/fa-bar-chart",
|
|
52
|
+
paletteLabel: "rate of change",
|
|
53
|
+
label: function() {
|
|
54
|
+
return this.name ? `${this.name} (${this.sampleSize} samples)` : `RoC (${this.sampleSize})`;
|
|
55
|
+
},
|
|
56
|
+
oneditprepare: function() {
|
|
57
|
+
const node = this;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Initialize typed inputs
|
|
61
|
+
$("#node-input-minValid").typedInput({
|
|
62
|
+
default: "num",
|
|
63
|
+
types: ["num", "msg", "flow", "global"],
|
|
64
|
+
typeField: "#node-input-minValidType"
|
|
65
|
+
}).typedInput("type", node.minValidType || "num").typedInput("value", node.minValid || "-40");
|
|
66
|
+
|
|
67
|
+
$("#node-input-maxValid").typedInput({
|
|
68
|
+
default: "num",
|
|
69
|
+
types: ["num", "msg", "flow", "global"],
|
|
70
|
+
typeField: "#node-input-maxValidType"
|
|
71
|
+
}).typedInput("type", node.maxValidType || "num").typedInput("value", node.maxValid || "150");
|
|
72
|
+
|
|
73
|
+
// Set units dropdown
|
|
74
|
+
$("#node-input-units").val(node.units || "minutes");
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("Error in oneditprepare:", err);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<script type="text/markdown" data-help-name="rate-of-change-block">
|
|
83
|
+
Calculates the rate of temperature change over time for HVAC applications.
|
|
84
|
+
|
|
85
|
+
### Inputs
|
|
86
|
+
: context (string) : Configures reset (`"reset"`), sample size (`"sampleSize"`), or units (`"units"`).
|
|
87
|
+
: payload (number) : Temperature value for rate calculation.
|
|
88
|
+
: timestamp (optional) : Custom timestamp for the reading.
|
|
89
|
+
|
|
90
|
+
### Outputs
|
|
91
|
+
: payload (number | null) : Rate of change in temperature per time unit.
|
|
92
|
+
: samples (number) : Current number of samples in buffer.
|
|
93
|
+
: units (string) : Rate units ("°/s", "°/min", "°/hr").
|
|
94
|
+
: currentValue (number) : Most recent temperature value.
|
|
95
|
+
: timeSpan (number) : Time span of sample buffer in seconds.
|
|
96
|
+
|
|
97
|
+
### Details
|
|
98
|
+
Tracks temperature changes over a rolling window of samples. Calculates rate as (last_value - first_value) / time_difference.
|
|
99
|
+
|
|
100
|
+
Useful for detecting HVAC issues like temperature droop, defrost cycles, or rapid changes.
|
|
101
|
+
|
|
102
|
+
### Configuration
|
|
103
|
+
- Reset via `msg.context = "reset"` with `msg.payload = true`
|
|
104
|
+
- Change sample size via `msg.context = "sampleSize"` with numeric payload
|
|
105
|
+
- Change units via `msg.context = "units"` with "seconds", "minutes", or "hours"
|
|
106
|
+
|
|
107
|
+
### Status
|
|
108
|
+
- Shows current rate with units
|
|
109
|
+
- Color indicates state change
|
|
110
|
+
</script>
|
|
@@ -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
|
+
};
|