@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,111 @@
1
+ <script type="text/html" data-template-name="nullify-block">
2
+ <div class="form-row">
3
+ <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Name">
5
+ </div>
6
+ <div class="form-row">
7
+ <label><i class="fa fa-eraser"></i> Rules</label>
8
+ <ol id="node-input-rule-container"></ol>
9
+ </div>
10
+ </script>
11
+
12
+ <script type="text/javascript">
13
+ RED.nodes.registerType("nullify-block", {
14
+ category: "control",
15
+ color: "#301934",
16
+ defaults: {
17
+ name: { value: "" },
18
+ rules: { value: [{ property: "payload", propertyType: "msg" }], required: true }
19
+ },
20
+ inputs: 1,
21
+ outputs: 1,
22
+ inputLabels: ["input"],
23
+ outputLabels: ["null"],
24
+ icon: "font-awesome/fa-eraser",
25
+ paletteLabel: "nullify",
26
+ label: function() {
27
+ return this.name || "Nullify";
28
+ },
29
+ oneditprepare: function() {
30
+ const node = this;
31
+ const $container = $("#node-input-rule-container");
32
+
33
+ // Initialize rule list
34
+ function addRule(rule) {
35
+ const index = $container.find("li").length;
36
+ const $li = $("<li>", { class: "node-input-rule-row" }).appendTo($container);
37
+ const $property = $("<input>", { type: "text", class: "node-input-rule-property" }).appendTo($li);
38
+ const $delete = $("<a>", { href: "#", class: "red-ui-button red-ui-button-small", style: "margin-left: 5px" })
39
+ .html('<i class="fa fa-remove"></i>')
40
+ .appendTo($li);
41
+
42
+ $property.typedInput({
43
+ default: rule.propertyType || "msg",
44
+ types: ["msg"],
45
+ typeField: $("<input>", { type: "hidden", class: "node-input-rule-property-type" }).appendTo($li)
46
+ }).typedInput("value", rule.property || "");
47
+
48
+ $delete.click(function(e) {
49
+ e.preventDefault();
50
+ $li.remove();
51
+ });
52
+ }
53
+
54
+ // Populate existing rules
55
+ (node.rules || [{ property: "payload", propertyType: "msg" }]).forEach(addRule);
56
+
57
+ // Add new rule button
58
+ $("<a>", { href: "#", class: "red-ui-button", style: "margin-top: 5px" })
59
+ .text("Add rule")
60
+ .click(function(e) {
61
+ e.preventDefault();
62
+ addRule({ property: "", propertyType: "msg" });
63
+ })
64
+ .insertAfter($container);
65
+ },
66
+ oneditsave: function() {
67
+ const rules = [];
68
+ $("#node-input-rule-container").find(".node-input-rule-row").each(function() {
69
+ const $property = $(this).find(".node-input-rule-property");
70
+ const property = $property.typedInput("value");
71
+ const propertyType = $property.typedInput("type");
72
+ if (property) {
73
+ rules.push({ property, propertyType });
74
+ }
75
+ });
76
+ this.rules = rules.length > 0 ? rules : [{ property: "payload", propertyType: "msg" }];
77
+ }
78
+ });
79
+ </script>
80
+
81
+ <script type="text/markdown" data-help-name="nullify-block">
82
+ Sets multiple specified message properties to null.
83
+
84
+ ### Inputs
85
+ : context (string) : Configures rules (`"rules"`). Unmatched values ignored.
86
+ : payload (any) : Input message with properties to nullify.
87
+
88
+ ### Outputs
89
+ : payload (any) : Input message with specified properties set to `null`.
90
+ : *other* (any) : Other input properties preserved.
91
+
92
+ ### Details
93
+ Sets multiple configured `msg` properties to `null`, creating each property if it doesn't exist.
94
+
95
+ Properties are specified via a dynamic rule list in the editor, each defining a `msg` property.
96
+
97
+ Outputs the modified input message, preserving all other properties.
98
+
99
+ Invalid inputs (missing message, invalid rule configuration) prevent output and show error status.
100
+
101
+ ### Status
102
+ - Green (dot): Configuration update
103
+ - Blue (dot): State changed
104
+ - Blue (ring): State unchanged
105
+ - Red (ring): Error
106
+ - Yellow (ring): Warning
107
+
108
+ ### References
109
+ - [Node-RED Documentation](https://nodered.org/docs/)
110
+ - [GitHub Repository](https://github.com/your-repo/node-red-contrib-buildingblocks-control)
111
+ </script>
@@ -0,0 +1,78 @@
1
+ module.exports = function(RED) {
2
+ function NullifyBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ rules: config.rules
10
+ };
11
+
12
+ // Validate configuration
13
+ let valid = true;
14
+ node.runtime.rules = node.runtime.rules.map(rule => {
15
+ if (rule.propertyType !== "msg" || !rule.property || typeof rule.property !== "string" || !rule.property.trim()) {
16
+ valid = false;
17
+ return { property: "payload", propertyType: "msg" };
18
+ }
19
+ return rule;
20
+ });
21
+ if (!valid) {
22
+ node.status({ fill: "red", shape: "ring", text: "invalid rules, using defaults" });
23
+ } else {
24
+ node.status({ fill: "green", shape: "dot", text: `rules: ${node.runtime.rules.map(r => r.property).join(", ")}` });
25
+ }
26
+
27
+ node.on("input", function(msg, send, done) {
28
+ send = send || function() { node.send.apply(node, arguments); };
29
+
30
+ // Guard against invalid message
31
+ if (!msg) {
32
+ node.status({ fill: "red", shape: "ring", text: "missing message" });
33
+ if (done) done();
34
+ return;
35
+ }
36
+
37
+ // Handle configuration messages
38
+ if (msg.context) {
39
+ if (typeof msg.context !== "string" || !msg.context.trim()) {
40
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
41
+ if (done) done();
42
+ return;
43
+ }
44
+ if (msg.context === "rules") {
45
+ if (!msg.hasOwnProperty("payload") || !Array.isArray(msg.payload) || !msg.payload.every(r => r.property && typeof r.property === "string" && r.propertyType === "msg")) {
46
+ node.status({ fill: "red", shape: "ring", text: "invalid rules" });
47
+ if (done) done();
48
+ return;
49
+ }
50
+ node.runtime.rules = msg.payload;
51
+ node.status({ fill: "green", shape: "dot", text: `rules: ${node.runtime.rules.map(r => r.property).join(", ")}` });
52
+ if (done) done();
53
+ return;
54
+ }
55
+ }
56
+
57
+ // Apply nullification rules
58
+ const outputMsg = RED.util.cloneMessage(msg);
59
+ const nullified = [];
60
+ node.runtime.rules.forEach(rule => {
61
+ RED.util.setMessageProperty(outputMsg, rule.property, null);
62
+ nullified.push(rule.property);
63
+ });
64
+
65
+ // Update status and send output
66
+ node.status({ fill: "blue", shape: "dot", text: `nullified: ${nullified.join(", ")}` });
67
+ send(outputMsg);
68
+
69
+ if (done) done();
70
+ });
71
+
72
+ node.on("close", function(done) {
73
+ done();
74
+ });
75
+ }
76
+
77
+ RED.nodes.registerType("nullify-block", NullifyBlockNode);
78
+ };
@@ -0,0 +1,79 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="on-change-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-period" title="Filter period in milliseconds (non-negative number from msg, flow, global, or static value)"><i class="fa fa-clock-o"></i> Filter Period (ms)</label>
9
+ <input type="text" id="node-input-period" placeholder="0">
10
+ <input type="hidden" id="node-input-periodType">
11
+ </div>
12
+ </script>
13
+
14
+ <!-- JavaScript Section -->
15
+ <script type="text/javascript">
16
+ RED.nodes.registerType("on-change-block", {
17
+ category: "control",
18
+ color: "#301934",
19
+ defaults: {
20
+ name: { value: "" },
21
+ period: { value: 0 },
22
+ periodType: { value: "num" }
23
+ },
24
+ inputs: 1,
25
+ outputs: 1,
26
+ inputLabels: ["input"],
27
+ outputLabels: ["output"],
28
+ icon: "font-awesome/fa-filter",
29
+ paletteLabel: "on change",
30
+ label: function() {
31
+ return this.name || "on change";
32
+ },
33
+ oneditprepare: function() {
34
+ const periodInput = $("#node-input-period").typedInput({
35
+ default: "num",
36
+ types: ["num", "msg", "flow", "global"],
37
+ typeField: "#node-input-periodType"
38
+ });
39
+ }
40
+ });
41
+ </script>
42
+
43
+ <!-- Help Section -->
44
+ <script type="text/markdown" data-help-name="on-change-block">
45
+ Filters redundant messages based on value changes and a configurable period.
46
+
47
+ ### Inputs
48
+ : payload (any) : Input value to compare.
49
+ : context (string, optional) : Configures period (`"period"`) or queries state (`"status"`). Unknown `msg.context` is ignored
50
+ : payload (number | any, for `context`) : Non-negative number for `"period"` (ms), any for `"status"`.
51
+
52
+ ### Outputs
53
+ : msg (any) : Input message if value changed and not filtered.
54
+ : payload (object, for `msg.context = "status"`) : `{ period, periodType }`.
55
+
56
+ ### Properties
57
+ : name (string) : Display name in editor. Default "" (shows "on change").
58
+ : period (number) : Filter period in milliseconds (≥ 0, static or from `msg`, `flow`, `global`). Default 0.
59
+ : periodType (string) : Source of period (`"num"`, `"msg"`, `"flow"`, `"global"`). Default `"num"`.
60
+
61
+ ### Details
62
+ Filters messages based on value changes (deep comparison) and a filter period. When `period = 0`, outputs the input message only if `msg.payload`
63
+ differs from the last output value. When `period > 0`, outputs the first message, suppresses all messages during the period (including blips),
64
+ and after the period, outputs only if the value differs from the last output. Supports complex payloads (objects, arrays) via deep comparison.
65
+ Configuration
66
+ - `msg.context = "period"` Sets period (ms), no output.
67
+ - `msg.context = "status"` Outputs `{ period, periodType }`.
68
+
69
+ ### Status
70
+ - Green (dot): Configuration update
71
+ - Blue (dot): State changed
72
+ - Blue (ring): State unchanged
73
+ - Red (ring): Error
74
+ - Yellow (ring): Warning
75
+
76
+ ### References
77
+ - [Node-RED Documentation](https://nodered.org/docs/)
78
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
79
+ </script>
@@ -0,0 +1,191 @@
1
+ module.exports = function(RED) {
2
+ function OnChangeBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ period: Number(config.period),
10
+ periodType: config.periodType,
11
+ lastValue: null,
12
+ periodValue: null,
13
+ blockTimer: null,
14
+ pendingMsg: null
15
+ };
16
+
17
+ // Validate initial config
18
+ if (isNaN(node.runtime.period) || node.runtime.period < 0) {
19
+ node.runtime.period = 0;
20
+ node.status({ fill: "red", shape: "ring", text: "invalid period, using 0" });
21
+ } else {
22
+ node.status({ fill: "green", shape: "dot", text: `name: ${node.runtime.name || "on change"}, period: ${node.runtime.period.toFixed(0)} ms` });
23
+ }
24
+
25
+ node.on("input", function(msg, send, done) {
26
+ send = send || function() { node.send.apply(node, arguments); };
27
+
28
+ // Guard against invalid message
29
+ if (!msg) {
30
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
31
+ if (done) done();
32
+ return;
33
+ }
34
+
35
+ // Handle context updates
36
+ if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
37
+ const contextLower = msg.context.toLowerCase();
38
+ if (contextLower === "period") {
39
+ if (!msg.hasOwnProperty("payload")) {
40
+ node.status({ fill: "red", shape: "ring", text: "missing payload for period" });
41
+ if (done) done();
42
+ return;
43
+ }
44
+ const newPeriod = parseFloat(msg.payload);
45
+ if (isNaN(newPeriod) || newPeriod < 0) {
46
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
47
+ if (done) done();
48
+ return;
49
+ }
50
+ node.runtime.period = newPeriod;
51
+ node.runtime.periodType = "num";
52
+ node.status({
53
+ fill: "green",
54
+ shape: "dot",
55
+ text: `period: ${node.runtime.period.toFixed(0)} ms`
56
+ });
57
+ if (done) done();
58
+ return;
59
+ }
60
+ if (contextLower === "status") {
61
+ send({
62
+ payload: {
63
+ period: node.runtime.period,
64
+ periodType: node.runtime.periodType
65
+ }
66
+ });
67
+ if (done) done();
68
+ return;
69
+ }
70
+ // Ignore unknown context
71
+ }
72
+
73
+ // Validate input payload
74
+ if (!msg.hasOwnProperty("payload")) {
75
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
76
+ send(msg);
77
+ if (done) done();
78
+ return;
79
+ }
80
+
81
+ // Get period
82
+ let period;
83
+ try {
84
+ period = RED.util.evaluateNodeProperty(
85
+ node.runtime.period,
86
+ node.runtime.periodType,
87
+ node,
88
+ msg
89
+ );
90
+ if (isNaN(period) || period < 0) {
91
+ throw new Error("invalid period");
92
+ }
93
+ } catch (err) {
94
+ node.status({ fill: "red", shape: "ring", text: "invalid period" });
95
+ send(msg);
96
+ if (done) done();
97
+ return;
98
+ }
99
+
100
+ const currentValue = msg.payload;
101
+
102
+ // Deep comparison function
103
+ function isEqual(a, b) {
104
+ if (a === b) return true;
105
+ if (typeof a !== typeof b) return false;
106
+ if (Array.isArray(a) && Array.isArray(b)) {
107
+ if (a.length !== b.length) return false;
108
+ return a.every((item, i) => isEqual(item, b[i]));
109
+ }
110
+ if (typeof a === "object" && a !== null && b !== null) {
111
+ const keysA = Object.keys(a);
112
+ const keysB = Object.keys(b);
113
+ if (keysA.length !== keysB.length) return false;
114
+ return keysA.every(key => isEqual(a[key], b[key]));
115
+ }
116
+ return false;
117
+ }
118
+
119
+ // Handle input during filter period
120
+ if (node.runtime.blockTimer) {
121
+ node.runtime.pendingMsg = RED.util.cloneMessage(msg);
122
+ node.status({
123
+ fill: "blue",
124
+ shape: "ring",
125
+ text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)}`
126
+ });
127
+ if (done) done();
128
+ return;
129
+ }
130
+
131
+ // Check if value changed
132
+ if (!isEqual(currentValue, node.runtime.lastValue)) {
133
+ node.runtime.lastValue = RED.util.cloneMessage(currentValue);
134
+ node.runtime.periodValue = RED.util.cloneMessage(currentValue);
135
+ node.status({
136
+ fill: "blue",
137
+ shape: "dot",
138
+ text: `out: ${JSON.stringify(currentValue).slice(0, 20)}`
139
+ });
140
+ send(msg);
141
+
142
+ // Start filter period if applicable
143
+ if (period > 0) {
144
+ node.runtime.blockTimer = setTimeout(() => {
145
+ node.runtime.blockTimer = null;
146
+ if (node.runtime.pendingMsg) {
147
+ const pendingValue = node.runtime.pendingMsg.payload;
148
+ if (!isEqual(pendingValue, node.runtime.lastValue)) {
149
+ node.runtime.lastValue = RED.util.cloneMessage(pendingValue);
150
+ node.runtime.periodValue = RED.util.cloneMessage(pendingValue);
151
+ node.status({
152
+ fill: "blue",
153
+ shape: "dot",
154
+ text: `out: ${JSON.stringify(pendingValue).slice(0, 20)}`
155
+ });
156
+ send(node.runtime.pendingMsg);
157
+ } else {
158
+ node.status({
159
+ fill: "blue",
160
+ shape: "ring",
161
+ text: `unchanged: ${JSON.stringify(pendingValue).slice(0, 20)}`
162
+ });
163
+ }
164
+ node.runtime.pendingMsg = null;
165
+ } else {
166
+ node.status({});
167
+ }
168
+ }, period);
169
+ }
170
+ } else {
171
+ node.status({
172
+ fill: "blue",
173
+ shape: "ring",
174
+ text: `unchanged: ${JSON.stringify(currentValue).slice(0, 20)}`
175
+ });
176
+ }
177
+
178
+ if (done) done();
179
+ });
180
+
181
+ node.on("close", function(done) {
182
+ if (node.runtime.blockTimer) {
183
+ clearTimeout(node.runtime.blockTimer);
184
+ node.runtime.blockTimer = null;
185
+ }
186
+ done();
187
+ });
188
+ }
189
+
190
+ RED.nodes.registerType("on-change-block", OnChangeBlockNode);
191
+ };
@@ -0,0 +1,96 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="oneshot-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-duration" title="Duration of true pulse (positive number)"><i class="fa fa-clock-o"></i> Duration</label>
9
+ <input type="number" id="node-input-duration" placeholder="1000" min="0" step="any">
10
+ <select id="node-input-durationUnits">
11
+ <option value="milliseconds">Milliseconds</option>
12
+ <option value="seconds">Seconds</option>
13
+ <option value="minutes">Minutes</option>
14
+ </select>
15
+ </div>
16
+ <div class="form-row">
17
+ <label for="node-input-resetRequireTrue" title="Require msg.payload = true for reset"><i class="fa fa-check"></i> Reset Require True</label>
18
+ <input type="checkbox" id="node-input-resetRequireTrue" style="width: auto; vertical-align: middle;" checked>
19
+ </div>
20
+ <div class="form-row">
21
+ <label for="node-input-resetOnComplete" title="Automatically reset (unlock) after pulse duration"><i class="fa fa-undo"></i> Reset On Complete</label>
22
+ <input type="checkbox" id="node-input-resetOnComplete" style="width: auto; vertical-align: middle;">
23
+ </div>
24
+ </script>
25
+
26
+ <!-- JavaScript Section -->
27
+ <script type="text/javascript">
28
+ RED.nodes.registerType("oneshot-block", {
29
+ category: "control",
30
+ color: "#301934",
31
+ defaults: {
32
+ name: { value: "" },
33
+ duration: {
34
+ value: 1000,
35
+ required: true,
36
+ validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) > 0; }
37
+ },
38
+ durationUnits: { value: "milliseconds" },
39
+ resetRequireTrue: { value: true },
40
+ resetOnComplete: { value: false }
41
+ },
42
+ inputs: 1,
43
+ outputs: 1,
44
+ inputLabels: ["trigger/reset"],
45
+ outputLabels: ["pulse"],
46
+ icon: "font-awesome/fa-bolt",
47
+ paletteLabel: "oneshot",
48
+ label: function() {
49
+ return this.name || `oneshot (${this.duration}${this.durationUnits.charAt(0)})`;
50
+ }
51
+ });
52
+ </script>
53
+
54
+ <!-- Help Section -->
55
+ <script type="text/markdown" data-help-name="oneshot-block">
56
+ Outputs a true pulse for a configurable duration when triggered with `msg.payload = true`, then false, and locks until reset.
57
+
58
+ ### Inputs
59
+ : context (string) : Configures node (`"reset"`, `"duration"`). Unmatched values trigger error.
60
+ : payload (boolean | number) : `true` triggers pulse if not locked; number for `"duration"`; boolean for `"reset"`.
61
+ : units (string) : Units for `"duration"` (`"milliseconds"`, `"seconds"`, `"minutes"`).
62
+
63
+ ### Outputs
64
+ : payload (boolean) : `true` for `duration` on trigger, then `false`; `false` on reset or if locked.
65
+
66
+ ### Properties
67
+ : name (string) : Display name in editor.
68
+ : duration (number) : Pulse duration (positive number).
69
+ : durationUnits (string) : Units for duration (`"milliseconds"`, `"seconds"`, `"minutes"`).
70
+ : resetRequireTrue (boolean) : Require `msg.payload = true` for reset.
71
+ : resetOnComplete (boolean) : Automatically reset (unlock) after pulse duration.
72
+
73
+ ### Details
74
+ Generates a `true` pulse for `duration` when triggered with `msg.payload = true`,
75
+ then outputs `false`.
76
+
77
+ Locks until reset via `msg.context = "reset"` (requires `msg.payload = true` if `resetRequireTrue = true`) or
78
+ automatically if `resetOnComplete = true`. Non-`true` payloads are ignored with no output.
79
+
80
+ Duration configurable via editor or `msg.context = "duration"` with `msg.units` (`"milliseconds"`, `"seconds"`, `"minutes"`).
81
+
82
+ Tracks `triggerCount` (number of triggers) and displays in status.
83
+
84
+ Outputs new `{ payload boolean }` messages for every trigger (`true` then `false`), reset, or locked input.
85
+
86
+ ### Status
87
+ - Green (dot): Configuration update
88
+ - Blue (dot): State changed
89
+ - Blue (ring): State unchanged
90
+ - Red (ring): Error
91
+ - Yellow (ring): Warning
92
+
93
+ ### References
94
+ - [Node-RED Documentation](https://nodered.org/docs/)
95
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
96
+ </script>