@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,289 @@
1
+ const validConversions = [
2
+ "F to C",
3
+ "C to F",
4
+ "K to C",
5
+ "C to K",
6
+ "K to F",
7
+ "F to K",
8
+ "R to F",
9
+ "F to R",
10
+ "decimal to %",
11
+ "% to decimal",
12
+ "Pa to inH₂O",
13
+ "inH₂O to Pa",
14
+ "Pa to inHg",
15
+ "inHg to Pa",
16
+ "Pa to bar",
17
+ "bar to Pa",
18
+ "Pa to psi",
19
+ "psi to Pa",
20
+ "m to ft",
21
+ "ft to m",
22
+ "m to in",
23
+ "in to m",
24
+ "mm to in",
25
+ "in to mm",
26
+ "kg to lb",
27
+ "lb to kg",
28
+ "L to gal",
29
+ "gal to L",
30
+ "kW to hp",
31
+ "hp to kW",
32
+ "rad to deg",
33
+ "deg to rad",
34
+ "s to min",
35
+ "min to s"
36
+ ];
37
+
38
+ module.exports = function(RED) {
39
+ function ConvertBlockNode(config) {
40
+ RED.nodes.createNode(this, config);
41
+ const node = this;
42
+
43
+ // Initialize runtime state
44
+ node.runtime = {
45
+ conversion: validConversions.includes(config.conversion) ? config.conversion : "C to F"
46
+ };
47
+
48
+ node.on("input", function(msg, send, done) {
49
+ send = send || function() { node.send.apply(node, arguments); };
50
+
51
+ // Guard against invalid message
52
+ if (!msg) {
53
+ node.status({ fill: "red", shape: "ring", text: "missing message" });
54
+ if (done) done();
55
+ return;
56
+ }
57
+
58
+ // Handle configuration messages
59
+ if (msg.hasOwnProperty("context")) {
60
+ if (typeof msg.context !== "string") {
61
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
62
+ if (done) done();
63
+ return;
64
+ }
65
+ if (msg.context === "conversion") {
66
+ if (!msg.hasOwnProperty("payload") || !validConversions.includes(msg.payload)) {
67
+ node.status({ fill: "red", shape: "ring", text: "invalid conversion" });
68
+ if (done) done();
69
+ return;
70
+ }
71
+ node.runtime.conversion = msg.payload;
72
+ node.status({ fill: "green", shape: "dot", text: `conversion: ${node.runtime.conversion}` });
73
+ if (done) done();
74
+ return;
75
+ }
76
+
77
+ if (done) done();
78
+ return;
79
+ }
80
+
81
+ // Validate input payload
82
+ if (!msg.hasOwnProperty("payload")) {
83
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
84
+ if (done) done();
85
+ return;
86
+ }
87
+
88
+ const inputValue = parseFloat(msg.payload);
89
+ if (isNaN(inputValue) || !isFinite(inputValue)) {
90
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
91
+ if (done) done();
92
+ return;
93
+ }
94
+
95
+ // Perform conversion
96
+ let output, inUnit, outUnit;
97
+ switch (node.runtime.conversion) {
98
+ case "F to C":
99
+ output = (msg.payload - 32) * 5 / 9;
100
+ inUnit = "°F";
101
+ outUnit = "°C";
102
+ break;
103
+ case "C to F":
104
+ output = (msg.payload * 9 / 5) + 32;
105
+ inUnit = "°C";
106
+ outUnit = "°F";
107
+ break;
108
+ case "decimal to %":
109
+ output = msg.payload * 100;
110
+ inUnit = "";
111
+ outUnit = "%";
112
+ break;
113
+ case "% to decimal":
114
+ output = msg.payload / 100;
115
+ inUnit = "%";
116
+ outUnit = "";
117
+ break;
118
+ case "Pa to inH₂O":
119
+ output = msg.payload * 0.00401463;
120
+ inUnit = "Pa";
121
+ outUnit = "inH₂O";
122
+ break;
123
+ case "inH₂O to Pa":
124
+ output = msg.payload / 0.00401463;
125
+ inUnit = "inH₂O";
126
+ outUnit = "Pa";
127
+ break;
128
+ case "Pa to inHg":
129
+ output = msg.payload * 0.0002953;
130
+ inUnit = "Pa";
131
+ outUnit = "inHg";
132
+ break;
133
+ case "inHg to Pa":
134
+ output = msg.payload / 0.0002953;
135
+ inUnit = "inHg";
136
+ outUnit = "Pa";
137
+ break;
138
+ case "Pa to bar":
139
+ output = msg.payload * 0.00001;
140
+ inUnit = "Pa";
141
+ outUnit = "bar";
142
+ break;
143
+ case "bar to Pa":
144
+ output = msg.payload / 0.00001;
145
+ inUnit = "bar";
146
+ outUnit = "Pa";
147
+ break;
148
+ case "Pa to psi":
149
+ output = msg.payload * 0.000145038;
150
+ inUnit = "Pa";
151
+ outUnit = "psi";
152
+ break;
153
+ case "psi to Pa":
154
+ output = msg.payload / 0.000145038;
155
+ inUnit = "psi";
156
+ outUnit = "Pa";
157
+ break;
158
+ case "K to C":
159
+ output = inputValue - 273.15;
160
+ inUnit = "K";
161
+ outUnit = "°C";
162
+ break;
163
+ case "C to K":
164
+ output = inputValue + 273.15;
165
+ inUnit = "°C";
166
+ outUnit = "K";
167
+ break;
168
+ case "K to F":
169
+ output = (inputValue * 9/5) - 459.67;
170
+ inUnit = "K";
171
+ outUnit = "°F";
172
+ break;
173
+ case "F to K":
174
+ output = (inputValue + 459.67) * 5/9;
175
+ inUnit = "°F";
176
+ outUnit = "K";
177
+ break;
178
+ case "R to F":
179
+ output = inputValue - 459.67;
180
+ inUnit = "°R";
181
+ outUnit = "°F";
182
+ break;
183
+ case "F to R":
184
+ output = inputValue + 459.67;
185
+ inUnit = "°F";
186
+ outUnit = "°R";
187
+ break;
188
+ case "m to ft":
189
+ output = inputValue * 3.28084;
190
+ inUnit = "m";
191
+ outUnit = "ft";
192
+ break;
193
+ case "ft to m":
194
+ output = inputValue / 3.28084;
195
+ inUnit = "ft";
196
+ outUnit = "m";
197
+ break;
198
+ case "m to in":
199
+ output = inputValue * 39.3701;
200
+ inUnit = "m";
201
+ outUnit = "in";
202
+ break;
203
+ case "in to m":
204
+ output = inputValue / 39.3701;
205
+ inUnit = "in";
206
+ outUnit = "m";
207
+ break;
208
+ case "mm to in":
209
+ output = inputValue / 25.4;
210
+ inUnit = "mm";
211
+ outUnit = "in";
212
+ break;
213
+ case "in to mm":
214
+ output = inputValue * 25.4;
215
+ inUnit = "in";
216
+ outUnit = "mm";
217
+ break;
218
+ case "kg to lb":
219
+ output = inputValue * 2.20462;
220
+ inUnit = "kg";
221
+ outUnit = "lb";
222
+ break;
223
+ case "lb to kg":
224
+ output = inputValue / 2.20462;
225
+ inUnit = "lb";
226
+ outUnit = "kg";
227
+ break;
228
+ case "L to gal":
229
+ output = inputValue * 0.264172;
230
+ inUnit = "L";
231
+ outUnit = "gal";
232
+ break;
233
+ case "gal to L":
234
+ output = inputValue / 0.264172;
235
+ inUnit = "gal";
236
+ outUnit = "L";
237
+ break;
238
+ case "kW to hp":
239
+ output = inputValue * 1.34102;
240
+ inUnit = "kW";
241
+ outUnit = "hp";
242
+ break;
243
+ case "hp to kW":
244
+ output = inputValue / 1.34102;
245
+ inUnit = "hp";
246
+ outUnit = "kW";
247
+ break;
248
+ case "rad to deg":
249
+ output = inputValue * (180 / Math.PI);
250
+ inUnit = "rad";
251
+ outUnit = "°";
252
+ break;
253
+ case "deg to rad":
254
+ output = inputValue * (Math.PI / 180);
255
+ inUnit = "°";
256
+ outUnit = "rad";
257
+ break;
258
+ case "s to min":
259
+ output = inputValue / 60;
260
+ inUnit = "s";
261
+ outUnit = "min";
262
+ break;
263
+ case "min to s":
264
+ output = inputValue * 60;
265
+ inUnit = "min";
266
+ outUnit = "s";
267
+ break;
268
+ }
269
+
270
+ // Format status numbers
271
+ const inDisplay = msg.payload % 1 === 0 ? msg.payload : msg.payload.toFixed(2);
272
+ const outDisplay = output % 1 === 0 ? output : output.toFixed(2);
273
+
274
+ // Update status and send output
275
+ node.status({ fill: "blue", shape: "dot", text: `${inDisplay} ${inUnit} → ${outDisplay} ${outUnit}` });
276
+
277
+ msg.payload = output;
278
+ send(msg);
279
+
280
+ if (done) done();
281
+ });
282
+
283
+ node.on("close", function(done) {
284
+ done();
285
+ });
286
+ }
287
+
288
+ RED.nodes.registerType("convert-block", ConvertBlockNode);
289
+ };
@@ -0,0 +1,57 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="count-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
+ </script>
8
+
9
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
10
+ <script type="text/javascript">
11
+ RED.nodes.registerType("count-block", {
12
+ category: "control",
13
+ color: "#301934",
14
+ defaults: {
15
+ name: { value: "" }
16
+ },
17
+ inputs: 1,
18
+ outputs: 1,
19
+ inputLabels: ["input"],
20
+ outputLabels: ["count"],
21
+ icon: "font-awesome/fa-tally",
22
+ paletteLabel: "count",
23
+ label: function() {
24
+ return this.name || "count";
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <!-- Help Section -->
30
+ <script type="text/markdown" data-help-name="count-block">
31
+ Counts boolean rising edges in the input.
32
+
33
+ ### Inputs
34
+ : context (string) : Resets count (`"reset"`). Unmatched values trigger error.
35
+ : payload (boolean) : Input boolean to detect rising edges.
36
+
37
+ ### Outputs
38
+ : payload (number) : Current count of rising edges.
39
+
40
+ ### Properties
41
+ : name (string) : Display name in editor.
42
+
43
+ ### Details
44
+ Counts rising edges (false-to-true transitions) in `msg.payload` (boolean), incrementing the count on each transition.
45
+ Resets count to 0 via `msg.context = "reset"` with `msg.payload = true`.
46
+
47
+ ### Status
48
+ - Green (dot): Configuration
49
+ - Blue (dot): Output, no alarm
50
+ - Red (dot): Output with alarm
51
+ - Red (ring): Errors
52
+ - Yellow (ring): Unknown context
53
+
54
+ ### References
55
+ - [Node-RED Documentation](https://nodered.org/docs/)
56
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
57
+ </script>
@@ -0,0 +1,92 @@
1
+ module.exports = function(RED) {
2
+ function CountBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ count: 0,
10
+ prevState: false
11
+ };
12
+
13
+ node.on("input", function(msg, send, done) {
14
+ send = send || function() { node.send.apply(node, arguments); };
15
+
16
+ // Guard against invalid message
17
+ if (!msg) {
18
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
19
+ if (done) done();
20
+ return;
21
+ }
22
+
23
+ // Handle context updates
24
+ if (msg.hasOwnProperty("context")) {
25
+ if (!msg.hasOwnProperty("payload")) {
26
+ node.status({ fill: "red", shape: "ring", text: "missing payload for reset" });
27
+ if (done) done();
28
+ return;
29
+ }
30
+ if (msg.context === "reset") {
31
+ if (typeof msg.payload !== "boolean") {
32
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
33
+ if (done) done();
34
+ return;
35
+ }
36
+ if (msg.payload === true) {
37
+ node.runtime.count = 0;
38
+ node.runtime.prevState = false;
39
+ node.status({ fill: "green", shape: "dot", text: "state reset" });
40
+ send({ payload: node.runtime.count });
41
+ }
42
+ if (done) done();
43
+ return;
44
+ } else {
45
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
46
+ if (done) done("Unknown context");
47
+ return;
48
+ }
49
+ }
50
+
51
+ // Validate input
52
+ if (!msg.hasOwnProperty("payload")) {
53
+ node.status({ fill: "red", shape: "ring", text: "missing input" });
54
+ if (done) done();
55
+ return;
56
+ }
57
+
58
+ const inputValue = msg.payload;
59
+ if (typeof inputValue !== "boolean") {
60
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
61
+ if (done) done();
62
+ return;
63
+ }
64
+
65
+ // Prevent integer overflow
66
+ if (node.runtime.count > Number.MAX_SAFE_INTEGER - 100000) {
67
+ node.runtime.count = 0;
68
+ node.status({ fill: "yellow", shape: "ring", text: "count overflow reset" });
69
+ }
70
+
71
+ // Increment on false → true transition
72
+ if (!node.runtime.prevState && inputValue === true) {
73
+ node.runtime.count++;
74
+ node.status({ fill: "blue", shape: "dot", text: `count: ${node.runtime.count}` });
75
+ send({ payload: node.runtime.count });
76
+ } else {
77
+ node.status({ fill: "blue", shape: "ring", text: `count: ${node.runtime.count}` });
78
+ }
79
+
80
+ // Update prevState
81
+ node.runtime.prevState = inputValue;
82
+
83
+ if (done) done();
84
+ });
85
+
86
+ node.on("close", function(done) {
87
+ done();
88
+ });
89
+ }
90
+
91
+ RED.nodes.registerType("count-block", CountBlockNode);
92
+ };
@@ -0,0 +1,64 @@
1
+ <script type="text/html" data-template-name="debounce-block">
2
+ <div class="form-row">
3
+ <label for="node-input-period" title="Filter period in milliseconds (positive number, e.g., 1000)"><i class="fa fa-clock-o"></i> Filter Period (ms)</label>
4
+ <input type="text" id="node-input-period" placeholder="1000" min="0.001" step="any">
5
+ <input type="hidden" id="node-input-periodType">
6
+
7
+ </div>
8
+ </script>
9
+
10
+ <script type="text/javascript">
11
+ RED.nodes.registerType("debounce-block", {
12
+ category: "control",
13
+ color: "#301934",
14
+ defaults: {
15
+ name: { value: "" },
16
+ period: {
17
+ value: 1000,
18
+ required: true,
19
+ validate: function() { return true; }
20
+ },
21
+ periodType: { value: "num" }
22
+ },
23
+ inputs: 1,
24
+ outputs: 1,
25
+ inputLabels: ["input"],
26
+ outputLabels: ["output"],
27
+ icon: "font-awesome/fa-hourglass-half",
28
+ paletteLabel: "debounce",
29
+ label: function() {
30
+ return this.name || "debounce";
31
+ }
32
+ });
33
+ </script>
34
+
35
+ <script type="text/markdown" data-help-name="debounce-block">
36
+ Debounces consecutive `true` payloads and passes `false` payloads immediately with a configurable filter period.
37
+
38
+ ### Inputs
39
+ : context (string) : Configures filter period (`"period"`).
40
+ : payload (boolean | number) : `true` for debounced output, `false` for immediate output.
41
+
42
+ ### Outputs
43
+ : payload (boolean) : `true` after filter period elapses without new `true` payloads, `false` immediately. `msg.context` is consumed.
44
+
45
+ ### Details
46
+ Filters consecutive `true` `msg.payload` inputs, outputting `true` after the filter period elapses without new `true` payloads.
47
+
48
+ Consecutive `true` payloads within the period reset the timer, delaying output. Outputs `false` `msg.payload` inputs immediately without debouncing.
49
+
50
+ Non-boolean payloads (e.g., strings, numbers) are ignored with an error status.
51
+
52
+ Tracks ignored `true` payloads, which increments for each `true` payload resetting an active timer, capped at 9999, resetting to 0 if exceeded or on redeployment.
53
+
54
+ ### Status
55
+ - Green (dot): Configuration
56
+ - Blue (dot): Output, no alarm
57
+ - Red (dot): Output with alarm
58
+ - Red (ring): Errors
59
+ - Yellow (ring): Unknown context
60
+
61
+ ### References
62
+ - [Node-RED Documentation](https://nodered.org/docs/)
63
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
64
+ </script>
@@ -0,0 +1,140 @@
1
+ module.exports = function(RED) {
2
+ function DebounceBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime for editor display
7
+ node.runtime = {
8
+ debounceCount: 0
9
+ };
10
+
11
+ // Initialize state
12
+ let debounceTimer = null;
13
+ let lastOutput = null;
14
+
15
+ node.on("input", function(msg, send, done) {
16
+ send = send || function() { node.send.apply(node, arguments); };
17
+
18
+ // Evaluate all properties
19
+ try {
20
+ node.runtime.period = RED.util.evaluateNodeProperty(
21
+ config.period, config.periodType, node, msg
22
+ );
23
+ node.runtime.period = parseFloat(node.runtime.period);
24
+
25
+ node.period = parseFloat(node.period);
26
+ if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
27
+ node.period = 1000;
28
+ node.status({ fill: "yellow", shape: "ring", text: "invalid period, using 1000ms" });
29
+ }
30
+ } catch(err) {
31
+ node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
32
+ if (done) done(err);
33
+ return;
34
+ }
35
+
36
+ // Guard against invalid msg
37
+ if (!msg) {
38
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
39
+ if (done) done();
40
+ return;
41
+ }
42
+
43
+ // Handle msg.context
44
+ if (msg.hasOwnProperty("context")) {
45
+ if (!msg.hasOwnProperty("payload")) {
46
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
47
+ if (done) done();
48
+ return;
49
+ }
50
+
51
+ if (msg.context === "period") {
52
+ const newPeriod = parseFloat(msg.payload);
53
+ if (isNaN(newPeriod) || newPeriod <= 0) {
54
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
55
+ if (done) done();
56
+ return;
57
+ }
58
+ node.runtime.period = newPeriod;
59
+ node.status({
60
+ fill: "green",
61
+ shape: "dot",
62
+ text: `period: ${newPeriod.toFixed(0)} ms, bounced: ${node.runtime.debounceCount}`
63
+ });
64
+ if (done) done();
65
+ return;
66
+ }
67
+
68
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
69
+ if (done) done();
70
+ return;
71
+ }
72
+
73
+ // Check for missing payload
74
+ if (!msg.hasOwnProperty("payload")) {
75
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
76
+ if (done) done();
77
+ return;
78
+ }
79
+
80
+ // Process false payloads immediately
81
+ if (msg.payload === false) {
82
+ const statusText = `in: false, out: false, bounced: ${node.runtime.debounceCount}`;
83
+ if (lastOutput === false) {
84
+ node.status({ fill: "blue", shape: "ring", text: statusText });
85
+ } else {
86
+ node.status({ fill: "blue", shape: "dot", text: statusText });
87
+ }
88
+ lastOutput = false;
89
+ delete msg.context;
90
+ send(msg);
91
+ if (done) done();
92
+ return;
93
+ }
94
+
95
+ // Process true payloads with debouncing
96
+ if (msg.payload === true) {
97
+ // Increment debounce counter if resetting an active timer
98
+ if (debounceTimer) {
99
+ node.runtime.debounceCount++;
100
+ if (node.runtime.debounceCount > 9999) {
101
+ node.runtime.debounceCount = 0;
102
+ }
103
+ }
104
+
105
+ // Clear existing timer
106
+ if (debounceTimer) {
107
+ clearTimeout(debounceTimer);
108
+ }
109
+
110
+ // Set new debounce timer
111
+ debounceTimer = setTimeout(() => {
112
+ debounceTimer = null;
113
+ const statusText = `in: true, out: true, bounced: ${node.runtime.debounceCount}`;
114
+ node.status({ fill: "blue", shape: "dot", text: statusText });
115
+ lastOutput = true;
116
+ delete msg.context;
117
+ send(msg);
118
+ }, node.runtime.period);
119
+
120
+ if (done) done();
121
+ return;
122
+ }
123
+
124
+ // Ignore non-boolean payloads
125
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
126
+ if (done) done();
127
+ });
128
+
129
+ node.on("close", function(done) {
130
+ if (debounceTimer) {
131
+ clearTimeout(debounceTimer);
132
+ debounceTimer = null;
133
+ }
134
+
135
+ done();
136
+ });
137
+ }
138
+
139
+ RED.nodes.registerType("debounce-block", DebounceBlockNode);
140
+ };