@bldgblocks/node-red-contrib-control 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/nodes/analog-switch-block.html +8 -9
- package/nodes/analog-switch-block.js +0 -23
- package/nodes/average-block.html +1 -1
- package/nodes/average-block.js +9 -9
- package/nodes/boolean-switch-block.html +2 -1
- package/nodes/boolean-switch-block.js +1 -2
- package/nodes/call-status-block.html +30 -45
- package/nodes/call-status-block.js +81 -195
- package/nodes/changeover-block.html +76 -12
- package/nodes/changeover-block.js +57 -28
- package/nodes/delay-block.html +1 -1
- package/nodes/delay-block.js +8 -8
- package/nodes/enum-switch-block.html +157 -0
- package/nodes/enum-switch-block.js +101 -0
- package/nodes/hysteresis-block.html +1 -1
- package/nodes/hysteresis-block.js +17 -13
- package/nodes/latch-block.html +55 -0
- package/nodes/latch-block.js +77 -0
- package/nodes/max-block.js +2 -4
- package/nodes/memory-block.js +3 -6
- package/nodes/min-block.js +2 -4
- package/nodes/minmax-block.js +9 -9
- package/nodes/on-change-block.html +0 -1
- package/nodes/on-change-block.js +4 -16
- package/nodes/pid-block.html +102 -80
- package/nodes/pid-block.js +121 -111
- package/nodes/rate-of-change-block.html +110 -0
- package/nodes/rate-of-change-block.js +233 -0
- package/nodes/string-builder-block.html +112 -0
- package/nodes/string-builder-block.js +89 -0
- package/nodes/tstat-block.html +85 -39
- package/nodes/tstat-block.js +105 -56
- package/nodes/utils.js +1 -22
- package/package.json +5 -1
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
</div>
|
|
6
6
|
<div class="form-row">
|
|
7
7
|
<label for="node-input-algorithm" title="Control algorithm type"><i class="fa fa-cog"></i> Algorithm</label>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
<option value="split">Split Setpoints</option>
|
|
11
|
-
</select>
|
|
8
|
+
<input type="text" id="node-input-algorithm" class="node-input-typed" placeholder="single">
|
|
9
|
+
<input type="hidden" id="node-input-algorithmType">
|
|
12
10
|
</div>
|
|
13
11
|
<div class="form-row single-only">
|
|
14
12
|
<label for="node-input-setpoint" title="Target temperature setpoint (number from num, msg, flow, or global)"><i class="fa fa-thermometer-half"></i> Setpoint</label>
|
|
@@ -50,6 +48,18 @@
|
|
|
50
48
|
Invalid
|
|
51
49
|
</div>
|
|
52
50
|
</div>
|
|
51
|
+
|
|
52
|
+
<div class="form-row specified-only" style="display: none;">
|
|
53
|
+
<label for="node-input-coolingOn" title="Temperature to turn cooling on (number from num, msg, flow, or global)"><i class="fa fa-snowflake-o"></i> Cooling On</label>
|
|
54
|
+
<input type="text" id="node-input-coolingOn" placeholder="74">
|
|
55
|
+
<input type="hidden" id="node-input-coolingOnType">
|
|
56
|
+
</div>
|
|
57
|
+
<div class="form-row specified-only" style="display: none;">
|
|
58
|
+
<label for="node-input-heatingOn" title="Temperature to turn heating on (number from num, msg, flow, or global)"><i class="fa fa-fire"></i> Heating On</label>
|
|
59
|
+
<input type="text" id="node-input-heatingOn" placeholder="66">
|
|
60
|
+
<input type="hidden" id="node-input-heatingOnType">
|
|
61
|
+
</div>
|
|
62
|
+
|
|
53
63
|
<div class="form-row">
|
|
54
64
|
<label for="node-input-swapTime" title="Minimum time before mode change (seconds, minimum 60, from num, msg, flow, or global)"><i class="fa fa-clock-o"></i> Swap Time</label>
|
|
55
65
|
<input type="text" id="node-input-swapTime" class="node-input-typed" placeholder="300">
|
|
@@ -76,11 +86,8 @@
|
|
|
76
86
|
</div>
|
|
77
87
|
<div class="form-row">
|
|
78
88
|
<label for="node-input-operationMode" title="Operation mode"><i class="fa fa-cogs"></i> Operation Mode</label>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
<option value="heat">Heat</option>
|
|
82
|
-
<option value="cool">Cool</option>
|
|
83
|
-
</select>
|
|
89
|
+
<input type="text" id="node-input-operationMode" placeholder="auto">
|
|
90
|
+
<input type="hidden" id="node-input-operationModeType">
|
|
84
91
|
</div>
|
|
85
92
|
</script>
|
|
86
93
|
|
|
@@ -91,6 +98,7 @@
|
|
|
91
98
|
defaults: {
|
|
92
99
|
name: { value: "" },
|
|
93
100
|
algorithm: { value: "single" },
|
|
101
|
+
algorithmType: { value: "dropdown" },
|
|
94
102
|
setpoint: { value: "70" },
|
|
95
103
|
setpointType: { value: "num" },
|
|
96
104
|
deadband: { value: "2" },
|
|
@@ -98,6 +106,10 @@
|
|
|
98
106
|
heatingSetpointType: { value: "num" },
|
|
99
107
|
coolingSetpoint: { value: "74" },
|
|
100
108
|
coolingSetpointType: { value: "num" },
|
|
109
|
+
coolingOn: { value: "74" },
|
|
110
|
+
coolingOnType: { value: "num" },
|
|
111
|
+
heatingOn: { value: "66" },
|
|
112
|
+
heatingOnType: { value: "num" },
|
|
101
113
|
extent: { value: "1" },
|
|
102
114
|
swapTime: { value: "300" },
|
|
103
115
|
swapTimeType: { value: "num" },
|
|
@@ -106,7 +118,8 @@
|
|
|
106
118
|
maxTempSetpoint: { value: "90" },
|
|
107
119
|
maxTempSetpointType: { value: "num"},
|
|
108
120
|
initWindow: { value: "10" },
|
|
109
|
-
operationMode: { value: "auto" }
|
|
121
|
+
operationMode: { value: "auto" },
|
|
122
|
+
operationModeType: { value: "dropdown" },
|
|
110
123
|
},
|
|
111
124
|
inputs: 1,
|
|
112
125
|
outputs: 1,
|
|
@@ -122,9 +135,37 @@
|
|
|
122
135
|
const $algorithm = $("#node-input-algorithm");
|
|
123
136
|
const $singleFields = $(".single-only");
|
|
124
137
|
const $splitFields = $(".split-only");
|
|
138
|
+
const $specifiedFields = $(".specified-only");
|
|
125
139
|
|
|
126
140
|
try {
|
|
127
141
|
// Initialize typed inputs
|
|
142
|
+
|
|
143
|
+
$("#node-input-operationMode").typedInput({
|
|
144
|
+
default: "dropdown",
|
|
145
|
+
types: [{
|
|
146
|
+
value: "dropdown",
|
|
147
|
+
options: [
|
|
148
|
+
{ value: "auto", label: "Auto"},
|
|
149
|
+
{ value: "heat", label: "Heat"},
|
|
150
|
+
{ value: "cool", label: "Cool"},
|
|
151
|
+
]
|
|
152
|
+
}, "msg", "flow", "global"],
|
|
153
|
+
typeField: "#node-input-operationModeType"
|
|
154
|
+
}).typedInput("type", node.operationModeType).typedInput("value", node.operationMode);
|
|
155
|
+
|
|
156
|
+
$("#node-input-algorithm").typedInput({
|
|
157
|
+
default: "dropdown",
|
|
158
|
+
types: [{
|
|
159
|
+
value: "dropdown",
|
|
160
|
+
options: [
|
|
161
|
+
{ value: "single", label: "Single"},
|
|
162
|
+
{ value: "split", label: "Split"},
|
|
163
|
+
{ value: "specified", label: "Specified"},
|
|
164
|
+
]
|
|
165
|
+
}, "msg", "flow", "global"],
|
|
166
|
+
typeField: "#node-input-algorithmType"
|
|
167
|
+
}).typedInput("type", node.algorithmType).typedInput("value", node.algorithm);
|
|
168
|
+
|
|
128
169
|
$("#node-input-setpoint").typedInput({
|
|
129
170
|
default: "num",
|
|
130
171
|
types: ["num", "msg", "flow", "global"],
|
|
@@ -143,6 +184,18 @@
|
|
|
143
184
|
typeField: "#node-input-coolingSetpointType"
|
|
144
185
|
}).typedInput("type", node.coolingSetpointType || "num").typedInput("value", node.coolingSetpoint);
|
|
145
186
|
|
|
187
|
+
$("#node-input-heatingOn").typedInput({
|
|
188
|
+
default: "num",
|
|
189
|
+
types: ["num", "msg", "flow", "global"],
|
|
190
|
+
typeField: "#node-input-heatingOnType"
|
|
191
|
+
}).typedInput("type", node.heatingOnType || "num").typedInput("value", node.heatingOn);
|
|
192
|
+
|
|
193
|
+
$("#node-input-coolingOn").typedInput({
|
|
194
|
+
default: "num",
|
|
195
|
+
types: ["num", "msg", "flow", "global"],
|
|
196
|
+
typeField: "#node-input-coolingOnType"
|
|
197
|
+
}).typedInput("type", node.coolingOnType || "num").typedInput("value", node.coolingOn);
|
|
198
|
+
|
|
146
199
|
$("#node-input-swapTime").typedInput({
|
|
147
200
|
default: "num",
|
|
148
201
|
types: ["num", "msg", "flow", "global"],
|
|
@@ -178,9 +231,20 @@
|
|
|
178
231
|
if ($algorithm.val() === "single") {
|
|
179
232
|
$singleFields.show();
|
|
180
233
|
$splitFields.hide();
|
|
181
|
-
|
|
234
|
+
$specifiedFields.hide();
|
|
235
|
+
} else if ($algorithm.val() === "split") {
|
|
182
236
|
$singleFields.hide();
|
|
183
237
|
$splitFields.show();
|
|
238
|
+
$specifiedFields.hide();
|
|
239
|
+
} else if ($algorithm.val() === "specified") {
|
|
240
|
+
$singleFields.hide();
|
|
241
|
+
$splitFields.hide();
|
|
242
|
+
$specifiedFields.show();
|
|
243
|
+
} else {
|
|
244
|
+
$algorithm.val("single");
|
|
245
|
+
$singleFields.show();
|
|
246
|
+
$splitFields.hide();
|
|
247
|
+
$specifiedFields.hide();
|
|
184
248
|
}
|
|
185
249
|
}
|
|
186
250
|
|
|
@@ -188,7 +252,7 @@
|
|
|
188
252
|
toggleFields();
|
|
189
253
|
|
|
190
254
|
} catch (err) {
|
|
191
|
-
console.error("Error in
|
|
255
|
+
console.error("Error in oneditprepare:", err);
|
|
192
256
|
}
|
|
193
257
|
}
|
|
194
258
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
//const { parse } = require('echarts/types/src/export/api/time.js');
|
|
2
|
+
|
|
1
3
|
module.exports = function(RED) {
|
|
2
4
|
const utils = require('./utils')(RED);
|
|
3
5
|
|
|
@@ -8,28 +10,24 @@ module.exports = function(RED) {
|
|
|
8
10
|
// Initialize runtime state
|
|
9
11
|
node.runtime = {
|
|
10
12
|
name: config.name,
|
|
11
|
-
algorithm: config.algorithm,
|
|
12
|
-
operationMode: config.operationMode,
|
|
13
13
|
initWindow: parseFloat(config.initWindow),
|
|
14
|
-
currentMode: (config.operationMode === "cool" ? "cooling" : "heating"),
|
|
15
14
|
lastTemperature: null,
|
|
16
15
|
lastModeChange: 0
|
|
17
16
|
};
|
|
18
17
|
|
|
19
|
-
const typedProperties = ['setpoint', 'heatingSetpoint', 'coolingSetpoint', 'swapTime', 'deadband',
|
|
20
|
-
'extent', 'minTempSetpoint', 'maxTempSetpoint'];
|
|
21
|
-
|
|
22
18
|
// Evaluate typed-input properties
|
|
23
|
-
try {
|
|
24
|
-
|
|
25
|
-
node.runtime.
|
|
26
|
-
node.runtime.
|
|
27
|
-
node.runtime.
|
|
28
|
-
node.runtime.
|
|
29
|
-
node.runtime.
|
|
30
|
-
node.runtime.
|
|
31
|
-
node.runtime.
|
|
32
|
-
node.runtime.
|
|
19
|
+
try {
|
|
20
|
+
node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node ));
|
|
21
|
+
node.runtime.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node ));
|
|
22
|
+
node.runtime.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node ));
|
|
23
|
+
node.runtime.swapTime = parseFloat(RED.util.evaluateNodeProperty( config.swapTime, config.swapTimeType, node ));
|
|
24
|
+
node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node ));
|
|
25
|
+
node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node ));
|
|
26
|
+
node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node ));
|
|
27
|
+
node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node ));
|
|
28
|
+
node.runtime.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node );
|
|
29
|
+
node.runtime.operationMode = RED.util.evaluateNodeProperty( config.operationMode, config.operationModeType, node );
|
|
30
|
+
node.runtime.currentMode = node.runtime.operationMode === "cool" ? "cooling" : "heating";
|
|
33
31
|
} catch (err) {
|
|
34
32
|
node.error(`Error evaluating properties: ${err.message}`);
|
|
35
33
|
if (done) done();
|
|
@@ -53,15 +51,37 @@ module.exports = function(RED) {
|
|
|
53
51
|
|
|
54
52
|
// Update typed-input properties if needed
|
|
55
53
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
if (utils.requiresEvaluation(config.setpointType)) {
|
|
55
|
+
node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node, msg ));
|
|
56
|
+
}
|
|
57
|
+
if (utils.requiresEvaluation(config.heatingSetpointType)) {
|
|
58
|
+
node.runtime.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node, msg ));
|
|
59
|
+
}
|
|
60
|
+
if (utils.requiresEvaluation(config.coolingSetpointType)) {
|
|
61
|
+
node.runtime.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node, msg ));
|
|
62
|
+
}
|
|
63
|
+
if (utils.requiresEvaluation(config.swapTimeType)) {
|
|
64
|
+
node.runtime.swapTime = parseFloat(RED.util.evaluateNodeProperty( config.swapTime, config.swapTimeType, node, msg ));
|
|
65
|
+
}
|
|
66
|
+
if (utils.requiresEvaluation(config.deadbandType)) {
|
|
67
|
+
node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node, msg ));
|
|
68
|
+
}
|
|
69
|
+
if (utils.requiresEvaluation(config.extentType)) {
|
|
70
|
+
node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node, msg ));
|
|
71
|
+
}
|
|
72
|
+
if (utils.requiresEvaluation(config.minTempSetpointType)) {
|
|
73
|
+
node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node, msg ));
|
|
74
|
+
}
|
|
75
|
+
if (utils.requiresEvaluation(config.maxTempSetpointType)) {
|
|
76
|
+
node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node, msg ));
|
|
77
|
+
}
|
|
78
|
+
if (utils.requiresEvaluation(config.algorithmType)) {
|
|
79
|
+
node.runtime.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node, msg );
|
|
80
|
+
}
|
|
81
|
+
if (utils.requiresEvaluation(config.operationModeType)) {
|
|
82
|
+
node.runtime.operationMode = RED.util.evaluateNodeProperty( config.operationMode, config.operationModeType, node, msg );
|
|
83
|
+
node.runtime.currentMode = node.runtime.operationMode === "cool" ? "cooling" : "heating";
|
|
84
|
+
}
|
|
65
85
|
} catch (err) {
|
|
66
86
|
node.error(`Error evaluating properties: ${err.message}`);
|
|
67
87
|
if (done) done();
|
|
@@ -264,9 +284,12 @@ module.exports = function(RED) {
|
|
|
264
284
|
if (node.runtime.algorithm === "single") {
|
|
265
285
|
heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
|
|
266
286
|
coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
|
|
267
|
-
} else {
|
|
287
|
+
} else if (node.runtime.algorithm === "split") {
|
|
268
288
|
heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
|
|
269
289
|
coolingThreshold = node.runtime.coolingSetpoint + node.runtime.extent;
|
|
290
|
+
} else if (node.runtime.algorithm === "specified") {
|
|
291
|
+
heatingThreshold = node.runtime.heatingOn - node.runtime.extent;
|
|
292
|
+
coolingThreshold = node.runtime.coolingOn + node.runtime.extent;
|
|
270
293
|
}
|
|
271
294
|
|
|
272
295
|
if (temp < heatingThreshold) {
|
|
@@ -298,9 +321,12 @@ module.exports = function(RED) {
|
|
|
298
321
|
if (node.runtime.algorithm === "single") {
|
|
299
322
|
heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
|
|
300
323
|
coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
|
|
301
|
-
} else {
|
|
324
|
+
} else if (node.runtime.algorithm === "split") {
|
|
302
325
|
heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
|
|
303
326
|
coolingThreshold = node.runtime.coolingSetpoint + node.runtime.extent;
|
|
327
|
+
} else if (node.runtime.algorithm === "specified") {
|
|
328
|
+
heatingThreshold = node.runtime.heatingOn - node.runtime.extent;
|
|
329
|
+
coolingThreshold = node.runtime.coolingOn + node.runtime.extent;
|
|
304
330
|
}
|
|
305
331
|
|
|
306
332
|
let desiredMode = node.runtime.currentMode;
|
|
@@ -339,9 +365,12 @@ module.exports = function(RED) {
|
|
|
339
365
|
if (node.runtime.algorithm === "single") {
|
|
340
366
|
effectiveHeatingSetpoint = node.runtime.setpoint - node.runtime.deadband / 2;
|
|
341
367
|
effectiveCoolingSetpoint = node.runtime.setpoint + node.runtime.deadband / 2;
|
|
342
|
-
} else {
|
|
368
|
+
} else if (node.runtime.algorithm === "split") {
|
|
343
369
|
effectiveHeatingSetpoint = node.runtime.heatingSetpoint;
|
|
344
370
|
effectiveCoolingSetpoint = node.runtime.coolingSetpoint;
|
|
371
|
+
} else if (node.runtime.algorithm === "specified") {
|
|
372
|
+
effectiveHeatingSetpoint = node.runtime.heatingOn;
|
|
373
|
+
effectiveCoolingSetpoint = node.runtime.coolingOn;
|
|
345
374
|
}
|
|
346
375
|
|
|
347
376
|
return [
|
package/nodes/delay-block.html
CHANGED
package/nodes/delay-block.js
CHANGED
|
@@ -11,13 +11,10 @@ module.exports = function(RED) {
|
|
|
11
11
|
desired: false
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
const typedProperties = ['delayOn', 'delayOff'];
|
|
15
|
-
|
|
16
14
|
// Evaluate typed-input properties
|
|
17
15
|
try {
|
|
18
|
-
|
|
19
|
-
node.runtime.
|
|
20
|
-
node.runtime.delayOff = (parseFloat(evaluatedValues.delayOff)) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
|
|
16
|
+
node.runtime.delayOn = parseFloat(RED.util.evaluateNodeProperty( config.delayOn, config.delayOnType, node )) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
|
|
17
|
+
node.runtime.delayOff = parseFloat(RED.util.evaluateNodeProperty( config.delayOff, config.delayOffType, node )) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
|
|
21
18
|
} catch (err) {
|
|
22
19
|
node.error(`Error evaluating properties: ${err.message}`);
|
|
23
20
|
}
|
|
@@ -33,9 +30,12 @@ module.exports = function(RED) {
|
|
|
33
30
|
|
|
34
31
|
// Update typed-input properties if needed
|
|
35
32
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
if (utils.requiresEvaluation(config.delayOnType)) {
|
|
34
|
+
node.runtime.delayOn = parseFloat(RED.util.evaluateNodeProperty( config.delayOn, config.delayOnType, node, msg )) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
|
|
35
|
+
}
|
|
36
|
+
if (utils.requiresEvaluation(config.delayOffType)) {
|
|
37
|
+
node.runtime.delayOff = parseFloat(RED.util.evaluateNodeProperty( config.delayOff, config.delayOffType, node, msg )) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
|
|
38
|
+
}
|
|
39
39
|
} catch (err) {
|
|
40
40
|
node.error(`Error evaluating properties: ${err.message}`);
|
|
41
41
|
if (done) done();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="enum-switch-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-property" title="Property to evaluate"><i class="fa fa-code"></i> Property</label>
|
|
8
|
+
<input type="text" id="node-input-property" placeholder="property">
|
|
9
|
+
<input type="hidden" id="node-input-propertyType">
|
|
10
|
+
<input type="hidden" id="node-input-outputs">
|
|
11
|
+
</div>
|
|
12
|
+
<div class="form-row">
|
|
13
|
+
<label><i class="fa fa-cogs"></i> Rules</label>
|
|
14
|
+
<div id="node-input-rules-container" style="border: 1px solid #999; padding: 5px; margin: 5px 0; max-height: 200px; overflow-y: auto;">
|
|
15
|
+
<input type="hidden" id="node-input-rules">
|
|
16
|
+
<div class="rule-template" style="display: none;">
|
|
17
|
+
<div class="form-row rule-item" style="margin-bottom: 5px; padding: 5px; border-bottom: 1px solid #eee; display: flex; align-items: center;">
|
|
18
|
+
<input type="text" class="node-input-rule-value" placeholder="value to match" style="flex: 1; margin-right: 5px;">
|
|
19
|
+
<button class="red-ui-button delete-rule" style="width: auto; padding: 0 8px;">
|
|
20
|
+
<i class="fa fa-times"></i>
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div id="node-input-rules-list"></div>
|
|
25
|
+
<button id="node-input-add-rule" class="red-ui-button" style="width: 100%; margin-top: 5px;">
|
|
26
|
+
<i class="fa fa-plus"></i> Add Rule
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<script type="text/javascript">
|
|
33
|
+
RED.nodes.registerType("enum-switch-block", {
|
|
34
|
+
category: "control",
|
|
35
|
+
color: "#301934",
|
|
36
|
+
defaults: {
|
|
37
|
+
name: { value: "" },
|
|
38
|
+
property: { value: "payload" },
|
|
39
|
+
propertyType: { value: "msg" },
|
|
40
|
+
rules: { value: "[]" },
|
|
41
|
+
outputs: { value: 1 }
|
|
42
|
+
},
|
|
43
|
+
inputs: 1,
|
|
44
|
+
outputs: 1,
|
|
45
|
+
inputLabels: ["input"],
|
|
46
|
+
outputLabels: function(index) {
|
|
47
|
+
try {
|
|
48
|
+
const rules = JSON.parse(this.rules || "[]");
|
|
49
|
+
if (rules[index]) {
|
|
50
|
+
return rules[index].value || `rule ${index + 1}`;
|
|
51
|
+
}
|
|
52
|
+
return "";
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
icon: "font-awesome/fa-exchange",
|
|
58
|
+
paletteLabel: "enum switch",
|
|
59
|
+
label: function() {
|
|
60
|
+
const rules = JSON.parse(this.rules || "[]");
|
|
61
|
+
return this.name ? this.name : `enum switch (${rules.length} rules)`;
|
|
62
|
+
},
|
|
63
|
+
oneditprepare: function() {
|
|
64
|
+
const node = this;
|
|
65
|
+
const rulesContainer = $("#node-input-rules-list");
|
|
66
|
+
const template = $(".rule-template").clone().removeClass("rule-template").show();
|
|
67
|
+
|
|
68
|
+
// Initialize typed inputs
|
|
69
|
+
$("#node-input-property").typedInput({
|
|
70
|
+
default: "msg",
|
|
71
|
+
types: ["msg", "flow", "global"],
|
|
72
|
+
typeField: "#node-input-propertyType"
|
|
73
|
+
}).typedInput("type", node.propertyType || "msg").typedInput("value", node.property);
|
|
74
|
+
|
|
75
|
+
// Parse existing rules
|
|
76
|
+
let rules = [];
|
|
77
|
+
try {
|
|
78
|
+
rules = JSON.parse(node.rules || "[]");
|
|
79
|
+
} catch (e) {
|
|
80
|
+
rules = [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Render existing rules
|
|
84
|
+
rules.forEach(function(rule, index) {
|
|
85
|
+
const ruleItem = template.clone();
|
|
86
|
+
ruleItem.find(".node-input-rule-value").val(rule.value || "");
|
|
87
|
+
rulesContainer.append(ruleItem);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Add new rule button click
|
|
91
|
+
$("#node-input-add-rule").on("click", function() {
|
|
92
|
+
const ruleItem = template.clone();
|
|
93
|
+
rulesContainer.append(ruleItem);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Delete rule button click
|
|
97
|
+
rulesContainer.on("click", ".delete-rule", function() {
|
|
98
|
+
$(this).closest(".rule-item").remove();
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
oneditsave: function() {
|
|
103
|
+
const rules = [];
|
|
104
|
+
$("#node-input-rules-list .rule-item").each(function() {
|
|
105
|
+
const value = $(this).find(".node-input-rule-value").val().trim();
|
|
106
|
+
if (value) {
|
|
107
|
+
rules.push({ value: value });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
$("#node-input-rules").val(JSON.stringify(rules));
|
|
112
|
+
$("#node-input-outputs").val(rules.length);
|
|
113
|
+
},
|
|
114
|
+
oneditresize: function(size) {
|
|
115
|
+
$("#node-input-rules-container").height(size.height - 200);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
<!-- Help Section -->
|
|
122
|
+
<script type="text/markdown" data-help-name="enum-switch-block">
|
|
123
|
+
Route input to the appropriate output based on switch values.
|
|
124
|
+
|
|
125
|
+
### Inputs
|
|
126
|
+
: payload (string) : `string` for comparison.
|
|
127
|
+
|
|
128
|
+
### Outputs
|
|
129
|
+
: payload (boolean) : Boolean output based on matching switch value.
|
|
130
|
+
|
|
131
|
+
### Properties
|
|
132
|
+
: slots (integer) : Number of input slots (≥ 2).
|
|
133
|
+
|
|
134
|
+
### Details
|
|
135
|
+
Similar to the NodeRED switch node, but single purpose, with the important difference:
|
|
136
|
+
1. Outputting boolean based on matching string input.
|
|
137
|
+
2. Updating all outputs `true`/`false` on each input message. (updating false being the key difference)
|
|
138
|
+
|
|
139
|
+
NodeRED `switch` node only outputs on matching cases, and does not update non-matching outputs unless you create opposing rules for each case,
|
|
140
|
+
which can be messy. Use case is to drive multiple boolean outputs based on a single string input, e.g., controlling visibility of multiple UI elements,
|
|
141
|
+
or routing logic based on a given mode.
|
|
142
|
+
|
|
143
|
+
Supports string, number, and boolean comparisons. Input type is inferred from the incoming message payload type.
|
|
144
|
+
|
|
145
|
+
Preserves original message structure, only modifying the payload to `true` or `false`.
|
|
146
|
+
|
|
147
|
+
### Status
|
|
148
|
+
- Green (dot): Configuration update
|
|
149
|
+
- Blue (dot): State changed
|
|
150
|
+
- Blue (ring): State unchanged
|
|
151
|
+
- Red (ring): Error
|
|
152
|
+
- Yellow (ring): Warning
|
|
153
|
+
|
|
154
|
+
### References
|
|
155
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
156
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
157
|
+
</script>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
|
|
4
|
+
function EnumSwitchBlockNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
// Parse rules from config
|
|
9
|
+
let rules = [];
|
|
10
|
+
try {
|
|
11
|
+
rules = JSON.parse(config.rules || "[]");
|
|
12
|
+
} catch (e) {
|
|
13
|
+
node.error("Invalid rules configuration");
|
|
14
|
+
rules = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
node.on("input", function(msg, send, done) {
|
|
18
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
19
|
+
|
|
20
|
+
// Guard against invalid msg
|
|
21
|
+
if (!msg) {
|
|
22
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
23
|
+
if (done) done();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let matchAgainst;
|
|
28
|
+
|
|
29
|
+
// Evaluate typed-input properties
|
|
30
|
+
try {
|
|
31
|
+
matchAgainst = RED.util.evaluateNodeProperty( config.property, config.propertyType, node, msg );
|
|
32
|
+
|
|
33
|
+
if (matchAgainst === undefined) {
|
|
34
|
+
node.status({ fill: "red", shape: "ring", text: "property evaluation failed" });
|
|
35
|
+
if (done) done();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
node.status({ fill: "red", shape: "ring", text: `Error: ${err.message}` });
|
|
40
|
+
if (done) done(err);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const outputs = [];
|
|
45
|
+
let matched = false;
|
|
46
|
+
|
|
47
|
+
// Evaluate each rule and set outputs
|
|
48
|
+
for (let i = 0; i < rules.length; i++) {
|
|
49
|
+
const rule = rules[i];
|
|
50
|
+
let match = false;
|
|
51
|
+
|
|
52
|
+
// Handle different types for comparison
|
|
53
|
+
if (matchAgainst === null || matchAgainst === undefined) {
|
|
54
|
+
match = (rule.value === null || rule.value === undefined || rule.value === "");
|
|
55
|
+
} else if (typeof matchAgainst === 'string' && typeof rule.value === 'string') {
|
|
56
|
+
match = matchAgainst === rule.value;
|
|
57
|
+
} else if (typeof matchAgainst === 'number') {
|
|
58
|
+
const numericRuleValue = parseFloat(rule.value);
|
|
59
|
+
match = !isNaN(numericRuleValue) && matchAgainst === numericRuleValue;
|
|
60
|
+
} else if (typeof matchAgainst === 'boolean') {
|
|
61
|
+
const boolRuleValue = rule.value.toLowerCase() === 'true';
|
|
62
|
+
match = matchAgainst === boolRuleValue;
|
|
63
|
+
} else {
|
|
64
|
+
match = String(matchAgainst) === String(rule.value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
outputs[i] = match;
|
|
68
|
+
|
|
69
|
+
if (match) {
|
|
70
|
+
matched = true;
|
|
71
|
+
node.status({ fill: "blue", shape: "dot", text: `Matched: ${rule.value}` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Send output messages (all outputs as booleans)
|
|
76
|
+
const messages = outputs.map(isMatch => {
|
|
77
|
+
return {
|
|
78
|
+
...msg,
|
|
79
|
+
payload: isMatch,
|
|
80
|
+
topic: msg.topic
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
send(messages);
|
|
85
|
+
|
|
86
|
+
if (!matched && rules.length > 0) {
|
|
87
|
+
node.status({ fill: "blue", shape: "ring", text: "No match" });
|
|
88
|
+
} else if (rules.length === 0) {
|
|
89
|
+
node.status({ fill: "yellow", shape: "ring", text: "No rules configured" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (done) done();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
node.on("close", function(done) {
|
|
96
|
+
if (done) done();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
RED.nodes.registerType("enum-switch-block", EnumSwitchBlockNode);
|
|
101
|
+
};
|
|
@@ -94,7 +94,7 @@ Hysteresis controller with separate turn-on limits and turn-off differentials.
|
|
|
94
94
|
|
|
95
95
|
### Inputs
|
|
96
96
|
: payload (number) : Input value to evaluate
|
|
97
|
-
: context (string) : Configure `
|
|
97
|
+
: context (string) : Configure `upperLimitThreshold`, `lowerLimitThreshold`
|
|
98
98
|
|
|
99
99
|
### Outputs
|
|
100
100
|
: above (boolean) : Input > upperLimit
|