@bldgblocks/node-red-contrib-control 0.1.4
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 +43 -0
- package/nodes/accumulate-block.html +71 -0
- package/nodes/accumulate-block.js +104 -0
- package/nodes/add-block.html +67 -0
- package/nodes/add-block.js +97 -0
- package/nodes/analog-switch-block.html +65 -0
- package/nodes/analog-switch-block.js +129 -0
- package/nodes/and-block.html +64 -0
- package/nodes/and-block.js +73 -0
- package/nodes/average-block.html +97 -0
- package/nodes/average-block.js +137 -0
- package/nodes/boolean-switch-block.html +59 -0
- package/nodes/boolean-switch-block.js +88 -0
- package/nodes/boolean-to-number-block.html +59 -0
- package/nodes/boolean-to-number-block.js +45 -0
- package/nodes/cache-block.html +69 -0
- package/nodes/cache-block.js +106 -0
- package/nodes/call-status-block.html +111 -0
- package/nodes/call-status-block.js +274 -0
- package/nodes/changeover-block.html +234 -0
- package/nodes/changeover-block.js +392 -0
- package/nodes/comment-block.html +83 -0
- package/nodes/comment-block.js +53 -0
- package/nodes/compare-block.html +64 -0
- package/nodes/compare-block.js +84 -0
- package/nodes/contextual-label-block.html +67 -0
- package/nodes/contextual-label-block.js +52 -0
- package/nodes/convert-block.html +179 -0
- package/nodes/convert-block.js +289 -0
- package/nodes/count-block.html +57 -0
- package/nodes/count-block.js +92 -0
- package/nodes/debounce-block.html +64 -0
- package/nodes/debounce-block.js +140 -0
- package/nodes/delay-block.html +104 -0
- package/nodes/delay-block.js +180 -0
- package/nodes/divide-block.html +65 -0
- package/nodes/divide-block.js +123 -0
- package/nodes/edge-block.html +71 -0
- package/nodes/edge-block.js +120 -0
- package/nodes/frequency-block.html +55 -0
- package/nodes/frequency-block.js +140 -0
- package/nodes/hysteresis-block.html +131 -0
- package/nodes/hysteresis-block.js +142 -0
- package/nodes/interpolate-block.html +74 -0
- package/nodes/interpolate-block.js +141 -0
- package/nodes/load-sequence-block.html +134 -0
- package/nodes/load-sequence-block.js +272 -0
- package/nodes/max-block.html +76 -0
- package/nodes/max-block.js +103 -0
- package/nodes/memory-block.html +90 -0
- package/nodes/memory-block.js +241 -0
- package/nodes/min-block.html +77 -0
- package/nodes/min-block.js +106 -0
- package/nodes/minmax-block.html +89 -0
- package/nodes/minmax-block.js +119 -0
- package/nodes/modulo-block.html +73 -0
- package/nodes/modulo-block.js +126 -0
- package/nodes/multiply-block.html +63 -0
- package/nodes/multiply-block.js +115 -0
- package/nodes/negate-block.html +55 -0
- package/nodes/negate-block.js +91 -0
- package/nodes/nullify-block.html +111 -0
- package/nodes/nullify-block.js +78 -0
- package/nodes/on-change-block.html +79 -0
- package/nodes/on-change-block.js +191 -0
- package/nodes/oneshot-block.html +96 -0
- package/nodes/oneshot-block.js +169 -0
- package/nodes/or-block.html +64 -0
- package/nodes/or-block.js +73 -0
- package/nodes/pid-block.html +205 -0
- package/nodes/pid-block.js +407 -0
- package/nodes/priority-block.html +66 -0
- package/nodes/priority-block.js +239 -0
- package/nodes/rate-limit-block.html +99 -0
- package/nodes/rate-limit-block.js +221 -0
- package/nodes/round-block.html +73 -0
- package/nodes/round-block.js +89 -0
- package/nodes/saw-tooth-wave-block.html +87 -0
- package/nodes/saw-tooth-wave-block.js +161 -0
- package/nodes/scale-range-block.html +90 -0
- package/nodes/scale-range-block.js +137 -0
- package/nodes/sine-wave-block.html +88 -0
- package/nodes/sine-wave-block.js +142 -0
- package/nodes/subtract-block.html +64 -0
- package/nodes/subtract-block.js +103 -0
- package/nodes/thermistor-block.html +81 -0
- package/nodes/thermistor-block.js +146 -0
- package/nodes/tick-tock-block.html +66 -0
- package/nodes/tick-tock-block.js +110 -0
- package/nodes/time-sequence-block.html +67 -0
- package/nodes/time-sequence-block.js +144 -0
- package/nodes/triangle-wave-block.html +86 -0
- package/nodes/triangle-wave-block.js +154 -0
- package/nodes/tstat-block.html +311 -0
- package/nodes/tstat-block.js +499 -0
- package/nodes/units-block.html +150 -0
- package/nodes/units-block.js +106 -0
- package/package.json +73 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="load-sequence-block">
|
|
3
|
+
<div class="form-row">
|
|
4
|
+
<label for="node-input-name" title="Display name shown on the canvas"><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-enable" title="Enable sequencing (boolean)"><i class="fa fa-power-off"></i> Enable</label>
|
|
9
|
+
<input type="checkbox" id="node-input-enable" style="width: auto; vertical-align: middle;">
|
|
10
|
+
</div>
|
|
11
|
+
<div class="form-row">
|
|
12
|
+
<label for="node-input-hysteresis" title="Hysteresis for threshold switching (non-negative number)"><i class="fa fa-exchange"></i> Hysteresis</label>
|
|
13
|
+
<input type="number" id="node-input-hysteresis" placeholder="0.5" min="0" step="any">
|
|
14
|
+
</div>
|
|
15
|
+
<div class="form-row">
|
|
16
|
+
<label for="node-input-threshold1" title="Threshold for load 1 (number)"><i class="fa fa-arrow-up"></i> Threshold 1</label>
|
|
17
|
+
<input type="number" id="node-input-threshold1" placeholder="10.0" min="0" step="any">
|
|
18
|
+
</div>
|
|
19
|
+
<div class="form-row">
|
|
20
|
+
<label for="node-input-threshold2" title="Threshold for load 2 (number, > threshold1)"><i class="fa fa-arrow-up"></i> Threshold 2</label>
|
|
21
|
+
<input type="number" id="node-input-threshold2" placeholder="20.0" min="0" step="any">
|
|
22
|
+
</div>
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<label for="node-input-threshold3" title="Threshold for load 3 (number, > threshold2)"><i class="fa fa-arrow-up"></i> Threshold 3</label>
|
|
25
|
+
<input type="number" id="node-input-threshold3" placeholder="30.0" min="0" step="any">
|
|
26
|
+
</div>
|
|
27
|
+
<div class="form-row">
|
|
28
|
+
<label for="node-input-threshold4" title="Threshold for load 4 (number, > threshold3)"><i class="fa fa-arrow-up"></i> Threshold 4</label>
|
|
29
|
+
<input type="number" id="node-input-threshold4" placeholder="40.0" min="0" step="any">
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-row">
|
|
32
|
+
<label for="node-input-feedback1" title="Feedback for load 1 (boolean)"><i class="fa fa-refresh"></i> Feedback 1</label>
|
|
33
|
+
<input type="checkbox" id="node-input-feedback1" style="width: auto; vertical-align: middle;">
|
|
34
|
+
</div>
|
|
35
|
+
<div class="form-row">
|
|
36
|
+
<label for="node-input-feedback2" title="Feedback for load 2 (boolean)"><i class="fa fa-refresh"></i> Feedback 2</label>
|
|
37
|
+
<input type="checkbox" id="node-input-feedback2" style="width: auto; vertical-align: middle;">
|
|
38
|
+
</div>
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-feedback3" title="Feedback for load 3 (boolean)"><i class="fa fa-refresh"></i> Feedback 3</label>
|
|
41
|
+
<input type="checkbox" id="node-input-feedback3" style="width: auto; vertical-align: middle;">
|
|
42
|
+
</div>
|
|
43
|
+
<div class="form-row">
|
|
44
|
+
<label for="node-input-feedback4" title="Feedback for load 4 (boolean)"><i class="fa fa-refresh"></i> Feedback 4</label>
|
|
45
|
+
<input type="checkbox" id="node-input-feedback4" style="width: auto; vertical-align: middle;">
|
|
46
|
+
</div>
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
50
|
+
<script type="text/javascript">
|
|
51
|
+
RED.nodes.registerType("load-sequence-block", {
|
|
52
|
+
category: "control",
|
|
53
|
+
color: "#301934",
|
|
54
|
+
defaults: {
|
|
55
|
+
name: { value: "" },
|
|
56
|
+
enable: { value: true },
|
|
57
|
+
hysteresis: { value: 0.5, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
|
|
58
|
+
threshold1: { value: 10.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
|
|
59
|
+
threshold2: { value: 20.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
|
|
60
|
+
threshold3: { value: 30.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
|
|
61
|
+
threshold4: { value: 40.0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
|
|
62
|
+
feedback1: { value: true },
|
|
63
|
+
feedback2: { value: true },
|
|
64
|
+
feedback3: { value: true },
|
|
65
|
+
feedback4: { value: true }
|
|
66
|
+
},
|
|
67
|
+
inputs: 1,
|
|
68
|
+
outputs: 4,
|
|
69
|
+
inputLabels: ["input"],
|
|
70
|
+
outputLabels: ["load1", "load2", "load3", "load4"],
|
|
71
|
+
icon: "font-awesome/fa-list-ol",
|
|
72
|
+
paletteLabel: "load sequence",
|
|
73
|
+
label: function() {
|
|
74
|
+
return this.name || "load sequence";
|
|
75
|
+
},
|
|
76
|
+
oneditprepare: function() {
|
|
77
|
+
const node = this;
|
|
78
|
+
$("#node-input-enable").prop("checked", node.enable !== false);
|
|
79
|
+
$("#node-input-feedback1").prop("checked", node.feedback1 !== false);
|
|
80
|
+
$("#node-input-feedback2").prop("checked", node.feedback2 !== false);
|
|
81
|
+
$("#node-input-feedback3").prop("checked", node.feedback3 !== false);
|
|
82
|
+
$("#node-input-feedback4").prop("checked", node.feedback4 !== false);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<!-- Help Section -->
|
|
88
|
+
<script type="text/markdown" data-help-name="load-sequence-block">
|
|
89
|
+
Sequences four boolean outputs based on input thresholds with feedback and hysteresis.
|
|
90
|
+
|
|
91
|
+
### Inputs
|
|
92
|
+
: context (string) : Configures settings (`"enable"`, `"hysteresis"`, `"threshold1-4"`, `"feedback1-4"`). Unmatched values trigger error.
|
|
93
|
+
: payload (number | string | boolean) : Number for input, `"kill"` to shut down, boolean for `enable`/`feedback1-4`, number for `hysteresis`/`threshold1-4`.
|
|
94
|
+
|
|
95
|
+
### Outputs
|
|
96
|
+
: load1 (boolean) : `true` when active, else `false` or `null`.
|
|
97
|
+
: load2 (boolean) : `true` when active, else `false` or `null`.
|
|
98
|
+
: load3 (boolean) : `true` when active, else `false` or `null`.
|
|
99
|
+
: load4 (boolean) : `true` when active, else `false` or `null`.
|
|
100
|
+
|
|
101
|
+
### Properties
|
|
102
|
+
: name (string) : Display name in editor.
|
|
103
|
+
: enable (boolean) : Enables sequencing.
|
|
104
|
+
: hysteresis (number) : Hysteresis for threshold switching (≥ 0).
|
|
105
|
+
: threshold1 (number) : Threshold for load 1 (≥ 0).
|
|
106
|
+
: threshold2 (number) : Threshold for load 2 (≥ 0, > threshold1).
|
|
107
|
+
: threshold3 (number) : Threshold for load 3 (≥ 0, > threshold2).
|
|
108
|
+
: threshold4 (number) : Threshold for load 4 (≥ 0, > threshold3).
|
|
109
|
+
: feedback1 (boolean) : Feedback for load 1.
|
|
110
|
+
: feedback2 (boolean) : Feedback for load 2.
|
|
111
|
+
: feedback3 (boolean) : Feedback for load 3.
|
|
112
|
+
: feedback4 (boolean) : Feedback for load 4.
|
|
113
|
+
|
|
114
|
+
### Details
|
|
115
|
+
Sequences four boolean outputs (`load1` to `load4`) based on `msg.payload` (number) crossing `threshold1` to `threshold4` with `hysteresis`
|
|
116
|
+
and `feedback1-4`. Outputs turn on when input exceeds a threshold and prior feedback is `true` (e.g., `load2` requires `feedback1`).
|
|
117
|
+
|
|
118
|
+
Outputs turn off when input falls below `thresholdX - hysteresis` and higher-stage feedback allows. Configurable via editor or `msg.context`.
|
|
119
|
+
|
|
120
|
+
`msg.payload = "kill"` shuts down all outputs. `enable = false` disables outputs sequentially (highest to lowest).
|
|
121
|
+
|
|
122
|
+
Tracks active stages (`dOn`). Outputs new messages only when a state changes, sending a single non-null message for the lowest stage that changed.
|
|
123
|
+
|
|
124
|
+
### Status
|
|
125
|
+
- Green (dot): Configuration update
|
|
126
|
+
- Blue (dot): State changed
|
|
127
|
+
- Blue (ring): State unchanged
|
|
128
|
+
- Red (ring): Error
|
|
129
|
+
- Yellow (ring): Warning
|
|
130
|
+
|
|
131
|
+
### References
|
|
132
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
133
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function LoadSequenceBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name || "",
|
|
9
|
+
enable: config.enable,
|
|
10
|
+
hysteresis: parseFloat(config.hysteresis),
|
|
11
|
+
threshold1: parseFloat(config.threshold1),
|
|
12
|
+
threshold2: parseFloat(config.threshold2),
|
|
13
|
+
threshold3: parseFloat(config.threshold3),
|
|
14
|
+
threshold4: parseFloat(config.threshold4),
|
|
15
|
+
feedback1: config.feedback1,
|
|
16
|
+
feedback2: config.feedback2,
|
|
17
|
+
feedback3: config.feedback3,
|
|
18
|
+
feedback4: config.feedback4,
|
|
19
|
+
out1: false,
|
|
20
|
+
out2: false,
|
|
21
|
+
out3: false,
|
|
22
|
+
out4: false,
|
|
23
|
+
dOn: 0,
|
|
24
|
+
lastInput: 0,
|
|
25
|
+
lastOutputs: [false, false, false, false]
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Validate initial config
|
|
29
|
+
if (isNaN(node.runtime.hysteresis) || node.runtime.hysteresis < 0) {
|
|
30
|
+
node.runtime.hysteresis = 0.5;
|
|
31
|
+
node.status({ fill: "red", shape: "ring", text: "invalid hysteresis" });
|
|
32
|
+
}
|
|
33
|
+
if (isNaN(node.runtime.threshold1) || isNaN(node.runtime.threshold2) || isNaN(node.runtime.threshold3) || isNaN(node.runtime.threshold4) ||
|
|
34
|
+
node.runtime.threshold1 < 0 || node.runtime.threshold2 < 0 || node.runtime.threshold3 < 0 || node.runtime.threshold4 < 0 ||
|
|
35
|
+
node.runtime.threshold1 >= node.runtime.threshold2 || node.runtime.threshold2 >= node.runtime.threshold3 || node.runtime.threshold3 >= node.runtime.threshold4) {
|
|
36
|
+
node.runtime.threshold1 = 10.0;
|
|
37
|
+
node.runtime.threshold2 = 20.0;
|
|
38
|
+
node.runtime.threshold3 = 30.0;
|
|
39
|
+
node.runtime.threshold4 = 40.0;
|
|
40
|
+
node.status({ fill: "red", shape: "ring", text: "invalid threshold order" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
node.on("input", function(msg, send, done) {
|
|
44
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
45
|
+
|
|
46
|
+
// Guard against invalid message
|
|
47
|
+
if (!msg) {
|
|
48
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
49
|
+
if (done) done();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle configuration updates
|
|
54
|
+
if (msg.hasOwnProperty("context")) {
|
|
55
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
56
|
+
node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
|
|
57
|
+
if (done) done();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
switch (msg.context) {
|
|
61
|
+
case "enable":
|
|
62
|
+
if (typeof msg.payload !== "boolean") {
|
|
63
|
+
node.status({ fill: "red", shape: "ring", text: "invalid enable" });
|
|
64
|
+
if (done) done();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
node.runtime.enable = msg.payload;
|
|
68
|
+
node.status({ fill: "green", shape: "dot", text: `enable: ${node.runtime.enable}` });
|
|
69
|
+
break;
|
|
70
|
+
case "hysteresis":
|
|
71
|
+
const hystValue = parseFloat(msg.payload);
|
|
72
|
+
if (isNaN(hystValue) || hystValue < 0) {
|
|
73
|
+
node.status({ fill: "red", shape: "ring", text: "invalid hysteresis" });
|
|
74
|
+
if (done) done();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
node.runtime.hysteresis = hystValue;
|
|
78
|
+
node.status({ fill: "green", shape: "dot", text: `hysteresis: ${node.runtime.hysteresis}` });
|
|
79
|
+
break;
|
|
80
|
+
case "threshold1":
|
|
81
|
+
case "threshold2":
|
|
82
|
+
case "threshold3":
|
|
83
|
+
case "threshold4":
|
|
84
|
+
const threshValue = parseFloat(msg.payload);
|
|
85
|
+
if (isNaN(threshValue) || threshValue < 0) {
|
|
86
|
+
node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
|
|
87
|
+
if (done) done();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const prevThresholds = [node.runtime.threshold1, node.runtime.threshold2, node.runtime.threshold3, node.runtime.threshold4];
|
|
91
|
+
const index = parseInt(msg.context.replace("threshold", "")) - 1;
|
|
92
|
+
const newThresholds = [...prevThresholds];
|
|
93
|
+
newThresholds[index] = threshValue;
|
|
94
|
+
if (newThresholds[0] >= newThresholds[1] || newThresholds[1] >= newThresholds[2] || newThresholds[2] >= newThresholds[3]) {
|
|
95
|
+
node.status({ fill: "red", shape: "ring", text: "invalid threshold order" });
|
|
96
|
+
if (done) done();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
node.runtime[`threshold${index + 1}`] = threshValue;
|
|
100
|
+
node.status({ fill: "green", shape: "dot", text: `${msg.context}: ${threshValue}` });
|
|
101
|
+
break;
|
|
102
|
+
case "feedback1":
|
|
103
|
+
case "feedback2":
|
|
104
|
+
case "feedback3":
|
|
105
|
+
case "feedback4":
|
|
106
|
+
if (typeof msg.payload !== "boolean") {
|
|
107
|
+
node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
|
|
108
|
+
if (done) done();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
node.runtime[msg.context] = msg.payload;
|
|
112
|
+
node.status({ fill: "green", shape: "dot", text: `${msg.context}: ${msg.payload}` });
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
116
|
+
if (done) done("Unknown context");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle input
|
|
122
|
+
let inputValue;
|
|
123
|
+
if (msg.hasOwnProperty("context")) {
|
|
124
|
+
inputValue = node.runtime.lastInput;
|
|
125
|
+
} else {
|
|
126
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
127
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
128
|
+
if (done) done();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (msg.payload === "kill") {
|
|
132
|
+
inputValue = node.runtime.lastInput;
|
|
133
|
+
} else {
|
|
134
|
+
inputValue = parseFloat(msg.payload);
|
|
135
|
+
if (isNaN(inputValue)) {
|
|
136
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
137
|
+
if (done) done();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
node.runtime.lastInput = inputValue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Kill switch
|
|
145
|
+
if (msg.payload === "kill") {
|
|
146
|
+
node.runtime.out1 = node.runtime.out2 = node.runtime.out3 = node.runtime.out4 = false;
|
|
147
|
+
node.runtime.dOn = 0;
|
|
148
|
+
node.runtime.lastOutputs = [false, false, false, false];
|
|
149
|
+
node.status({ fill: "red", shape: "dot", text: "kill: all off" });
|
|
150
|
+
send([{ payload: false }, { payload: false }, { payload: false }, { payload: false }]);
|
|
151
|
+
if (done) done();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate thresholds
|
|
156
|
+
if (node.runtime.threshold1 >= node.runtime.threshold2 || node.runtime.threshold2 >= node.runtime.threshold3 || node.runtime.threshold3 >= node.runtime.threshold4) {
|
|
157
|
+
node.status({ fill: "red", shape: "ring", text: "invalid threshold order" });
|
|
158
|
+
if (done) done();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Process logic
|
|
163
|
+
let newMsg = [null, null, null, null];
|
|
164
|
+
let numStagesOn = 0;
|
|
165
|
+
|
|
166
|
+
if (!node.runtime.enable) {
|
|
167
|
+
if (node.runtime.out4) {
|
|
168
|
+
node.runtime.out4 = false;
|
|
169
|
+
newMsg[3] = { payload: false };
|
|
170
|
+
} else if (node.runtime.out3) {
|
|
171
|
+
node.runtime.out3 = false;
|
|
172
|
+
newMsg[2] = { payload: false };
|
|
173
|
+
} else if (node.runtime.out2) {
|
|
174
|
+
node.runtime.out2 = false;
|
|
175
|
+
newMsg[1] = { payload: false };
|
|
176
|
+
} else if (node.runtime.out1) {
|
|
177
|
+
node.runtime.out1 = false;
|
|
178
|
+
newMsg[0] = { payload: false };
|
|
179
|
+
}
|
|
180
|
+
numStagesOn = 0;
|
|
181
|
+
} else {
|
|
182
|
+
let newOut1 = node.runtime.out1;
|
|
183
|
+
let newOut2 = node.runtime.out2;
|
|
184
|
+
let newOut3 = node.runtime.out3;
|
|
185
|
+
let newOut4 = node.runtime.out4;
|
|
186
|
+
|
|
187
|
+
// Output 1
|
|
188
|
+
if (node.runtime.out1) {
|
|
189
|
+
if (inputValue < (node.runtime.threshold1 - node.runtime.hysteresis) && (node.runtime.feedback1 && !node.runtime.out2)) {
|
|
190
|
+
newOut1 = false;
|
|
191
|
+
}
|
|
192
|
+
} else if (inputValue >= node.runtime.threshold1) {
|
|
193
|
+
newOut1 = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Output 2
|
|
197
|
+
if (node.runtime.out2) {
|
|
198
|
+
if (inputValue < (node.runtime.threshold2 - node.runtime.hysteresis) && (node.runtime.feedback2 && !node.runtime.out3)) {
|
|
199
|
+
newOut2 = false;
|
|
200
|
+
}
|
|
201
|
+
} else if (inputValue >= node.runtime.threshold2 && node.runtime.feedback1) {
|
|
202
|
+
newOut2 = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Output 3
|
|
206
|
+
if (node.runtime.out3) {
|
|
207
|
+
if (inputValue < (node.runtime.threshold3 - node.runtime.hysteresis) && (node.runtime.feedback3 && !node.runtime.out4)) {
|
|
208
|
+
newOut3 = false;
|
|
209
|
+
}
|
|
210
|
+
} else if (inputValue >= node.runtime.threshold3 && node.runtime.feedback2) {
|
|
211
|
+
newOut3 = true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Output 4
|
|
215
|
+
if (node.runtime.out4) {
|
|
216
|
+
if (inputValue < (node.runtime.threshold4 - node.runtime.hysteresis) && node.runtime.feedback4) {
|
|
217
|
+
newOut4 = false;
|
|
218
|
+
}
|
|
219
|
+
} else if (inputValue >= node.runtime.threshold4 && node.runtime.feedback3) {
|
|
220
|
+
newOut4 = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Prioritize lowest stage change
|
|
224
|
+
if (newOut1 !== node.runtime.out1) {
|
|
225
|
+
node.runtime.out1 = newOut1;
|
|
226
|
+
newMsg = [{ payload: node.runtime.out1 }, null, null, null];
|
|
227
|
+
} else if (newOut2 !== node.runtime.out2) {
|
|
228
|
+
node.runtime.out2 = newOut2;
|
|
229
|
+
newMsg = [null, { payload: node.runtime.out2 }, null, null];
|
|
230
|
+
} else if (newOut3 !== node.runtime.out3) {
|
|
231
|
+
node.runtime.out3 = newOut3;
|
|
232
|
+
newMsg = [null, null, { payload: node.runtime.out3 }, null];
|
|
233
|
+
} else if (newOut4 !== node.runtime.out4) {
|
|
234
|
+
node.runtime.out4 = newOut4;
|
|
235
|
+
newMsg = [null, null, null, { payload: node.runtime.out4 }];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
numStagesOn = (node.runtime.out1 ? 1 : 0) + (node.runtime.out2 ? 1 : 0) + (node.runtime.out3 ? 1 : 0) + (node.runtime.out4 ? 1 : 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Update state
|
|
242
|
+
node.runtime.dOn = numStagesOn;
|
|
243
|
+
|
|
244
|
+
// Check if outputs changed
|
|
245
|
+
const outputsChanged = newMsg.some((msg, i) => msg !== null && msg.payload !== node.runtime.lastOutputs[i]);
|
|
246
|
+
node.runtime.lastOutputs = [node.runtime.out1, node.runtime.out2, node.runtime.out3, node.runtime.out4];
|
|
247
|
+
|
|
248
|
+
if (outputsChanged) {
|
|
249
|
+
node.status({
|
|
250
|
+
fill: "blue",
|
|
251
|
+
shape: "dot",
|
|
252
|
+
text: `in: ${inputValue.toFixed(2)}, out: [${node.runtime.out1}, ${node.runtime.out2}, ${node.runtime.out3}, ${node.runtime.out4}]`
|
|
253
|
+
});
|
|
254
|
+
send(newMsg);
|
|
255
|
+
} else {
|
|
256
|
+
node.status({
|
|
257
|
+
fill: "blue",
|
|
258
|
+
shape: "ring",
|
|
259
|
+
text: `in: ${inputValue.toFixed(2)}, out: [${node.runtime.out1}, ${node.runtime.out2}, ${node.runtime.out3}, ${node.runtime.out4}]`
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (done) done();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
node.on("close", function(done) {
|
|
267
|
+
done();
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
RED.nodes.registerType("load-sequence-block", LoadSequenceBlockNode);
|
|
272
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!-- UI Template Section: Defines the edit dialog -->
|
|
2
|
+
<script type="text/html" data-template-name="max-block">
|
|
3
|
+
<div class="form-row">
|
|
4
|
+
<label for="node-input-name" title="Display name shown on the canvas"><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-max" title="Maximum value for capping the input number"><i class="fa fa-arrow-up"></i> Max</label>
|
|
9
|
+
<input type="text" id="node-input-max" placeholder="50" min="0" step="any">
|
|
10
|
+
<input type="hidden" id="node-input-maxType">
|
|
11
|
+
</div>
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
15
|
+
<script type="text/javascript">
|
|
16
|
+
RED.nodes.registerType("max-block", {
|
|
17
|
+
category: "control",
|
|
18
|
+
color: "#301934",
|
|
19
|
+
defaults: {
|
|
20
|
+
name: { value: "" },
|
|
21
|
+
max: { value: 50, required: true },
|
|
22
|
+
maxType: { value: "num" },
|
|
23
|
+
},
|
|
24
|
+
inputs: 1,
|
|
25
|
+
outputs: 1,
|
|
26
|
+
inputLabels: ["input"],
|
|
27
|
+
outputLabels: ["output"],
|
|
28
|
+
icon: "font-awesome/fa-arrow-circle-up",
|
|
29
|
+
paletteLabel: "max",
|
|
30
|
+
label: function() {
|
|
31
|
+
return this.name || "max";
|
|
32
|
+
},
|
|
33
|
+
oneditprepare: function() {
|
|
34
|
+
const node = this;
|
|
35
|
+
|
|
36
|
+
// Initialize typed inputs
|
|
37
|
+
$("#node-input-max").typedInput({
|
|
38
|
+
default: "num",
|
|
39
|
+
types: ["num", "msg", "flow", "global"],
|
|
40
|
+
typeField: "#node-input-maxType"
|
|
41
|
+
}).typedInput("type", node.maxType || "num").typedInput("value", node.max);
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<!-- Help Section -->
|
|
48
|
+
<script type="text/markdown" data-help-name="max-block">
|
|
49
|
+
Caps a numeric input at a configurable maximum value.
|
|
50
|
+
|
|
51
|
+
### Inputs
|
|
52
|
+
: context (string) : Configures maximum value (`"max"`, `"setpoint"`). Unmatched values trigger warning.
|
|
53
|
+
: payload (number) : Input number to cap or new maximum value with `msg.context`.
|
|
54
|
+
|
|
55
|
+
### Outputs
|
|
56
|
+
: payload (number) : Input number capped at the maximum value.
|
|
57
|
+
|
|
58
|
+
### Properties
|
|
59
|
+
: max (number) : Maximum value for capping.
|
|
60
|
+
|
|
61
|
+
### Details
|
|
62
|
+
Caps `msg.payload` (a number) to the maximum value, forwarding the input message with updated `msg.payload`.
|
|
63
|
+
|
|
64
|
+
Configurable via editor (`name`, `max`) or `msg.context` (`"max"`, `"setpoint"`) with numeric `msg.payload`.
|
|
65
|
+
|
|
66
|
+
### Status
|
|
67
|
+
- Green (dot): Configuration update
|
|
68
|
+
- Blue (dot): State changed
|
|
69
|
+
- Blue (ring): State unchanged
|
|
70
|
+
- Red (ring): Error
|
|
71
|
+
- Yellow (ring): Warning
|
|
72
|
+
|
|
73
|
+
### References
|
|
74
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
75
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
76
|
+
</script>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function MaxBlockNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Initialize runtime state
|
|
7
|
+
node.runtime = {
|
|
8
|
+
name: config.name
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Store last output value for status
|
|
12
|
+
let lastOutput = null;
|
|
13
|
+
|
|
14
|
+
node.on("input", function(msg, send, done) {
|
|
15
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
16
|
+
|
|
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
|
+
// Guard against invalid message
|
|
36
|
+
if (!msg) {
|
|
37
|
+
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
38
|
+
if (done) done();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle context updates
|
|
43
|
+
if (msg.hasOwnProperty("context")) {
|
|
44
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
45
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload for max" });
|
|
46
|
+
if (done) done();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (msg.context === "max" || msg.context === "setpoint") {
|
|
50
|
+
const maxValue = parseFloat(msg.payload);
|
|
51
|
+
if (!isNaN(maxValue) && maxValue >= 0) {
|
|
52
|
+
node.runtime.max = maxValue;
|
|
53
|
+
node.status({ fill: "green", shape: "dot", text: `max: ${maxValue}`
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
node.status({ fill: "red", shape: "ring", text: "invalid max" });
|
|
57
|
+
}
|
|
58
|
+
if (done) done();
|
|
59
|
+
return;
|
|
60
|
+
} else {
|
|
61
|
+
node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
|
|
62
|
+
if (done) done();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate input payload
|
|
68
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
69
|
+
node.status({ fill: "red", shape: "ring", text: "missing payload" });
|
|
70
|
+
if (done) done();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const inputValue = parseFloat(msg.payload);
|
|
75
|
+
if (isNaN(inputValue)) {
|
|
76
|
+
node.status({ fill: "red", shape: "ring", text: "invalid payload" });
|
|
77
|
+
if (done) done();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Cap input at max
|
|
82
|
+
const outputValue = Math.min(inputValue, node.runtime.max);
|
|
83
|
+
|
|
84
|
+
// Update status and send output
|
|
85
|
+
msg.payload = outputValue;
|
|
86
|
+
node.status({
|
|
87
|
+
fill: "blue",
|
|
88
|
+
shape: lastOutput === outputValue ? "ring" : "dot",
|
|
89
|
+
text: `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`
|
|
90
|
+
});
|
|
91
|
+
lastOutput = outputValue;
|
|
92
|
+
send(msg);
|
|
93
|
+
|
|
94
|
+
if (done) done();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
node.on("close", function(done) {
|
|
98
|
+
done();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
RED.nodes.registerType("max-block", MaxBlockNode);
|
|
103
|
+
};
|