@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,239 @@
1
+ module.exports = function(RED) {
2
+ function PriorityBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+ const context = this.context();
6
+
7
+ // Initialize runtime state
8
+ node.runtime = {
9
+ name: config.name
10
+ };
11
+
12
+ // Initialize state from context or defaults
13
+ let priorities = context.get("priorities") || {
14
+ priority1: null, priority2: null, priority3: null, priority4: null,
15
+ priority5: null, priority6: null, priority7: null, priority8: null,
16
+ priority9: null, priority10: null, priority11: null, priority12: null,
17
+ priority13: null, priority14: null, priority15: null, priority16: null
18
+ };
19
+ let defaultValue = context.get("defaultValue") || null;
20
+ let fallbackValue = context.get("fallbackValue") || null;
21
+ let messages = context.get("messages") || {
22
+ priority1: null, priority2: null, priority3: null, priority4: null,
23
+ priority5: null, priority6: null, priority7: null, priority8: null,
24
+ priority9: null, priority10: null, priority11: null, priority12: null,
25
+ priority13: null, priority14: null, priority15: null, priority16: null,
26
+ default: null, fallback: null
27
+ };
28
+
29
+ // Save initial state to context
30
+ context.set("priorities", priorities);
31
+ context.set("defaultValue", defaultValue);
32
+ context.set("fallbackValue", fallbackValue);
33
+ context.set("messages", messages);
34
+
35
+ node.on("input", function(msg, send, done) {
36
+ send = send || function() { node.send.apply(node, arguments); };
37
+
38
+ // Guard against invalid message
39
+ if (!msg) {
40
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
41
+ if (done) done();
42
+ return;
43
+ }
44
+
45
+ // Validate payload
46
+ if (!msg.hasOwnProperty("payload")) {
47
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
48
+ if (done) done();
49
+ return;
50
+ }
51
+
52
+ // Handle keyed object payload for clearing
53
+ if (typeof msg.payload === "object" && msg.payload !== null && msg.payload.hasOwnProperty("clear")) {
54
+ const clear = msg.payload.clear;
55
+ if (clear === "all") {
56
+ priorities = {
57
+ priority1: null, priority2: null, priority3: null, priority4: null,
58
+ priority5: null, priority6: null, priority7: null, priority8: null,
59
+ priority9: null, priority10: null, priority11: null, priority12: null,
60
+ priority13: null, priority14: null, priority15: null, priority16: null
61
+ };
62
+ defaultValue = null;
63
+ fallbackValue = null;
64
+ messages = {
65
+ priority1: null, priority2: null, priority3: null, priority4: null,
66
+ priority5: null, priority6: null, priority7: null, priority8: null,
67
+ priority9: null, priority10: null, priority11: null, priority12: null,
68
+ priority13: null, priority14: null, priority15: null, priority16: null,
69
+ default: null, fallback: null
70
+ };
71
+ context.set("priorities", priorities);
72
+ context.set("defaultValue", defaultValue);
73
+ context.set("fallbackValue", fallbackValue);
74
+ context.set("messages", messages);
75
+ node.status({ fill: "green", shape: "dot", text: "all slots cleared" });
76
+ } else if (typeof clear === "string" && isValidSlot(clear)) {
77
+ if (clear.startsWith("priority")) priorities[clear] = null;
78
+ else if (clear === "default") defaultValue = null;
79
+ else if (clear === "fallback") fallbackValue = null;
80
+ messages[clear] = null;
81
+ context.set("priorities", priorities);
82
+ context.set("defaultValue", defaultValue);
83
+ context.set("fallbackValue", fallbackValue);
84
+ context.set("messages", messages);
85
+ node.status({ fill: "green", shape: "dot", text: `${clear} cleared` });
86
+ } else if (Array.isArray(clear) && clear.every(isValidSlot)) {
87
+ clear.forEach(slot => {
88
+ if (slot.startsWith("priority")) priorities[slot] = null;
89
+ else if (slot === "default") defaultValue = null;
90
+ else if (slot === "fallback") fallbackValue = null;
91
+ messages[slot] = null;
92
+ });
93
+ context.set("priorities", priorities);
94
+ context.set("defaultValue", defaultValue);
95
+ context.set("fallbackValue", fallbackValue);
96
+ context.set("messages", messages);
97
+ node.status({ fill: "green", shape: "dot", text: `${clear.join(", ")} cleared` });
98
+ } else {
99
+ node.status({ fill: "red", shape: "ring", text: "invalid clear" });
100
+ if (done) done();
101
+ return;
102
+ }
103
+ } else if (msg.payload === "clear") {
104
+ // Handle string "clear" with msg.context
105
+ if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
106
+ node.status({ fill: "red", shape: "ring", text: "missing or invalid context for clear" });
107
+ if (done) done();
108
+ return;
109
+ }
110
+ const contextMsg = msg.context;
111
+ if (isValidSlot(contextMsg)) {
112
+ if (contextMsg.startsWith("priority")) priorities[contextMsg] = null;
113
+ else if (contextMsg === "default") defaultValue = null;
114
+ else if (contextMsg === "fallback") fallbackValue = null;
115
+ messages[contextMsg] = null;
116
+ context.set("priorities", priorities);
117
+ context.set("defaultValue", defaultValue);
118
+ context.set("fallbackValue", fallbackValue);
119
+ context.set("messages", messages);
120
+ node.status({ fill: "green", shape: "dot", text: `${contextMsg} cleared` });
121
+ } else {
122
+ node.status({ fill: "red", shape: "ring", text: "invalid clear context" });
123
+ if (done) done();
124
+ return;
125
+ }
126
+ } else {
127
+ // Handle non-object, non-"clear" payloads
128
+ if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
129
+ node.status({ fill: "red", shape: "ring", text: "missing or invalid context" });
130
+ if (done) done();
131
+ return;
132
+ }
133
+
134
+ const contextMsg = msg.context;
135
+ const value = msg.payload === null ? null : typeof msg.payload === "number" ? parseFloat(msg.payload) : typeof msg.payload === "boolean" ? msg.payload : null;
136
+
137
+ if (value === null && msg.payload !== null) {
138
+ node.status({ fill: "red", shape: "ring", text: `invalid ${contextMsg}` });
139
+ if (done) done();
140
+ return;
141
+ }
142
+
143
+ if (/^priority([1-9]|1[0-6])$/.test(contextMsg)) {
144
+ priorities[contextMsg] = value;
145
+ messages[contextMsg] = RED.util.cloneMessage(msg);
146
+ context.set("priorities", priorities);
147
+ context.set("messages", messages);
148
+ node.status({
149
+ fill: "green",
150
+ shape: "dot",
151
+ text: value === null ? `${contextMsg} relinquished` : `${contextMsg}: ${typeof value === "number" ? value.toFixed(2) : value}`
152
+ });
153
+ } else if (contextMsg === "default") {
154
+ defaultValue = value;
155
+ messages[contextMsg] = RED.util.cloneMessage(msg);
156
+ context.set("defaultValue", defaultValue);
157
+ context.set("messages", messages);
158
+ node.status({
159
+ fill: "green",
160
+ shape: "dot",
161
+ text: value === null ? "default relinquished" : `default: ${typeof value === "number" ? value.toFixed(2) : value}`
162
+ });
163
+ } else if (contextMsg === "fallback") {
164
+ fallbackValue = value;
165
+ messages[contextMsg] = RED.util.cloneMessage(msg);
166
+ context.set("fallbackValue", fallbackValue);
167
+ context.set("messages", messages);
168
+ node.status({
169
+ fill: "green",
170
+ shape: "dot",
171
+ text: value === null ? "fallback relinquished" : `fallback: ${typeof value === "number" ? value.toFixed(2) : value}`
172
+ });
173
+ } else {
174
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
175
+ if (done) done("Unknown context");
176
+ return;
177
+ }
178
+ }
179
+
180
+ // Output highest priority message
181
+ const currentOutput = evaluatePriority();
182
+ send(currentOutput);
183
+ const inDisplay = typeof msg.payload === "number" ? msg.payload.toFixed(2) : typeof msg.payload === "object" ? JSON.stringify(msg.payload).slice(0, 20) : msg.payload;
184
+ const outDisplay = currentOutput.payload === null ? "null" : typeof currentOutput.payload === "number" ? currentOutput.payload.toFixed(2) : currentOutput.payload;
185
+ node.status({
186
+ fill: "blue",
187
+ shape: "dot",
188
+ text: `in: ${inDisplay}, out: ${outDisplay}, slot: ${currentOutput.diagnostics.activePriority || "none"}`
189
+ });
190
+
191
+ if (done) done();
192
+
193
+ function isValidSlot(slot) {
194
+ return /^priority([1-9]|1[0-6])$/.test(slot) || slot === "default" || slot === "fallback";
195
+ }
196
+
197
+ function evaluatePriority() {
198
+ let selectedValue = null;
199
+ let activePriority = null;
200
+ let selectedMessage = null;
201
+
202
+ // Check priorities from 1 to 16
203
+ for (let i = 1; i <= 16; i++) {
204
+ const key = `priority${i}`;
205
+ if (priorities[key] !== null) {
206
+ selectedValue = priorities[key];
207
+ activePriority = key;
208
+ selectedMessage = messages[key];
209
+ break;
210
+ }
211
+ }
212
+
213
+ // Fall back to default or fallback
214
+ if (selectedValue === null) {
215
+ if (defaultValue !== null) {
216
+ selectedValue = defaultValue;
217
+ activePriority = "default";
218
+ selectedMessage = messages.default;
219
+ } else if (fallbackValue !== null) {
220
+ selectedValue = fallbackValue;
221
+ activePriority = "fallback";
222
+ selectedMessage = messages.fallback;
223
+ }
224
+ }
225
+
226
+ // Return the original message if available, otherwise a new message
227
+ const output = selectedMessage ? RED.util.cloneMessage(selectedMessage) : { payload: selectedValue };
228
+ output.diagnostics = { activePriority };
229
+ return output;
230
+ }
231
+ });
232
+
233
+ node.on("close", function(done) {
234
+ done();
235
+ });
236
+ }
237
+
238
+ RED.nodes.registerType("priority-block", PriorityBlockNode);
239
+ };
@@ -0,0 +1,99 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="rate-limit-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-mode" title="Processing mode"><i class="fa fa-cog"></i> Mode</label>
9
+ <select id="node-input-mode">
10
+ <option value="rate-limit">Rate Limit</option>
11
+ <option value="threshold">Threshold</option>
12
+ <option value="full-value">Full Value</option>
13
+ </select>
14
+ </div>
15
+ <div class="form-row rate-limit-field">
16
+ <label for="node-input-rate" title="Rate of change (units per second, > 0)"><i class="fa fa-tachometer"></i> Rate (units/s)</label>
17
+ <input type="number" id="node-input-rate" placeholder="1" min="0.01" step="0.01">
18
+ </div>
19
+ <div class="form-row rate-limit-field">
20
+ <label for="node-input-interval" title="Update interval (milliseconds, ≥ 10, integer)"><i class="fa fa-clock-o"></i> Interval (ms)</label>
21
+ <input type="number" id="node-input-interval" placeholder="100" min="10" step="10">
22
+ </div>
23
+ <div class="form-row threshold-field">
24
+ <label for="node-input-threshold" title="Threshold for output (units, ≥ 0)"><i class="fa fa-filter"></i> Threshold (units)</label>
25
+ <input type="number" id="node-input-threshold" placeholder="5" min="0" step="0.01">
26
+ </div>
27
+ </script>
28
+
29
+ <!-- JavaScript Section -->
30
+ <script type="text/javascript">
31
+ RED.nodes.registerType("rate-limit-block", {
32
+ category: "control",
33
+ color: "#301934",
34
+ defaults: {
35
+ name: { value: "" },
36
+ mode: { value: "rate-limit", required: true, validate: function(v) { return ["rate-limit", "threshold", "full-value"].includes(v); } },
37
+ rate: { value: 1, required: false, validate: function(v) { return !v || (!isNaN(parseFloat(v)) && parseFloat(v) > 0 && isFinite(parseFloat(v))); } },
38
+ interval: { value: 100, required: false, validate: function(v) { return !v || (Number.isInteger(parseInt(v)) && parseInt(v) >= 10); } },
39
+ threshold: { value: 5, required: false, validate: function(v) { return !v || (!isNaN(parseFloat(v)) && parseFloat(v) >= 0 && isFinite(parseFloat(v))); } }
40
+ },
41
+ inputs: 1,
42
+ outputs: 1,
43
+ inputLabels: ["value"],
44
+ outputLabels: ["processed value"],
45
+ icon: "font-awesome/fa-tachometer-alt",
46
+ paletteLabel: "rate limit",
47
+ label: function() {
48
+ return this.name || this.mode || "rate limit";
49
+ },
50
+ oneditprepare: function() {
51
+ const node = this;
52
+
53
+ // Field visibility logic
54
+ function updateFieldVisibility() {
55
+ const mode = $("#node-input-mode").val();
56
+ $(".rate-limit-field").toggle(mode === "rate-limit");
57
+ $(".threshold-field").toggle(mode === "threshold");
58
+ }
59
+ $("#node-input-mode").on("change", updateFieldVisibility);
60
+ updateFieldVisibility();
61
+ }
62
+ });
63
+ </script>
64
+
65
+ <!-- Help Section -->
66
+ <script type="text/markdown" data-help-name="rate-limit-block">
67
+ Processes a numeric input value based on a selected mode and passes the original message.
68
+
69
+ ### Inputs
70
+ : context (string) : Configures settings (`"mode"`, `"rate"`, `"interval"`, `"threshold"`). Unmatched values trigger error.
71
+ : payload (number | string | number) : Number to process, or value for configuration (string for mode, number for others).
72
+
73
+ ### Outputs
74
+ : payload (number) : Processed value, depending on mode.
75
+ : Other properties (e.g., `msg.topic`) from the input message are preserved.
76
+
77
+ ### Properties
78
+ : mode (string) : Processing mode.
79
+ : rate (number) : Rate of change for rate-limit mode.
80
+ : interval (number) : Update interval for rate-limit mode.
81
+ : threshold (number) : Threshold for output in threshold mode.
82
+
83
+ ### Details
84
+ Processes `msg.payload` (number) in one of three modes:
85
+ - `Rate Limit` Limits rate of change to `rate` (units/s), updating every `interval` (ms) toward the input value.
86
+ - `Threshold` Outputs when input differs from last output by more than `threshold` (units).
87
+ - `Full Value` Passes input unchanged.
88
+
89
+ ### Status
90
+ - Green (dot): Configuration update
91
+ - Blue (dot): State changed
92
+ - Blue (ring): State unchanged
93
+ - Red (ring): Error
94
+ - Yellow (ring): Warning
95
+
96
+ ### References
97
+ - [Node-RED Documentation](https://nodered.org/docs/)
98
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
99
+ </script>
@@ -0,0 +1,221 @@
1
+ module.exports = function(RED) {
2
+ function RateLimitBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ mode: config.mode,
10
+ rate: parseFloat(config.rate),
11
+ interval: parseInt(config.interval),
12
+ threshold: parseFloat(config.threshold),
13
+ currentValue: 0,
14
+ targetValue: 0,
15
+ lastUpdate: Date.now(),
16
+ lastInputMsg: null
17
+ };
18
+
19
+ // Validate initial config
20
+ if (isNaN(node.runtime.rate) || node.runtime.rate <= 0 || !isFinite(node.runtime.rate)) {
21
+ node.runtime.rate = 1.0;
22
+ node.status({ fill: "red", shape: "ring", text: "invalid rate" });
23
+ }
24
+ if (isNaN(node.runtime.interval) || node.runtime.interval < 10 || !Number.isInteger(node.runtime.interval)) {
25
+ node.runtime.interval = 100;
26
+ node.status({ fill: "red", shape: "ring", text: "invalid interval" });
27
+ }
28
+ if (isNaN(node.runtime.threshold) || node.runtime.threshold < 0 || !isFinite(node.runtime.threshold)) {
29
+ node.runtime.threshold = 5.0;
30
+ node.status({ fill: "red", shape: "ring", text: "invalid threshold" });
31
+ }
32
+ if (!["rate-limit", "threshold", "full-value"].includes(node.runtime.mode)) {
33
+ node.runtime.mode = "rate-limit";
34
+ node.status({ fill: "red", shape: "ring", text: "invalid mode" });
35
+ }
36
+
37
+ // Set initial status
38
+ node.status({ fill: "blue", shape: "dot", text: `mode: ${node.runtime.mode}, out: ${node.runtime.currentValue.toFixed(2)}` });
39
+
40
+ let updateTimer = null;
41
+
42
+ // Function to update output for rate-limit mode
43
+ function updateRateLimitOutput() {
44
+ if (!node.runtime.lastInputMsg) return;
45
+ const now = Date.now();
46
+ const elapsed = (now - node.runtime.lastUpdate) / 1000; // Seconds
47
+ const maxChange = node.runtime.rate * elapsed;
48
+ let newValue = node.runtime.currentValue;
49
+
50
+ if (node.runtime.currentValue < node.runtime.targetValue) {
51
+ newValue = Math.min(node.runtime.currentValue + maxChange, node.runtime.targetValue);
52
+ } else if (node.runtime.currentValue > node.runtime.targetValue) {
53
+ newValue = Math.max(node.runtime.currentValue - maxChange, node.runtime.targetValue);
54
+ }
55
+
56
+ if (newValue !== node.runtime.currentValue) {
57
+ node.runtime.currentValue = newValue;
58
+ node.runtime.lastUpdate = now;
59
+ const msg = RED.util.cloneMessage(node.runtime.lastInputMsg);
60
+ msg.payload = node.runtime.currentValue;
61
+ node.status({
62
+ fill: "blue",
63
+ shape: "dot",
64
+ text: `mode: rate-limit, out: ${node.runtime.currentValue.toFixed(2)}`
65
+ });
66
+ node.send(msg);
67
+ }
68
+ }
69
+
70
+ // Start update timer for rate-limit mode
71
+ function startTimer() {
72
+ if (updateTimer) clearInterval(updateTimer);
73
+ if (node.runtime.mode === "rate-limit") {
74
+ updateTimer = setInterval(updateRateLimitOutput, node.runtime.interval);
75
+ }
76
+ }
77
+
78
+ node.on("input", function(msg, send, done) {
79
+ send = send || function() { node.send.apply(node, arguments); };
80
+
81
+ // Guard against invalid message
82
+ if (!msg) {
83
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
84
+ if (done) done();
85
+ return;
86
+ }
87
+
88
+ // Handle context updates
89
+ if (msg.hasOwnProperty("context")) {
90
+ if (!msg.hasOwnProperty("payload")) {
91
+ node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
92
+ if (done) done();
93
+ return;
94
+ }
95
+ switch (msg.context) {
96
+ case "mode":
97
+ if (!["rate-limit", "threshold", "full-value"].includes(msg.payload)) {
98
+ node.status({ fill: "red", shape: "ring", text: "invalid mode" });
99
+ if (done) done();
100
+ return;
101
+ }
102
+ node.runtime.mode = msg.payload;
103
+ startTimer();
104
+ node.status({
105
+ fill: "green",
106
+ shape: "dot",
107
+ text: `mode: ${node.runtime.mode}`
108
+ });
109
+ break;
110
+ case "rate":
111
+ const rate = parseFloat(msg.payload);
112
+ if (isNaN(rate) || rate <= 0 || !isFinite(rate)) {
113
+ node.status({ fill: "red", shape: "ring", text: "invalid rate" });
114
+ if (done) done();
115
+ return;
116
+ }
117
+ node.runtime.rate = rate;
118
+ node.status({
119
+ fill: "green",
120
+ shape: "dot",
121
+ text: `rate: ${node.runtime.rate.toFixed(2)}`
122
+ });
123
+ break;
124
+ case "interval":
125
+ const interval = parseInt(msg.payload);
126
+ if (isNaN(interval) || interval < 10 || !Number.isInteger(interval)) {
127
+ node.status({ fill: "red", shape: "ring", text: "invalid interval" });
128
+ if (done) done();
129
+ return;
130
+ }
131
+ node.runtime.interval = interval;
132
+ startTimer();
133
+ node.status({
134
+ fill: "green",
135
+ shape: "dot",
136
+ text: `interval: ${node.runtime.interval}`
137
+ });
138
+ break;
139
+ case "threshold":
140
+ const threshold = parseFloat(msg.payload);
141
+ if (isNaN(threshold) || threshold < 0 || !isFinite(threshold)) {
142
+ node.status({ fill: "red", shape: "ring", text: "invalid threshold" });
143
+ if (done) done();
144
+ return;
145
+ }
146
+ node.runtime.threshold = threshold;
147
+ node.status({
148
+ fill: "green",
149
+ shape: "dot",
150
+ text: `threshold: ${node.runtime.threshold.toFixed(2)}`
151
+ });
152
+ break;
153
+ default:
154
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
155
+ if (done) done("Unknown context");
156
+ return;
157
+ }
158
+ if (done) done();
159
+ return;
160
+ }
161
+
162
+ // Validate input
163
+ if (typeof msg.payload !== "number" || isNaN(msg.payload) || !isFinite(msg.payload)) {
164
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
165
+ if (done) done();
166
+ return;
167
+ }
168
+
169
+ const inputValue = msg.payload;
170
+ node.runtime.lastInputMsg = RED.util.cloneMessage(msg);
171
+
172
+ if (node.runtime.mode === "rate-limit") {
173
+ node.runtime.targetValue = inputValue;
174
+ node.status({
175
+ fill: "green",
176
+ shape: "dot",
177
+ text: `mode: rate-limit, target: ${node.runtime.targetValue.toFixed(2)}`
178
+ });
179
+ updateRateLimitOutput();
180
+ startTimer();
181
+ } else if (node.runtime.mode === "threshold") {
182
+ const diff = Math.abs(inputValue - node.runtime.currentValue);
183
+ node.runtime.currentValue = inputValue;
184
+ if (diff > node.runtime.threshold) {
185
+ msg.payload = inputValue;
186
+ node.status({
187
+ fill: "blue",
188
+ shape: "dot",
189
+ text: `mode: threshold, out: ${node.runtime.currentValue.toFixed(2)}`
190
+ });
191
+ send(msg);
192
+ } else {
193
+ node.status({
194
+ fill: "blue",
195
+ shape: "ring",
196
+ text: `mode: threshold, out: ${node.runtime.currentValue.toFixed(2)}`
197
+ });
198
+ }
199
+ } else if (node.runtime.mode === "full-value") {
200
+ node.runtime.currentValue = inputValue;
201
+ msg.payload = inputValue;
202
+ node.status({
203
+ fill: "blue",
204
+ shape: "dot",
205
+ text: `mode: full-value, out: ${node.runtime.currentValue.toFixed(2)}`
206
+ });
207
+ send(msg);
208
+ }
209
+
210
+ if (done) done();
211
+ });
212
+
213
+ node.on("close", function(done) {
214
+ if (updateTimer) clearInterval(updateTimer);
215
+ updateTimer = null;
216
+ done();
217
+ });
218
+ }
219
+
220
+ RED.nodes.registerType("rate-limit-block", RateLimitBlockNode);
221
+ };
@@ -0,0 +1,73 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="round-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-precision" title="Rounding precision (0.1 for tenths, 0.5 for halves, 1.0 for whole numbers)"><i class="fa fa-ruler"></i> Precision</label>
9
+ <select id="node-input-precision">
10
+ <option value="0.1">0.1 (tenths)</option>
11
+ <option value="0.5">0.5 (halves)</option>
12
+ <option value="1.0">1.0 (whole)</option>
13
+ </select>
14
+ </div>
15
+ </script>
16
+
17
+ <!-- JavaScript Section -->
18
+ <script type="text/javascript">
19
+ RED.nodes.registerType("round-block", {
20
+ category: "control",
21
+ color: "#301934",
22
+ defaults: {
23
+ name: { value: "" },
24
+ precision: { value: "1.0", required: true }
25
+ },
26
+ inputs: 1,
27
+ outputs: 1,
28
+ inputLabels: ["input"],
29
+ outputLabels: ["rounded"],
30
+ icon: "font-awesome/fa-circle-o-notch",
31
+ paletteLabel: "round",
32
+ label: function() {
33
+ return this.name || "";
34
+ }
35
+ });
36
+ </script>
37
+
38
+ <!-- Help Section -->
39
+ <script type="text/markdown" data-help-name="round-block">
40
+ Rounds a float in `msg.payload` to the nearest configurable precision (tenth, half, or whole number).
41
+
42
+ ### Inputs
43
+ : payload (number) : Float to round.
44
+ : context (string, optional) : Action (`"precision"` to set precision). Unknown `msg.context` values are ignored.
45
+ : payload (string, for `"precision"`) : Precision value (`"0.1"`, `"0.5"`, `"1.0"`).
46
+
47
+ ### Outputs
48
+ : msg : Original message with `msg.payload` rounded to configured precision.
49
+
50
+ ### Properties
51
+ : name (string) : Display name in editor.
52
+ : precision (string) : Rounding precision (`"0.1"`, `"0.5"`, `"1.0"`).
53
+
54
+ ### Details
55
+ Rounds a float in `msg.payload` to the nearest tenth (0.1), half (0.5), or whole number (1.0), set via editor or `msg.context = "precision"`.
56
+
57
+ Operates as a passthrough node, modifying `msg.payload` and forwarding the original message.
58
+
59
+ If `msg.payload` is not a finite number, the message is passed unchanged.
60
+
61
+ Unknown `msg.context` values are ignored.
62
+
63
+ ### Status
64
+ - Green (dot): Configuration update
65
+ - Blue (dot): State changed
66
+ - Blue (ring): State unchanged
67
+ - Red (ring): Error
68
+ - Yellow (ring): Warning
69
+
70
+ ### References
71
+ - [Node-RED Documentation](https://nodered.org/docs/)
72
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
73
+ </script>