@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,73 @@
1
+ module.exports = function(RED) {
2
+ function AndBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize state
7
+ node.inputs = Array(parseInt(config.slots) || 2).fill(false);
8
+ node.slots = parseInt(config.slots);
9
+
10
+ node.status({ fill: "green", shape: "dot", text: `slots: ${node.slots}` });
11
+
12
+ // Initialize fields
13
+ let lastResult = null;
14
+ let lastInputs = node.inputs.slice();
15
+
16
+ node.on("input", function(msg, send, done) {
17
+ send = send || function() { node.send.apply(node, arguments); };
18
+
19
+ // Guard against invalid msg
20
+ if (!msg) {
21
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
22
+ if (done) done();
23
+ return;
24
+ }
25
+
26
+ // Check required properties
27
+ if (!msg.hasOwnProperty("context")) {
28
+ node.status({ fill: "red", shape: "ring", text: "missing context" });
29
+ if (done) done();
30
+ return;
31
+ }
32
+
33
+ if (!msg.hasOwnProperty("payload")) {
34
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
35
+ if (done) done();
36
+ return;
37
+ }
38
+
39
+ // Process input slot
40
+ if (msg.context.startsWith("in")) {
41
+ let index = parseInt(msg.context.slice(2), 10);
42
+ if (!isNaN(index) && index >= 1 && index <= node.slots) {
43
+ node.inputs[index - 1] = Boolean(msg.payload);
44
+ const result = node.inputs.every(v => v === true);
45
+ const isUnchanged = result === lastResult && node.inputs.every((v, i) => v === lastInputs[i]);
46
+ node.status({
47
+ fill: "blue",
48
+ shape: isUnchanged ? "ring" : "dot",
49
+ text: `in: [${node.inputs.join(", ")}], out: ${result}`
50
+ });
51
+ lastResult = result;
52
+ lastInputs = node.inputs.slice();
53
+ send({ payload: result });
54
+ if (done) done();
55
+ return;
56
+ } else {
57
+ node.status({ fill: "red", shape: "ring", text: `invalid input index ${index || "NaN"}` });
58
+ if (done) done();
59
+ return;
60
+ }
61
+ }
62
+
63
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
64
+ if (done) done();
65
+ });
66
+
67
+ node.on("close", function(done) {
68
+ done();
69
+ });
70
+ }
71
+
72
+ RED.nodes.registerType("and-block", AndBlockNode);
73
+ };
@@ -0,0 +1,97 @@
1
+ <script type="text/html" data-template-name="average-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 for="node-input-sampleSize" title="Rolling window size (positive integer, e.g., 10)"><i class="fa fa-list-ol"></i> Window Size</label>
8
+ <input type="number" id="node-input-sampleSize" placeholder="10" min="1" step="1">
9
+ </div>
10
+ <div class="form-row">
11
+ <label for="node-input-minValid"><i class="fa fa-arrow-down"></i> Minimum Valid</label>
12
+ <input type="text" id="node-input-minValid" placeholder="150">
13
+ <input type="hidden" id="node-input-minValidType">
14
+ </div>
15
+ <div class="form-row">
16
+ <label for="node-input-maxValid"><i class="fa fa-arrow-up"></i> Maximum Valid</label>
17
+ <input type="text" id="node-input-maxValid" placeholder="1">
18
+ <input type="hidden" id="node-input-maxValidType">
19
+ </div>
20
+ </script>
21
+
22
+ <script type="text/javascript">
23
+ RED.nodes.registerType("average-block", {
24
+ category: "control",
25
+ color: "#301934",
26
+ defaults: {
27
+ name: { value: "" },
28
+ sampleSize: {
29
+ value: 10,
30
+ required: true,
31
+ validate: function(v) { return !isNaN(parseInt(v)) && parseInt(v) >= 1; }
32
+ },
33
+ minValid: { value: 1, required: true },
34
+ minValidType: { value: "num" },
35
+ maxValid: { value: 150, required: true },
36
+ maxValidType: { value: "num" }
37
+ },
38
+ inputs: 1,
39
+ outputs: 1,
40
+ inputLabels: ["input"],
41
+ outputLabels: ["average"],
42
+ icon: "join.svg",
43
+ paletteLabel: "average",
44
+ label: function() {
45
+ return this.name ? `${this.name} (${this.sampleSize})` : `average (${this.sampleSize})`;
46
+ },
47
+ oneditprepare: function() {
48
+ const node = this;
49
+
50
+ try {
51
+ // Initialize typed inputs
52
+ $("#node-input-minValid").typedInput({
53
+ default: "num",
54
+ types: ["num", "msg", "flow", "global"],
55
+ typeField: "#node-input-minValidType"
56
+ }).typedInput("type", node.minValidType || "num").typedInput("value", node.minValid || "1");
57
+
58
+ $("#node-input-maxValid").typedInput({
59
+ default: "num",
60
+ types: ["num", "msg", "flow", "global"],
61
+ typeField: "#node-input-maxValidType"
62
+ }).typedInput("type", node.maxValidType || "num").typedInput("value", node.maxValid || "150");
63
+ } catch (err) {
64
+ console.error("Error in hysteresis-block oneditprepare:", err);
65
+ }
66
+ }
67
+ });
68
+ </script>
69
+
70
+ <script type="text/markdown" data-help-name="average-block">
71
+ Computes a rolling average of numeric inputs within a valid range.
72
+
73
+ ### Inputs
74
+ : context (string) : Configures reset (`"reset"`) or window size (`"sampleSize"`).
75
+ : payload (number | boolean) : Number for averaging, boolean for reset.
76
+ : minValid (number) : Minimum valid range for payload input.
77
+ : maxValid (number) : Maximum valid range for payload input.
78
+
79
+ ### Outputs
80
+ : payload (number | null) : Rolling average, or `null` if no valid inputs.
81
+
82
+ ### Details
83
+ Averages numeric `msg.payload` values over a rolling window (default: 10).
84
+
85
+ Resets state (`sum`, `count`) via `msg.context = "reset"` with `msg.payload = true`.
86
+
87
+ ### Status
88
+ - Green (dot): Configuration update
89
+ - Blue (dot): State changed
90
+ - Blue (ring): State unchanged
91
+ - Red (ring): Error
92
+ - Yellow (ring): Warning
93
+
94
+ ### References
95
+ - [Node-RED Documentation](https://nodered.org/docs/)
96
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
97
+ </script>
@@ -0,0 +1,137 @@
1
+ module.exports = function(RED) {
2
+ function AverageBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ maxValues: parseInt(config.sampleSize),
9
+ values: [], // Queue for rolling window
10
+ lastAvg: null
11
+ };
12
+
13
+ // Validate initial config
14
+ if (isNaN(node.runtime.maxValues) || node.runtime.maxValues < 1) {
15
+ node.runtime.maxValues = 10;
16
+ node.status({ fill: "red", shape: "ring", text: "invalid window size, using 10" });
17
+ } else {
18
+ node.status({ shape: "dot", text: `name: ${config.name}, window: ${config.sampleSize}` });
19
+ }
20
+
21
+ node.on("input", function(msg, send, done) {
22
+ send = send || function() { node.send.apply(node, arguments); };
23
+
24
+ // Guard against invalid msg
25
+ if (!msg) {
26
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
27
+ if (done) done();
28
+ return;
29
+ }
30
+
31
+ // Evaluate all properties
32
+ try {
33
+ node.runtime.minValid = RED.util.evaluateNodeProperty(
34
+ config.minValid, config.minValidType, node, msg
35
+ );
36
+
37
+ node.runtime.maxValid = RED.util.evaluateNodeProperty(
38
+ config.maxValid, config.maxValidType, node, msg
39
+ );
40
+
41
+ // Validate values
42
+ if (isNaN(node.runtime.maxValid) || isNaN(node.runtime.minValid) || node.runtime.maxValid <= node.runtime.minValid ) {
43
+ node.status({ fill: "red", shape: "ring", text: `invalid evaluated values ${node.runtime.minValid}, ${node.runtime.maxValid}` });
44
+ if (done) done();
45
+ return;
46
+ }
47
+ } catch(err) {
48
+ node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
49
+ if (done) done(err);
50
+ return;
51
+ }
52
+
53
+ // Handle configuration messages
54
+ if (msg.hasOwnProperty("context")) {
55
+ if (!msg.hasOwnProperty("payload")) {
56
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
57
+ if (done) done();
58
+ return;
59
+ }
60
+
61
+ switch (msg.context) {
62
+ case "reset":
63
+ if (typeof msg.payload !== "boolean") {
64
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
65
+ if (done) done();
66
+ return;
67
+ }
68
+ if (msg.payload === true) {
69
+ node.runtime.values = [];
70
+ node.runtime.lastAvg = null;
71
+ node.status({ fill: "green", shape: "dot", text: "state reset" });
72
+ }
73
+ break;
74
+
75
+ case "sampleSize":
76
+ let newMaxValues = parseInt(msg.payload);
77
+ if (isNaN(newMaxValues) || newMaxValues < 1) {
78
+ node.status({ fill: "red", shape: "ring", text: "invalid window size" });
79
+ if (done) done();
80
+ return;
81
+ }
82
+ node.runtime.maxValues = newMaxValues;
83
+ // Trim values if new window is smaller
84
+ if (node.runtime.values.length > newMaxValues) {
85
+ node.runtime.values = node.runtime.values.slice(-newMaxValues);
86
+ }
87
+ node.status({ fill: "green", shape: "dot", text: `window: ${newMaxValues}` });
88
+ break;
89
+
90
+ default:
91
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
92
+ break;
93
+ }
94
+ if (done) done();
95
+ return;
96
+ }
97
+
98
+ // Check for missing payload
99
+ if (!msg.hasOwnProperty("payload")) {
100
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
101
+ if (done) done();
102
+ return;
103
+ }
104
+
105
+ // Process input
106
+ const inputValue = parseFloat(msg.payload);
107
+ if (isNaN(inputValue) || inputValue < node.runtime.minValid || inputValue > node.runtime.maxValid) {
108
+ node.status({ fill: "yellow", shape: "ring", text: "out of range" });
109
+ if (done) done();
110
+ return;
111
+ }
112
+
113
+ // Update rolling window
114
+ node.runtime.values.push(inputValue);
115
+ if (node.runtime.values.length > node.runtime.maxValues) {
116
+ node.runtime.values.shift();
117
+ }
118
+
119
+ // Calculate average
120
+ const avg = node.runtime.values.length ? node.runtime.values.reduce((a, b) => a + b, 0) / node.runtime.values.length : null;
121
+ const isUnchanged = avg === node.runtime.lastAvg;
122
+
123
+ // Send new message
124
+ node.status({ fill: "blue", shape: isUnchanged ? "ring" : "dot", text: `out: ${avg !== null ? avg.toFixed(3) : "null"}` });
125
+ node.runtime.lastAvg = avg;
126
+ send({ payload: avg });
127
+
128
+ if (done) done();
129
+ });
130
+
131
+ node.on("close", function(done) {
132
+ done();
133
+ });
134
+ }
135
+
136
+ RED.nodes.registerType("average-block", AverageBlockNode);
137
+ };
@@ -0,0 +1,59 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="boolean-switch-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 -->
10
+ <script type="text/javascript">
11
+ RED.nodes.registerType("boolean-switch-block", {
12
+ category: "control",
13
+ color: "#301934",
14
+ defaults: {
15
+ name: { value: "" }
16
+ },
17
+ inputs: 1,
18
+ outputs: 3,
19
+ inputLabels: ["control"],
20
+ outputLabels: ["outTrue", "outFalse", "outControl"],
21
+ icon: "font-awesome/fa-toggle-on",
22
+ paletteLabel: "boolean switch",
23
+ label: function() {
24
+ return this.name || "boolean switch";
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <!-- Help Section -->
30
+ <script type="text/markdown" data-help-name="boolean-switch-block">
31
+ Routes input flows to one of three outputs based on a boolean switch state.
32
+
33
+ ### Inputs
34
+ : context (string) : Configuration commands (`toggle`, `switch`, `inTrue`, `inFalse`).
35
+ : payload (any) : Flow data for `"inTrue"` or `"inFalse"`.
36
+
37
+ ### Outputs
38
+ : outTrue (msg) : Receives `msg` with `context "inTrue"` when switch is `true`.
39
+ : outFalse (msg) : Receives `msg` with `context "inFalse"` when switch is `false`.
40
+ : outControl (msg) : `msg.payload` set to switch state on toggle.
41
+
42
+ ### Details
43
+ Routes input messages to one of three outputs based on a boolean switch state (`true` or `false`).
44
+
45
+ Messages with `msg.context = "inTrue"` are sent to `outTrue` when `state = true`, and messages with `msg.context = "inFalse"` are sent to `outFalse` when `state = false`.
46
+
47
+ State can be controlled directly with the `"switch"` property given a `msg.payload` boolean value.
48
+
49
+ ### Status
50
+ - Green (dot): Configuration update
51
+ - Blue (dot): State changed
52
+ - Blue (ring): State unchanged
53
+ - Red (ring): Error
54
+ - Yellow (ring): Warning
55
+
56
+ ### References
57
+ - [Node-RED Documentation](https://nodered.org/docs/)
58
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
59
+ </script>
@@ -0,0 +1,88 @@
1
+ module.exports = function(RED) {
2
+ function BooleanSwitchBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize state from config
7
+ node.state = config.state !== undefined ? Boolean(config.state) : false;
8
+
9
+ // Set initial status
10
+ node.status({
11
+ fill: "green",
12
+ shape: "dot",
13
+ text: `state: ${node.state}`
14
+ });
15
+
16
+ node.on("input", function(msg, send, done) {
17
+ send = send || function() { node.send.apply(node, arguments); };
18
+
19
+ // Guard against invalid message
20
+ if (!msg) {
21
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
22
+ if (done) done();
23
+ return;
24
+ }
25
+
26
+ // Validate context
27
+ if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
28
+ node.status({ fill: "red", shape: "ring", text: "missing or invalid context" });
29
+ if (done) done();
30
+ return;
31
+ }
32
+
33
+ // Handle context commands
34
+ switch (msg.context) {
35
+ case "toggle":
36
+ node.state = !node.state;
37
+ node.status({
38
+ fill: "green",
39
+ shape: "dot",
40
+ text: `state: ${node.state}`
41
+ });
42
+ send([null, null, { payload: node.state }]);
43
+ break;
44
+ case "switch":
45
+ node.state = !!msg.payload;
46
+ node.status({
47
+ fill: "green",
48
+ shape: "dot",
49
+ text: `state: ${node.state}`
50
+ });
51
+ send([null, null, { payload: node.state }]);
52
+ break;
53
+ case "inTrue":
54
+ if (node.state) {
55
+ node.status({
56
+ fill: "blue",
57
+ shape: "dot",
58
+ text: `out: ${msg.payload}`
59
+ });
60
+ send([msg, null, { payload: node.state }]);
61
+ }
62
+ break;
63
+ case "inFalse":
64
+ if (!node.state) {
65
+ node.status({
66
+ fill: "blue",
67
+ shape: "dot",
68
+ text: `out: ${msg.payload}`
69
+ });
70
+ send([null, msg, { payload: node.state }]);
71
+ }
72
+ break;
73
+ default:
74
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
75
+ if (done) done("Unknown context");
76
+ return;
77
+ }
78
+ if (done) done();
79
+ });
80
+
81
+ node.on("close", function(done) {
82
+ node.status({});
83
+ done();
84
+ });
85
+ }
86
+
87
+ RED.nodes.registerType("boolean-switch-block", BooleanSwitchBlockNode);
88
+ };
@@ -0,0 +1,59 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="boolean-to-number-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-nullToZero"><i class="fa fa-cog"></i> Null Mapping</label>
9
+ <input type="checkbox" id="node-input-nullToZero" style="width: auto; margin-left: 10px;"> Map null to 0
10
+ </div>
11
+ </script>
12
+
13
+ <!-- JavaScript Section -->
14
+ <script type="text/javascript">
15
+ RED.nodes.registerType("boolean-to-number-block", {
16
+ category: "control",
17
+ color: "#301934",
18
+ defaults: {
19
+ name: { value: "" },
20
+ nullToZero: { value: false }
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ inputLabels: ["input"],
25
+ outputLabels: ["output"],
26
+ icon: "font-awesome/fa-calculator",
27
+ paletteLabel: "boolean to number",
28
+ label: function() {
29
+ return this.name || "boolean to number";
30
+ }
31
+ });
32
+ </script>
33
+
34
+ <!-- Help Section -->
35
+ <script type="text/markdown" data-help-name="boolean-to-number-block">
36
+ Converts a boolean or null input payload to a numeric output.
37
+
38
+ ### Inputs
39
+ : payload (boolean | null) : Value to convert (`true`, `false`, `null`).
40
+
41
+ ### Outputs
42
+ : payload (number) : Converted value `null` to `0` (if `nullToZero`) or `-1`, `false` to `0`, `true` to `1`.
43
+
44
+ ### Details
45
+ Converts `msg.payload` (boolean or null) to a number `null` maps to `0` (if `nullToZero` is `true`) or `-1` (if `false`), `false` to `0`, `true` to `1`.
46
+
47
+ Passthrough all other input message properties
48
+
49
+ ### Status
50
+ - Green (dot): Configuration update
51
+ - Blue (dot): State changed
52
+ - Blue (ring): State unchanged
53
+ - Red (ring): Error
54
+ - Yellow (ring): Warning
55
+
56
+ ### References
57
+ - [Node-RED Documentation](https://nodered.org/docs/)
58
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
59
+ </script>
@@ -0,0 +1,45 @@
1
+ module.exports = function(RED) {
2
+ function BooleanToNumberBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime for editor display
7
+ node.runtime = {
8
+ name: config.name,
9
+ nullToZero: Boolean(config.nullToZero)
10
+ };
11
+
12
+ node.on("input", function(msg, send, done) {
13
+ send = send || function() { node.send.apply(node, arguments); };
14
+
15
+ // Check for missing payload
16
+ if (!msg.hasOwnProperty("payload")) {
17
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
18
+ if (done) done();
19
+ return;
20
+ }
21
+
22
+ // Validate and convert payload
23
+ const inputDisplay = msg.payload === null ? "null" : String(msg.payload);
24
+ if (msg.payload === null) {
25
+ msg.payload = node.runtime.nullToZero ? 0 : -1;
26
+ node.status({ fill: "blue", shape: "dot", text: `in: ${inputDisplay}, out: ${msg.payload}` });
27
+ send(msg);
28
+ } else if (typeof msg.payload === "boolean") {
29
+ msg.payload = msg.payload ? 1 : 0;
30
+ node.status({ fill: "blue", shape: "dot", text: `in: ${inputDisplay}, out: ${msg.payload}` });
31
+ send(msg);
32
+ } else {
33
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
34
+ }
35
+
36
+ if (done) done();
37
+ });
38
+
39
+ node.on("close", function(done) {
40
+ done();
41
+ });
42
+ }
43
+
44
+ RED.nodes.registerType("boolean-to-number-block", BooleanToNumberBlockNode);
45
+ };
@@ -0,0 +1,69 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="cache-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-operationMode" title="Choose whether to output the full cloned message or only the payload"><i class="fa fa-cog"></i> Operation Mode</label>
9
+ <input type="text" id="node-input-operationMode" placeholder="payload">
10
+ </div>
11
+ </script>
12
+
13
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
14
+ <script type="text/javascript">
15
+ RED.nodes.registerType("cache-block", {
16
+ category: "control",
17
+ color: "#301934",
18
+ defaults: {
19
+ name: { value: "" },
20
+ operationMode: { value: "payload", required: true }
21
+ },
22
+ inputs: 1,
23
+ outputs: 1,
24
+ inputLabels: ["input"],
25
+ outputLabels: ["output"],
26
+ icon: "font-awesome/fa-database",
27
+ paletteLabel: "cache",
28
+ label: function() {
29
+ return this.name || "cache";
30
+ }
31
+ });
32
+ </script>
33
+
34
+ <!-- Help Section -->
35
+ <script type="text/markdown" data-help-name="cache-block">
36
+ Caches an input message and outputs either a copy or just the payload based on configuration.
37
+
38
+ ### Inputs
39
+ : context (string) : Sets action (`"update"` to store, `"execute"` to output, `"reset"` to clear).
40
+ Unmatched values trigger an error.
41
+ : payload (any | boolean) : Value to store (`update`), ignored for `execute`, boolean `true` for `reset`.
42
+
43
+ ### Outputs
44
+ : payload (any) : Cached value when `execute` is triggered; `null` if cache is empty.
45
+ : other properties : Included only if "Clone Message" mode is selected.
46
+
47
+ ### Properties
48
+ : name (string) : Display name in editor. Default; "".
49
+ : operationMode (string) : Selects output behavior; "Clone Message" (full message) or "Payload Only" (just payload).
50
+
51
+ ### Details
52
+ Stores the entire input message when `msg.context = "update"`.
53
+
54
+ Outputs either a full copy of the cached message (`operationMode = "Clone Message"`)
55
+ or a new message with only the cached `msg.payload`
56
+
57
+ Clears the cache to `null` when `msg.context = "reset"` with `msg.payload = true`.
58
+
59
+ ### Status
60
+ - Green (dot): Configuration update
61
+ - Blue (dot): State changed
62
+ - Blue (ring): State unchanged
63
+ - Red (ring): Error
64
+ - Yellow (ring): Warning
65
+
66
+ ### References
67
+ - [Node-RED Documentation](https://nodered.org/docs/)
68
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
69
+ </script>