@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.
Files changed (98) hide show
  1. package/README.md +43 -0
  2. package/nodes/accumulate-block.html +71 -0
  3. package/nodes/accumulate-block.js +104 -0
  4. package/nodes/add-block.html +67 -0
  5. package/nodes/add-block.js +97 -0
  6. package/nodes/analog-switch-block.html +65 -0
  7. package/nodes/analog-switch-block.js +129 -0
  8. package/nodes/and-block.html +64 -0
  9. package/nodes/and-block.js +73 -0
  10. package/nodes/average-block.html +97 -0
  11. package/nodes/average-block.js +137 -0
  12. package/nodes/boolean-switch-block.html +59 -0
  13. package/nodes/boolean-switch-block.js +88 -0
  14. package/nodes/boolean-to-number-block.html +59 -0
  15. package/nodes/boolean-to-number-block.js +45 -0
  16. package/nodes/cache-block.html +69 -0
  17. package/nodes/cache-block.js +106 -0
  18. package/nodes/call-status-block.html +111 -0
  19. package/nodes/call-status-block.js +274 -0
  20. package/nodes/changeover-block.html +234 -0
  21. package/nodes/changeover-block.js +392 -0
  22. package/nodes/comment-block.html +83 -0
  23. package/nodes/comment-block.js +53 -0
  24. package/nodes/compare-block.html +64 -0
  25. package/nodes/compare-block.js +84 -0
  26. package/nodes/contextual-label-block.html +67 -0
  27. package/nodes/contextual-label-block.js +52 -0
  28. package/nodes/convert-block.html +179 -0
  29. package/nodes/convert-block.js +289 -0
  30. package/nodes/count-block.html +57 -0
  31. package/nodes/count-block.js +92 -0
  32. package/nodes/debounce-block.html +64 -0
  33. package/nodes/debounce-block.js +140 -0
  34. package/nodes/delay-block.html +104 -0
  35. package/nodes/delay-block.js +180 -0
  36. package/nodes/divide-block.html +65 -0
  37. package/nodes/divide-block.js +123 -0
  38. package/nodes/edge-block.html +71 -0
  39. package/nodes/edge-block.js +120 -0
  40. package/nodes/frequency-block.html +55 -0
  41. package/nodes/frequency-block.js +140 -0
  42. package/nodes/hysteresis-block.html +131 -0
  43. package/nodes/hysteresis-block.js +142 -0
  44. package/nodes/interpolate-block.html +74 -0
  45. package/nodes/interpolate-block.js +141 -0
  46. package/nodes/load-sequence-block.html +134 -0
  47. package/nodes/load-sequence-block.js +272 -0
  48. package/nodes/max-block.html +76 -0
  49. package/nodes/max-block.js +103 -0
  50. package/nodes/memory-block.html +90 -0
  51. package/nodes/memory-block.js +241 -0
  52. package/nodes/min-block.html +77 -0
  53. package/nodes/min-block.js +106 -0
  54. package/nodes/minmax-block.html +89 -0
  55. package/nodes/minmax-block.js +119 -0
  56. package/nodes/modulo-block.html +73 -0
  57. package/nodes/modulo-block.js +126 -0
  58. package/nodes/multiply-block.html +63 -0
  59. package/nodes/multiply-block.js +115 -0
  60. package/nodes/negate-block.html +55 -0
  61. package/nodes/negate-block.js +91 -0
  62. package/nodes/nullify-block.html +111 -0
  63. package/nodes/nullify-block.js +78 -0
  64. package/nodes/on-change-block.html +79 -0
  65. package/nodes/on-change-block.js +191 -0
  66. package/nodes/oneshot-block.html +96 -0
  67. package/nodes/oneshot-block.js +169 -0
  68. package/nodes/or-block.html +64 -0
  69. package/nodes/or-block.js +73 -0
  70. package/nodes/pid-block.html +205 -0
  71. package/nodes/pid-block.js +407 -0
  72. package/nodes/priority-block.html +66 -0
  73. package/nodes/priority-block.js +239 -0
  74. package/nodes/rate-limit-block.html +99 -0
  75. package/nodes/rate-limit-block.js +221 -0
  76. package/nodes/round-block.html +73 -0
  77. package/nodes/round-block.js +89 -0
  78. package/nodes/saw-tooth-wave-block.html +87 -0
  79. package/nodes/saw-tooth-wave-block.js +161 -0
  80. package/nodes/scale-range-block.html +90 -0
  81. package/nodes/scale-range-block.js +137 -0
  82. package/nodes/sine-wave-block.html +88 -0
  83. package/nodes/sine-wave-block.js +142 -0
  84. package/nodes/subtract-block.html +64 -0
  85. package/nodes/subtract-block.js +103 -0
  86. package/nodes/thermistor-block.html +81 -0
  87. package/nodes/thermistor-block.js +146 -0
  88. package/nodes/tick-tock-block.html +66 -0
  89. package/nodes/tick-tock-block.js +110 -0
  90. package/nodes/time-sequence-block.html +67 -0
  91. package/nodes/time-sequence-block.js +144 -0
  92. package/nodes/triangle-wave-block.html +86 -0
  93. package/nodes/triangle-wave-block.js +154 -0
  94. package/nodes/tstat-block.html +311 -0
  95. package/nodes/tstat-block.js +499 -0
  96. package/nodes/units-block.html +150 -0
  97. package/nodes/units-block.js +106 -0
  98. 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
+ };