@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,90 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="memory-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-writePeriod" title="Delay in milliseconds before writing to file (non-negative number from msg, flow, global, or static value). Ignored if Write On Update is enabled."><i class="fa fa-clock-o"></i> File Write Period (ms)</label>
9
+ <input type="text" id="node-input-writePeriod" placeholder="60000">
10
+ <input type="hidden" id="node-input-writePeriodType">
11
+ </div>
12
+ <div class="form-row">
13
+ <label for="node-input-transferProperty" title="Property to transfer from stored message to output message"><i class="fa fa-exchange"></i> Transfer Property</label>
14
+ <input type="text" id="node-input-transferProperty" placeholder="payload">
15
+ </div>
16
+ <div class="form-row">
17
+ <label for="node-input-writeOnUpdate" title="If checked, updates write directly to file without storing in memory. Execute reads from file."><i class="fa fa-save"></i> Write On Update</label>
18
+ <input type="checkbox" id="node-input-writeOnUpdate" style="width: auto;">
19
+ </div>
20
+ </script>
21
+
22
+ <!-- JavaScript Section -->
23
+ <script type="text/javascript">
24
+ RED.nodes.registerType("memory-block", {
25
+ category: "control",
26
+ color: "#301934",
27
+ defaults: {
28
+ name: { value: "" },
29
+ writePeriod: { value: "60000", validate: function(v) { return RED.validators.number(true)(v) && parseFloat(v) >= 0; } },
30
+ writePeriodType: { value: "num" },
31
+ transferProperty: { value: "payload", required: true, validate: RED.validators.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/) },
32
+ writeOnUpdate: { value: false }
33
+ },
34
+ inputs: 1,
35
+ outputs: 2,
36
+ inputLabels: ["input"],
37
+ outputLabels: ["query", "value"],
38
+ icon: "font-awesome/fa-database",
39
+ paletteLabel: "memory",
40
+ label: function() {
41
+ return this.name || "memory";
42
+ },
43
+ oneditprepare: function() {
44
+ const periodInput = $("#node-input-writePeriod").typedInput({
45
+ default: "num",
46
+ types: ["num", "msg", "flow", "global"],
47
+ typeField: "#node-input-writePeriodType"
48
+ });
49
+
50
+ }
51
+ });
52
+ </script>
53
+
54
+ <!-- Help Section -->
55
+ <script type="text/markdown" data-help-name="memory-block">
56
+ Stores a message property persistently across reboots and redeployments, with configurable property transfer and optional direct file writes.
57
+
58
+ ### Inputs
59
+ : context (string) : Action (`"update"` to store message property, `"execute"` to output stored property, `"executeWithFallback"` to output stored property or store and output fallback, `"query"` to check stored value existence).
60
+ : [transferProperty] (any) : Property to store (`"update"`, `"executeWithFallback"`) or pass through (no `context`).
61
+
62
+ ### Outputs
63
+ : query : For `context = "query"`; `{ payload; boolean }` (`true` if stored value exists, `false` if none).
64
+ : value : For no `context`; Input `msg` unchanged. For `context = "execute"`; Input `msg` with `transferProperty` from stored message, or `{ payload; null }` if none. For `context = "executeWithFallback"`; Input `msg` with `transferProperty` from stored message, or stores and outputs `msg[transferProperty]`. For `context = "update"`; No output.
65
+
66
+ ### Properties
67
+ : writePeriod (string) : Delay in milliseconds before writing to file (non-negative, from `msg`, `flow`, `global`, or static). Ignored if Write On Update is enabled.
68
+ : writePeriodType (string) : Source of `writePeriod` (`"num"`, `"msg"`, `"flow"`, `"global"`).
69
+ : transferProperty (string) : Property to transfer from stored message to output message (valid JavaScript property name).
70
+ : writeOnUpdate (boolean) : If true, `update` writes directly to file without in-memory storage; `execute` and `executeWithFallback` read from file.
71
+
72
+ ### Details
73
+ Stores a single message property persistently in a JSON file, surviving Node-RED reboots and redeployments. Actions;
74
+ - `msg.context = "update"` Stores `msg[transferProperty]`. If Write On Update is enabled, writes directly to file without in-memory storage; otherwise, stores in memory and context, schedules file write after `writePeriod` (ms, default 60000). No output.
75
+ - `msg.context = "execute"` Outputs input `msg` with `transferProperty` from stored message to Output 2 if available, or `{ payload; null }` if none. Reads from file if Write On Update is enabled, else uses in-memory value. No file write.
76
+ - `msg.context = "executeWithFallback"` Outputs input `msg` with `transferProperty` from stored message to Output 2 if available; if none, stores `msg[transferProperty]`, schedules file write (or writes directly if Write On Update is enabled), and outputs `msg[transferProperty]`.
77
+ - `msg.context = "query"` Outputs `{ payload; true }` to Output 1 if a stored value exists, `{ payload; false }` if none. Checks file existence if Write On Update is enabled. No file write or Output 2 message.
78
+ - No `msg.context` Passes input `msg` unchanged to Output 2, no effect on stored message or file.
79
+
80
+ ### Status
81
+ - Green (dot): Configuration update
82
+ - Blue (dot): State changed
83
+ - Blue (ring): State unchanged
84
+ - Red (ring): Error
85
+ - Yellow (ring): Warning
86
+
87
+ ### References
88
+ - [Node-RED Documentation](https://nodered.org/docs/)
89
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
90
+ </script>
@@ -0,0 +1,241 @@
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+ const fsSync = require("fs");
4
+
5
+ module.exports = function(RED) {
6
+ function MemoryBlockNode(config) {
7
+ RED.nodes.createNode(this, config);
8
+ const node = this;
9
+
10
+ // Initialize runtime state
11
+ node.runtime = {
12
+ name: config.name,
13
+ writePeriod: config.writePeriod,
14
+ writePeriodType: config.writePeriodType,
15
+ transferProperty: config.transferProperty,
16
+ writeOnUpdate: config.writeOnUpdate === true,
17
+ storedMsg: null
18
+ };
19
+
20
+ // File path for persistent storage
21
+ const filePath = path.join(RED.settings.userDir, `memory-${node.id}.json`);
22
+
23
+ let writeTimeout = null;
24
+ let lastUpdateMsg = null;
25
+
26
+ // Load stored message from file
27
+ async function loadStoredMessage() {
28
+ try {
29
+ const data = await fs.readFile(filePath, "utf8");
30
+ node.runtime.storedMsg = JSON.parse(data);
31
+ const payloadStr = node.runtime.storedMsg[node.runtime.transferProperty] != null ? String(node.runtime.storedMsg[node.runtime.transferProperty]).substring(0, 20) : "null";
32
+ node.status({ fill: "green", shape: "dot", text: `loaded: ${payloadStr}` });
33
+ } catch (err) {
34
+ if (err.code !== "ENOENT") {
35
+ node.status({ fill: "red", shape: "ring", text: "file error" });
36
+ }
37
+ }
38
+ }
39
+
40
+ // Read message from file synchronously (for execute and executeWithFallback when writeOnUpdate is true)
41
+ function readStoredMessageSync() {
42
+ try {
43
+ if (fsSync.existsSync(filePath)) {
44
+ const data = fsSync.readFileSync(filePath, "utf8");
45
+ return JSON.parse(data);
46
+ }
47
+ return null;
48
+ } catch (err) {
49
+ node.status({ fill: "red", shape: "ring", text: "file read error" });
50
+ node.error("Failed to read stored message: " + err.message);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ // Save message to file
56
+ async function saveMessage() {
57
+ if (lastUpdateMsg === null) return;
58
+ try {
59
+ await fs.writeFile(filePath, JSON.stringify(lastUpdateMsg));
60
+ lastUpdateMsg = null;
61
+ } catch (err) {
62
+ node.status({ fill: "red", shape: "ring", text: "file error" });
63
+ node.error("Failed to save message: " + err.message);
64
+ }
65
+ }
66
+
67
+ // Initialize (load only if writeOnUpdate is false)
68
+ if (!node.runtime.writeOnUpdate) {
69
+ loadStoredMessage().catch(err => {
70
+ node.error("Failed to load stored message: " + err.message);
71
+ });
72
+ }
73
+
74
+ node.on("input", function(msg, send, done) {
75
+ send = send || function() { node.send.apply(node, arguments); };
76
+
77
+ // Guard against invalid message
78
+ if (!msg) {
79
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
80
+ if (done) done();
81
+ return;
82
+ }
83
+
84
+ // Resolve typed inputs
85
+ node.runtime.writePeriod = RED.util.evaluateNodeProperty(
86
+ config.writePeriod, config.writePeriodType, node, msg
87
+ );
88
+ node.runtime.writePeriod = parseFloat(node.runtime.writePeriod);
89
+
90
+ // Initialize output array: [Output 1, Output 2]
91
+ const output = [null, null];
92
+
93
+ // Handle context
94
+ if (!msg.hasOwnProperty("context") || !msg.context || typeof msg.context !== "string") {
95
+ // Pass-through message to Output 2
96
+ const payloadStr = msg[node.runtime.transferProperty] != null ? String(msg[node.runtime.transferProperty]).substring(0, 20) : "null";
97
+ node.status({ fill: "blue", shape: "dot", text: `in: ${payloadStr}, out2: ${payloadStr}` });
98
+ output[1] = msg;
99
+ send(output);
100
+ if (done) done();
101
+ return;
102
+ }
103
+
104
+ if (msg.context === "update") {
105
+ if (!msg.hasOwnProperty(node.runtime.transferProperty)) {
106
+ node.status({ fill: "red", shape: "ring", text: `missing ${node.runtime.transferProperty}` });
107
+ if (done) done();
108
+ return;
109
+ }
110
+ const payloadStr = msg[node.runtime.transferProperty] != null ? String(msg[node.runtime.transferProperty]).substring(0, 20) : "null";
111
+ if (node.runtime.writeOnUpdate) {
112
+ // Write directly to file, do not store in memory
113
+ try {
114
+ fs.writeFile(filePath, JSON.stringify(msg)).catch(err => {
115
+ node.status({ fill: "red", shape: "ring", text: "file error" });
116
+ node.error("Failed to save message: " + err.message);
117
+ });
118
+ node.status({ fill: "green", shape: "dot", text: `updated: ${payloadStr}` });
119
+ } catch (err) {
120
+ node.status({ fill: "red", shape: "ring", text: "file error" });
121
+ node.error("Failed to save message: " + err.message);
122
+ }
123
+ } else {
124
+ // Original behavior: store in memory and context, delay write
125
+ node.runtime.storedMsg = RED.util.cloneMessage(msg);
126
+ node.context().set("storedMsg", node.runtime.storedMsg);
127
+ lastUpdateMsg = node.runtime.storedMsg;
128
+ node.status({ fill: "green", shape: "dot", text: `updated: ${payloadStr}` });
129
+ if (writeTimeout) clearTimeout(writeTimeout);
130
+ writeTimeout = setTimeout(() => {
131
+ saveMessage();
132
+ }, writePeriod);
133
+ }
134
+ if (done) done();
135
+ return;
136
+ }
137
+
138
+ if (msg.context === "execute") {
139
+ let storedMsg = node.runtime.writeOnUpdate ? readStoredMessageSync() : node.runtime.storedMsg;
140
+ if (storedMsg !== null) {
141
+ const outMsg = RED.util.cloneMessage(msg);
142
+ outMsg[node.runtime.transferProperty] = storedMsg[node.runtime.transferProperty];
143
+ const payloadStr = outMsg[node.runtime.transferProperty] != null ? String(outMsg[node.runtime.transferProperty]).substring(0, 20) : "null";
144
+ node.status({ fill: "blue", shape: "dot", text: `in: execute, out2: ${payloadStr}` });
145
+ output[1] = outMsg;
146
+ } else {
147
+ node.status({ fill: "blue", shape: "ring", text: `in: execute, out2: null` });
148
+ output[1] = { payload: null };
149
+ }
150
+ send(output);
151
+ if (done) done();
152
+ return;
153
+ }
154
+
155
+ if (msg.context === "executeWithFallback") {
156
+ let storedMsg = node.runtime.writeOnUpdate ? readStoredMessageSync() : node.runtime.storedMsg;
157
+ if (storedMsg !== null) {
158
+ const outMsg = RED.util.cloneMessage(msg);
159
+ outMsg[node.runtime.transferProperty] = storedMsg[node.runtime.transferProperty];
160
+ const payloadStr = outMsg[node.runtime.transferProperty] != null ? String(outMsg[node.runtime.transferProperty]).substring(0, 20) : "null";
161
+ node.status({ fill: "blue", shape: "dot", text: `in: executeWithFallback, out2: ${payloadStr}` });
162
+ output[1] = outMsg;
163
+ } else {
164
+ let value;
165
+ if (msg.hasOwnProperty(node.runtime.transferProperty)) {
166
+ value = msg[node.runtime.transferProperty];
167
+ }
168
+ else if (msg.hasOwnProperty("fallback")) {
169
+ value = msg.fallback;
170
+ } else {
171
+ node.status({ fill: "red", shape: "ring", text: `missing ${node.runtime.transferProperty}` });
172
+ if (done) done();
173
+ return;
174
+ }
175
+
176
+ if (node.runtime.writeOnUpdate) {
177
+ // Write directly to file
178
+ try {
179
+ fs.writeFile(filePath, JSON.stringify({ [node.runtime.transferProperty]: value })).catch(err => {
180
+ node.status({ fill: "red", shape: "ring", text: "file error" });
181
+ node.error("Failed to save message: " + err.message);
182
+ });
183
+ } catch (err) {
184
+ node.status({ fill: "red", shape: "ring", text: "file error" });
185
+ node.error("Failed to save message: " + err.message);
186
+ }
187
+ } else {
188
+ // Store in memory and context
189
+ node.runtime.storedMsg = { [node.runtime.transferProperty]: value };
190
+ node.context().set("storedMsg", node.runtime.storedMsg);
191
+ lastUpdateMsg = node.runtime.storedMsg;
192
+ if (writeTimeout) clearTimeout(writeTimeout);
193
+ writeTimeout = setTimeout(() => {
194
+ saveMessage();
195
+ }, writePeriod);
196
+ }
197
+ const outMsg = RED.util.cloneMessage(msg);
198
+ outMsg[node.runtime.transferProperty] = value;
199
+ const payloadStr = msg[node.runtime.transferProperty] != null ? String(msg[node.runtime.transferProperty]).substring(0, 20) : "null";
200
+ node.status({ fill: "blue", shape: "dot", text: `in: executeWithFallback, out2: ${payloadStr}` });
201
+ output[1] = outMsg;
202
+ }
203
+ send(output);
204
+ if (done) done();
205
+ return;
206
+ }
207
+
208
+ if (msg.context === "query") {
209
+ const hasValue = node.runtime.writeOnUpdate ? fsSync.existsSync(filePath) : node.runtime.storedMsg !== null;
210
+ node.status({ fill: "blue", shape: "dot", text: `in: query, out1: ${hasValue}` });
211
+ output[0] = { payload: hasValue };
212
+ send(output);
213
+ if (done) done();
214
+ return;
215
+ }
216
+
217
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
218
+ if (done) done("Unknown context");
219
+ });
220
+
221
+ node.on("close", function(done) {
222
+ if (writeTimeout) clearTimeout(writeTimeout);
223
+ if (!node.runtime.writeOnUpdate && lastUpdateMsg) {
224
+ saveMessage()
225
+ .then(() => {
226
+ node.status({});
227
+ done();
228
+ })
229
+ .catch(err => {
230
+ node.error("Failed to save message on close: " + err.message);
231
+ node.status({});
232
+ done();
233
+ });
234
+ } else {
235
+ done();
236
+ }
237
+ });
238
+ }
239
+
240
+ RED.nodes.registerType("memory-block", MemoryBlockNode);
241
+ };
@@ -0,0 +1,77 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="min-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-min" title="Minimum value for capping the input number"><i class="fa fa-arrow-down"></i> Min</label>
9
+ <input type="text" id="node-input-min" placeholder="50" min="0" step="any">
10
+ <input type="hidden" id="node-input-minType">
11
+ </div>
12
+ </script>
13
+
14
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
15
+ <script type="text/javascript">
16
+ RED.nodes.registerType("min-block", {
17
+ category: "control",
18
+ color: "#301934",
19
+ defaults: {
20
+ name: { value: "" },
21
+ min: { value: 50, required: true },
22
+ minType: { value: "num" },
23
+ },
24
+ inputs: 1,
25
+ outputs: 1,
26
+ inputLabels: ["input"],
27
+ outputLabels: ["output"],
28
+ icon: "font-awesome/fa-arrow-circle-down",
29
+ paletteLabel: "min",
30
+ label: function() {
31
+ return this.name || "min";
32
+ },
33
+ oneditprepare: function() {
34
+ const node = this;
35
+
36
+ // Initialize typed inputs
37
+ $("#node-input-min").typedInput({
38
+ default: "num",
39
+ types: ["num", "msg", "flow", "global"],
40
+ typeField: "#node-input-minType"
41
+ }).typedInput("type", node.minType || "num").typedInput("value", node.min);
42
+
43
+ }
44
+ });
45
+ </script>
46
+
47
+ <!-- Help Section -->
48
+ <script type="text/markdown" data-help-name="min-block">
49
+ Ensures a numeric input is at least a configurable minimum value.
50
+
51
+ ### Inputs
52
+ : context (string) : Configures minimum value (`"min"`, `"setpoint"`). Unmatched values trigger warning.
53
+ : payload (number) : Input number to cap or new minimum value with `msg.context`.
54
+
55
+ ### Outputs
56
+ : payload (number) : Input number capped at the minimum value.
57
+ : *other* (any) : All input properties preserved, including `msg.context`.
58
+
59
+ ### Properties
60
+ : min (number) : Minimum value for capping.
61
+
62
+ ### Details
63
+ Ensures `msg.payload` (a number) is at least the minimum value, forwarding the input message with updated `msg.payload`.
64
+
65
+ Outputs on every valid input, with status indicating whether the output changed.
66
+
67
+ ### Status
68
+ - Green (dot): Configuration update
69
+ - Blue (dot): State changed
70
+ - Blue (ring): State unchanged
71
+ - Red (ring): Error
72
+ - Yellow (ring): Warning
73
+
74
+ ### References
75
+ - [Node-RED Documentation](https://nodered.org/docs/)
76
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
77
+ </script>
@@ -0,0 +1,106 @@
1
+ module.exports = function(RED) {
2
+ function MinBlockNode(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.min = RED.util.evaluateNodeProperty(
20
+ config.min, config.minType, node, msg
21
+ );
22
+
23
+ // Validate values
24
+ if (isNaN(node.runtime.min)) {
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 min" });
46
+ if (done) done();
47
+ return;
48
+ }
49
+ if (msg.context === "min" || msg.context === "setpoint") {
50
+ const minValue = parseFloat(msg.payload);
51
+ if (!isNaN(minValue) && minValue >= 0) {
52
+ node.runtime.min = minValue;
53
+ node.status({
54
+ fill: "green",
55
+ shape: "dot",
56
+ text: `min: ${minValue}`
57
+ });
58
+ } else {
59
+ node.status({ fill: "red", shape: "ring", text: "invalid min" });
60
+ }
61
+ if (done) done();
62
+ return;
63
+ } else {
64
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
65
+ if (done) done();
66
+ return;
67
+ }
68
+ }
69
+
70
+ // Validate input payload
71
+ if (!msg.hasOwnProperty("payload")) {
72
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
73
+ if (done) done();
74
+ return;
75
+ }
76
+
77
+ const inputValue = parseFloat(msg.payload);
78
+ if (isNaN(inputValue)) {
79
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
80
+ if (done) done();
81
+ return;
82
+ }
83
+
84
+ // Cap input at min
85
+ const outputValue = Math.max(inputValue, node.runtime.min);
86
+
87
+ // Update status and send output
88
+ msg.payload = outputValue;
89
+ node.status({
90
+ fill: "blue",
91
+ shape: lastOutput === outputValue ? "ring" : "dot",
92
+ text: `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`
93
+ });
94
+ lastOutput = outputValue;
95
+ send(msg);
96
+
97
+ if (done) done();
98
+ });
99
+
100
+ node.on("close", function(done) {
101
+ done();
102
+ });
103
+ }
104
+
105
+ RED.nodes.registerType("min-block", MinBlockNode);
106
+ };
@@ -0,0 +1,89 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="minmax-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-min" title="Minimum value for clamping the input number"><i class="fa fa-arrow-down"></i> Min</label>
9
+ <input type="text" id="node-input-min" placeholder="0" min="0" step="any">
10
+ <input type="hidden" id="node-input-minType">
11
+ </div>
12
+ <div class="form-row">
13
+ <label for="node-input-max" title="Maximum value for clamping the input number"><i class="fa fa-arrow-up"></i> Max</label>
14
+ <input type="text" id="node-input-max" placeholder="100" min="0" step="any">
15
+ <input type="hidden" id="node-input-maxType">
16
+ </div>
17
+ </script>
18
+
19
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
20
+ <script type="text/javascript">
21
+ RED.nodes.registerType("minmax-block", {
22
+ category: "control",
23
+ color: "#301934",
24
+ defaults: {
25
+ name: { value: "" },
26
+ min: { value: 0, required: true },
27
+ max: { value: 100, required: true }
28
+ },
29
+ inputs: 1,
30
+ outputs: 1,
31
+ inputLabels: ["input"],
32
+ outputLabels: ["output"],
33
+ icon: "font-awesome/fa-arrows-v",
34
+ paletteLabel: "minmax",
35
+ label: function() {
36
+ return this.name || "minmax";
37
+ },
38
+ oneditprepare: function() {
39
+ const node = this;
40
+
41
+ // Initialize typed inputs
42
+ $("#node-input-min").typedInput({
43
+ default: "num",
44
+ types: ["num", "msg", "flow", "global"],
45
+ typeField: "#node-input-minType"
46
+ }).typedInput("type", node.minType || "num").typedInput("value", node.min);
47
+
48
+ $("#node-input-max").typedInput({
49
+ default: "num",
50
+ types: ["num", "msg", "flow", "global"],
51
+ typeField: "#node-input-maxType"
52
+ }).typedInput("type", node.maxType || "num").typedInput("value", node.max);
53
+
54
+ }
55
+ });
56
+ </script>
57
+
58
+ <!-- Help Section -->
59
+ <script type="text/markdown" data-help-name="minmax-block">
60
+ Clamps a numeric input to a configurable range.
61
+
62
+ ### Inputs
63
+ : context (string) : Configures range (`"min"`, `"max"`). Unmatched values trigger warning.
64
+ : payload (number) : Input number to clamp or new min/max value with `msg.context`.
65
+
66
+ ### Outputs
67
+ : payload (number) : Input number clamped to `[min, max]`.
68
+ : *other* (any) : All input properties preserved, including `msg.context`.
69
+
70
+ ### Properties
71
+ : min (number) : Minimum value for clamping (≥ 0).
72
+ : max (number) : Maximum value for clamping (≥ 0, ≥ min)..
73
+
74
+ ### Details
75
+ Clamps `msg.payload` (a number) to the range `[min, max]`, forwarding the input message with updated `msg.payload`.
76
+
77
+ Ensures `min ≤ max` by adjusting the other value if needed (e.g., setting `min > max` sets `max = min`).
78
+
79
+ ### Status
80
+ - Green (dot): Configuration update
81
+ - Blue (dot): State changed
82
+ - Blue (ring): State unchanged
83
+ - Red (ring): Error
84
+ - Yellow (ring): Warning
85
+
86
+ ### References
87
+ - [Node-RED Documentation](https://nodered.org/docs/)
88
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
89
+ </script>