@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,104 @@
1
+ <!-- UI Template Section: Defines the edit dialog -->
2
+ <script type="text/html" data-template-name="delay-block">
3
+ <div class="form-row">
4
+ <label for="node-input-name"><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-delayOn"><i class="fa fa-clock-o"></i> On Delay</label>
9
+ <input type="number" id="node-input-delayOn" placeholder="1000" min="0" step="1">
10
+ <select id="node-input-delayOnUnits">
11
+ <option value="milliseconds">Milliseconds</option>
12
+ <option value="seconds">Seconds</option>
13
+ <option value="minutes">Minutes</option>
14
+ </select>
15
+ <input type="hidden" id="node-input-delayOnType">
16
+
17
+ </div>
18
+ <div class="form-row">
19
+ <label for="node-input-delayOff"><i class="fa fa-clock-o"></i> Off Delay</label>
20
+ <input type="number" id="node-input-delayOff" placeholder="1000" min="0" step="1">
21
+ <select id="node-input-delayOffUnits">
22
+ <option value="milliseconds">Milliseconds</option>
23
+ <option value="seconds">Seconds</option>
24
+ <option value="minutes">Minutes</option>
25
+ </select>
26
+ <input type="hidden" id="node-input-delayOffType">
27
+
28
+ </div>
29
+ </script>
30
+
31
+ <!-- JavaScript Section: Registers the node and handles editor logic -->
32
+ <script type="text/javascript">
33
+ RED.nodes.registerType("delay-block", {
34
+ category: "control",
35
+ color: "#301934",
36
+ defaults: {
37
+ name: { value: "" },
38
+ delayOn: { value: 1000, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
39
+ delayOnUnits: { value: "milliseconds" },
40
+ delayOnType: { value: "num" },
41
+ delayOff: { value: 1000, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
42
+ delayOffUnits: { value: "milliseconds" },
43
+ delayOffType: { value: "num" }
44
+ },
45
+ inputs: 1,
46
+ outputs: 1,
47
+ inputLabels: ["input"],
48
+ outputLabels: ["output"],
49
+ icon: "font-awesome/fa-clock-o",
50
+ paletteLabel: "delay",
51
+ label: function() {
52
+ return this.name || "delay";
53
+ },
54
+ oneditprepare: function() {
55
+ try {
56
+ // Initialize typed inputs
57
+ $("#node-input-delayOn").typedInput({
58
+ default: "num",
59
+ types: ["num", "msg", "flow", "global"],
60
+ typeField: "#node-input-delayOnType"
61
+ }).typedInput("type", node.delayOnType || "num").typedInput("value", node.delayOn);
62
+
63
+ $("#node-input-delayOff").typedInput({
64
+ default: "num",
65
+ types: ["num", "msg", "flow", "global"],
66
+ typeField: "#node-input-delayOffType"
67
+ }).typedInput("type", node.delayOffType || "num").typedInput("value", node.delayOff);
68
+
69
+ } catch (err) {
70
+ console.error("Error in hysteresis-block oneditprepare:", err);
71
+ }
72
+ }
73
+ });
74
+ </script>
75
+
76
+ <!-- Help Section: -->
77
+ <script type="text/markdown" data-help-name="delay-block">
78
+ Delays boolean state transitions with configurable on/off delays.
79
+
80
+ ### Inputs
81
+ : context (string) : Configures node (`"reset"`, `"delayOn"`, `"delayOff"`). Unmatched values ignored silently.
82
+ : payload (boolean | number) : Boolean for state change; number for delay config with `msg.context`.
83
+ : *units* (string) : Units for delay context config (`"milliseconds"`, `"seconds"`, `"minutes"`).
84
+
85
+ ### Outputs
86
+ : payload (boolean) : `true` after `delayOn` ms or `false` after `delayOff` ms if state persists. `msg.context` is consumed.
87
+ : *other* (any) : Other input properties preserved, except `msg.context`.
88
+
89
+ ### Details
90
+ Delays `msg.payload` boolean transitions, outputting `true` after `delayOn` ms for false-to-true or `false` after `delayOff` ms for true-to-false,
91
+ if the input state persists. Forwards the input message with updated `msg.payload`, removing `msg.context`.
92
+ Non-transition inputs (e.g., `true` when `state=true`) or state reversions cancel pending delays without output.
93
+
94
+ ### Status
95
+ - Green (dot): Configuration
96
+ - Blue (dot): Output, no alarm
97
+ - Red (dot): Output with alarm
98
+ - Red (ring): Errors
99
+ - Yellow (ring): Unknown context
100
+
101
+ ### References
102
+ - [Node-RED Documentation](https://nodered.org/docs/)
103
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
104
+ </script>
@@ -0,0 +1,180 @@
1
+ module.exports = function(RED) {
2
+ function DelayBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ node.runtime = {
7
+ name: config.name || "",
8
+ state: false
9
+ };
10
+
11
+ if (isNaN(node.runtime.delayOn) || node.runtime.delayOn < 0) {
12
+ node.runtime.delayOn = 1000;
13
+ node.status({ fill: "red", shape: "ring", text: "invalid delayOn" });
14
+ }
15
+ if (isNaN(node.runtime.delayOff) || node.runtime.delayOff < 0) {
16
+ node.runtime.delayOff = 1000;
17
+ node.status({ fill: "red", shape: "ring", text: "invalid delayOff" });
18
+ }
19
+
20
+ // Set initial status
21
+ node.status({
22
+ fill: "green",
23
+ shape: "dot",
24
+ text: `On: ${node.runtime.delayOn}ms, Off: ${node.runtime.delayOff}ms`
25
+ });
26
+
27
+ let timeoutId = null;
28
+
29
+ node.on("input", function(msg, send, done) {
30
+ send = send || function() { node.send.apply(node, arguments); };
31
+ if (!msg) {
32
+ if (done) done();
33
+ return;
34
+ }
35
+
36
+ // Evaluate typed-inputs
37
+ try {
38
+ node.runtime.delayOn = RED.util.evaluateNodeProperty(
39
+ config.delayOn, config.delayOnType, node, msg
40
+ );
41
+ node.runtime.delayOn = (parseFloat(node.runtime.delayOn)) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
42
+
43
+ node.runtime.delayOff = RED.util.evaluateNodeProperty(
44
+ config.delayOff, config.delayOffType, node, msg
45
+ );
46
+ node.runtime.delayOff = (parseFloat(node.runtime.delayOff)) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
47
+
48
+ node.period = parseFloat(node.period);
49
+ if (isNaN(node.period) || node.period <= 0 || !isFinite(node.period)) {
50
+ node.period = 1000;
51
+ node.status({ fill: "yellow", shape: "ring", text: "invalid period, using 1000ms" });
52
+ }
53
+ } catch(err) {
54
+ node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
55
+ if (done) done(err);
56
+ return;
57
+ }
58
+
59
+ if (msg.hasOwnProperty("context")) {
60
+ if (msg.context === "reset") {
61
+ if (!msg.hasOwnProperty("payload") || typeof msg.payload !== "boolean") {
62
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
63
+ if (done) done();
64
+ return;
65
+ }
66
+ if (msg.payload === true) {
67
+ if (timeoutId) {
68
+ clearTimeout(timeoutId);
69
+ timeoutId = null;
70
+ }
71
+ node.runtime.state = false;
72
+ node.status({ fill: "green", shape: "dot", text: "reset" });
73
+ }
74
+ if (done) done();
75
+ return;
76
+ } else if (msg.context === "delayOn") {
77
+ if (!msg.hasOwnProperty("payload")) {
78
+ node.status({ fill: "red", shape: "ring", text: "missing payload for delayOn" });
79
+ if (done) done();
80
+ return;
81
+ }
82
+ let newDelayOn = parseFloat(msg.payload);
83
+ const newDelayOnMultiplier = msg.units === "seconds" ? 1000 : msg.units === "minutes" ? 60000 : 1;
84
+ newDelayOn *= newDelayOnMultiplier;
85
+ if (isNaN(newDelayOn) || newDelayOn < 0) {
86
+ node.status({ fill: "red", shape: "ring", text: "invalid delayOn" });
87
+ if (done) done();
88
+ return;
89
+ }
90
+ node.runtime.delayOn = newDelayOn;
91
+ node.status({ fill: "green", shape: "dot", text: `delayOn: ${newDelayOn.toFixed(0)} ms` });
92
+ if (done) done();
93
+ return;
94
+ } else if (msg.context === "delayOff") {
95
+ if (!msg.hasOwnProperty("payload")) {
96
+ node.status({ fill: "red", shape: "ring", text: "missing payload for delayOff" });
97
+ if (done) done();
98
+ return;
99
+ }
100
+ let newDelayOff = parseFloat(msg.payload);
101
+ const newDelayOffMultiplier = msg.units === "seconds" ? 1000 : msg.units === "minutes" ? 60000 : 1;
102
+ newDelayOff *= newDelayOffMultiplier;
103
+ if (isNaN(newDelayOff) || newDelayOff < 0) {
104
+ node.status({ fill: "red", shape: "ring", text: "invalid delayOff" });
105
+ if (done) done();
106
+ return;
107
+ }
108
+ node.runtime.delayOff = newDelayOff;
109
+ node.status({ fill: "green", shape: "dot", text: `delayOff: ${newDelayOff.toFixed(0)} ms` });
110
+ if (done) done();
111
+ return;
112
+ }
113
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
114
+ if (done) done();
115
+ return;
116
+ }
117
+
118
+ if (!msg.hasOwnProperty("payload")) {
119
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
120
+ if (done) done();
121
+ return;
122
+ }
123
+
124
+ const inputValue = msg.payload;
125
+ if (typeof inputValue !== "boolean") {
126
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
127
+ if (done) done();
128
+ return;
129
+ }
130
+
131
+ if (!node.runtime.state && inputValue === true) {
132
+ if (timeoutId) {
133
+ clearTimeout(timeoutId);
134
+ }
135
+ node.status({ fill: "blue", shape: "ring", text: `awaiting true` });
136
+ timeoutId = setTimeout(() => {
137
+ node.runtime.state = true;
138
+ msg.payload = true;
139
+ delete msg.context;
140
+ node.status({ fill: "blue", shape: "dot", text: `in: true, out: true` });
141
+ send(msg);
142
+ timeoutId = null;
143
+ }, node.runtime.delayOn);
144
+ } else if (node.runtime.state && inputValue === false) {
145
+ if (timeoutId) {
146
+ clearTimeout(timeoutId);
147
+ }
148
+ node.status({ fill: "blue", shape: "ring", text: `awaiting false` });
149
+ timeoutId = setTimeout(() => {
150
+ node.runtime.state = false;
151
+ msg.payload = false;
152
+ delete msg.context;
153
+ node.status({ fill: "blue", shape: "dot", text: `in: false, out: false` });
154
+ send(msg);
155
+ timeoutId = null;
156
+ }, node.runtime.delayOff);
157
+ } else {
158
+ if (timeoutId) {
159
+ clearTimeout(timeoutId);
160
+ timeoutId = null;
161
+ node.status({ fill: "blue", shape: "ring", text: `canceled awaiting ${node.runtime.state}` });
162
+ } else {
163
+ node.status({ fill: "blue", shape: "ring", text: `awaiting ${inputValue}` });
164
+ }
165
+ }
166
+
167
+ if (done) done();
168
+ });
169
+
170
+ node.on("close", function(done) {
171
+ if (timeoutId) {
172
+ clearTimeout(timeoutId);
173
+ timeoutId = null;
174
+ }
175
+ done();
176
+ });
177
+ }
178
+
179
+ RED.nodes.registerType("delay-block", DelayBlockNode);
180
+ };
@@ -0,0 +1,65 @@
1
+ <script type="text/html" data-template-name="divide-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-slots" title="Number of input slots (positive integer, e.g., 2)"><i class="fa fa-list-ol"></i> Slots</label>
8
+ <input type="number" id="node-input-slots" placeholder="2" min="1" step="1">
9
+ </div>
10
+ </script>
11
+
12
+ <script type="text/javascript">
13
+ RED.nodes.registerType("divide-block", {
14
+ category: "control",
15
+ color: "#301934",
16
+ defaults: {
17
+ name: { value: "" },
18
+ slots: {
19
+ value: 2,
20
+ required: true,
21
+ validate: function(v) { return !isNaN(parseInt(v)) && parseInt(v) > 1; }
22
+ }
23
+ },
24
+ inputs: 1,
25
+ outputs: 1,
26
+ inputLabels: ["input"],
27
+ outputLabels: ["quotient"],
28
+ icon: "split.svg",
29
+ paletteLabel: "divide",
30
+ label: function() {
31
+ return this.name ? `${this.name} (${this.slots})` : `divide (${this.slots})`;
32
+ }
33
+ });
34
+ </script>
35
+
36
+ <script type="text/markdown" data-help-name="divide-block">
37
+ Divides numeric inputs from multiple slots in sequence.
38
+
39
+ ### Inputs
40
+ : context (string) : Configuration commands - `"reset"`, `"slots"`, or identifies input slot (e.g., `"in1"`, `"in2"`).
41
+ : payload (number | boolean) : Number for slot input, boolean for reset, integer for slots configuration.
42
+
43
+ ### Outputs
44
+ : payload (number) : Result of dividing slots in sequence (in1 / in2 / ...).
45
+
46
+ ### Details
47
+ Divides numeric `msg.payload` values from slots identified by `msg.context` (e.g., `"in1"`, `"in2"`) in sequence.
48
+
49
+ Slots are set via editor or `msg.context = "slots"` with positive integer.
50
+
51
+ Inputs for `in2` onward must be non-zero to avoid division by zero. Resets inputs to 1 via `msg.context = "reset"` with `msg.payload = true`.
52
+
53
+ Outputs a number only when the result changes.
54
+
55
+ ### Status
56
+ - Green (dot): Configuration
57
+ - Blue (dot): Output, no alarm
58
+ - Red (dot): Output with alarm
59
+ - Red (ring): Errors
60
+ - Yellow (ring): Unknown context
61
+
62
+ ### References
63
+ - [Node-RED Documentation](https://nodered.org/docs/)
64
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
65
+ </script>
@@ -0,0 +1,123 @@
1
+ module.exports = function(RED) {
2
+ function DivideBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+
5
+ const node = this;
6
+
7
+ // Initialize runtime state
8
+ node.runtime = {
9
+ name: config.name,
10
+ slots: parseInt(config.slots),
11
+ inputs: Array(node.runtime.slots).fill(1).map(x => parseFloat(x)),
12
+ lastResult: null
13
+ };
14
+
15
+ node.status({
16
+ fill: "green",
17
+ shape: "dot",
18
+ text: `name: ${node.runtime.name}, slots: ${node.runtime.slots}`
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
+ // Check for missing context or payload
32
+ if (!msg.hasOwnProperty("context")) {
33
+ node.status({ fill: "red", shape: "ring", text: "missing context" });
34
+ if (done) done();
35
+ return;
36
+ }
37
+
38
+ if (!msg.hasOwnProperty("payload")) {
39
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
40
+ if (done) done();
41
+ return;
42
+ }
43
+
44
+ // Handle configuration messages
45
+ if (msg.context === "reset") {
46
+ if (typeof msg.payload !== "boolean") {
47
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
48
+ if (done) done();
49
+ return;
50
+ }
51
+ if (msg.payload === true) {
52
+ node.runtime.inputs = Array(node.runtime.slots).fill(1);
53
+ node.runtime.lastResult = null;
54
+ node.status({ fill: "green", shape: "dot", text: "state reset" });
55
+ if (done) done();
56
+ return;
57
+ }
58
+ } else if (msg.context === "slots") {
59
+ let newSlots = parseInt(msg.payload);
60
+ if (isNaN(newSlots) || newSlots < 1) {
61
+ node.status({ fill: "red", shape: "ring", text: "invalid slots" });
62
+ if (done) done();
63
+ return;
64
+ }
65
+ node.runtime.slots = newSlots;
66
+ node.runtime.inputs = Array(newSlots).fill(1);
67
+ node.runtime.lastResult = null;
68
+ node.status({ fill: "green", shape: "dot", text: `slots: ${node.runtime.slots}` });
69
+ if (done) done();
70
+ return;
71
+ } else if (msg.context.startsWith("in")) {
72
+ let slotIndex = parseInt(msg.context.slice(2)) - 1;
73
+ if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.runtime.slots) {
74
+ node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
75
+ if (done) done();
76
+ return;
77
+ }
78
+ let newValue = parseFloat(msg.payload);
79
+ if (isNaN(newValue)) {
80
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
81
+ if (done) done();
82
+ return;
83
+ }
84
+ if (slotIndex > 0 && newValue === 0) {
85
+ node.status({ fill: "red", shape: "ring", text: "divide by zero" });
86
+ if (done) done();
87
+ return;
88
+ }
89
+ // Handle division by very small numbers approaching zero
90
+ if (slotIndex > 0 && Math.abs(newValue) < 1e-10) { // Near-zero check
91
+ node.status({ fill: "red", shape: "ring", text: "divide by near-zero" });
92
+ if (done) done();
93
+ return;
94
+ }
95
+ node.runtime.inputs[slotIndex] = newValue;
96
+ // Calculate division
97
+ const result = node.runtime.inputs.reduce((acc, val, idx) => idx === 0 ? val : acc / val, 1);
98
+ const isUnchanged = result === node.runtime.lastResult;
99
+ node.status({
100
+ fill: "blue",
101
+ shape: isUnchanged ? "ring" : "dot",
102
+ text: `in: ${msg.context}=${newValue.toFixed(2)}, out: ${result.toFixed(2)}`
103
+ });
104
+ if (!isUnchanged) {
105
+ node.runtime.lastResult = result;
106
+ send({ payload: result });
107
+ }
108
+ if (done) done();
109
+ return;
110
+ } else {
111
+ node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
112
+ if (done) done();
113
+ return;
114
+ }
115
+ });
116
+
117
+ node.on("close", function(done) {
118
+ done();
119
+ });
120
+ }
121
+
122
+ RED.nodes.registerType("divide-block", DivideBlockNode);
123
+ };
@@ -0,0 +1,71 @@
1
+ <!-- UI Template Section -->
2
+ <script type="text/html" data-template-name="edge-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-algorithm" title="Transition to detect (true-to-false or false-to-true)"><i class="fa fa-exchange"></i> Algorithm</label>
9
+ <select id="node-input-algorithm">
10
+ <option value="true-to-false">True to False</option>
11
+ <option value="false-to-true">False to True</option>
12
+ </select>
13
+ </div>
14
+ </script>
15
+
16
+ <!-- JavaScript Section -->
17
+ <script type="text/javascript">
18
+ RED.nodes.registerType("edge-block", {
19
+ category: "control",
20
+ color: "#301934",
21
+ defaults: {
22
+ name: { value: "" },
23
+ algorithm: { value: "true-to-false", required: true }
24
+ },
25
+ inputs: 1,
26
+ outputs: 1,
27
+ inputLabels: ["input"],
28
+ outputLabels: ["transition"],
29
+ icon: "font-awesome/fa-exchange",
30
+ paletteLabel: "edge",
31
+ label: function() {
32
+ return this.name || "edge";
33
+ }
34
+ });
35
+ </script>
36
+
37
+ <!-- Help Section -->
38
+ <script type="text/markdown" data-help-name="edge-block">
39
+ Detects configured boolean transitions (true-to-false or false-to-true) in `msg.payload`.
40
+
41
+ ### Inputs
42
+ : payload (boolean) : Boolean to monitor for transitions.
43
+ : context (string) : Action (`"algorithm"` to set transition type, `"reset"` to clear state). Unknown `msg.context` is ignored.
44
+ : payload (string | boolean) : Transition type (`"true-to-false"`, `"false-to-true"`) for `"algorithm"`, true for `"reset"`.
45
+
46
+ ### Outputs
47
+ : payload (boolean) : `true` when the specified transition occurs (true-to-false or false-to-true).
48
+
49
+ ### Properties
50
+ : name (string) : Display name in editor.
51
+ : algorithm (string) : Transition to detect (`"true-to-false"`, `"false-to-true"`).
52
+
53
+ ### Details
54
+ Detects transitions in boolean payloads based on the configured `algorithm`.
55
+ Outputs `msg.payload` as true only when the specified transition occurs (true-to-false or false-to-true).
56
+ No output on first input after reset, if no transition occurs, or for non-boolean inputs.
57
+ Configuration via `msg.context`
58
+ - `"algorithm"` Sets transition type, no output.
59
+ - `"reset"` Clears state, no output.
60
+
61
+ ### Status
62
+ - Green (dot): Configuration
63
+ - Blue (dot): Output, no alarm
64
+ - Red (dot): Output with alarm
65
+ - Red (ring): Errors
66
+ - Yellow (ring): Unknown context
67
+
68
+ ### References
69
+ - [Node-RED Documentation](https://nodered.org/docs/)
70
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
71
+ </script>
@@ -0,0 +1,120 @@
1
+ module.exports = function(RED) {
2
+ function EdgeBlockNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ // Initialize runtime state
7
+ node.runtime = {
8
+ name: config.name,
9
+ algorithm: config.algorithm,
10
+ lastValue: null
11
+ };
12
+
13
+ node.status({
14
+ fill: "green",
15
+ shape: "dot",
16
+ text: `name: ${node.runtime.name || "edge"}, algorithm: ${node.runtime.algorithm}`
17
+ });
18
+
19
+ node.on("input", function(msg, send, done) {
20
+ send = send || function() { node.send.apply(node, arguments); };
21
+
22
+ const validAlgorithms = ["true-to-false", "false-to-true"];
23
+
24
+ // Guard against invalid message
25
+ if (!msg) {
26
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
27
+ if (done) done();
28
+ return;
29
+ }
30
+
31
+ // Handle configuration messages
32
+ if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
33
+ if (msg.context === "algorithm") {
34
+ if (!msg.hasOwnProperty("payload")) {
35
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
36
+ if (done) done();
37
+ return;
38
+ }
39
+ const newAlgorithm = String(msg.payload);
40
+ if (!validAlgorithms.includes(newAlgorithm)) {
41
+ node.status({ fill: "red", shape: "ring", text: "invalid algorithm" });
42
+ if (done) done();
43
+ return;
44
+ }
45
+ node.runtime.algorithm = newAlgorithm;
46
+ node.status({ fill: "green", shape: "dot", text: `algorithm: ${newAlgorithm}` });
47
+ if (done) done();
48
+ return;
49
+ }
50
+
51
+ if (msg.context === "reset") {
52
+ if (!msg.hasOwnProperty("payload") || typeof msg.payload !== "boolean") {
53
+ node.status({ fill: "red", shape: "ring", text: "invalid reset" });
54
+ if (done) done();
55
+ return;
56
+ }
57
+ if (msg.payload === true) {
58
+ node.runtime.lastValue = null;
59
+ node.status({ fill: "green", shape: "dot", text: "state reset" });
60
+ if (done) done();
61
+ return;
62
+ }
63
+ if (done) done();
64
+ return;
65
+ }
66
+ // Ignore unknown context, process payload
67
+ }
68
+
69
+ // Validate payload
70
+ if (!msg.hasOwnProperty("payload")) {
71
+ node.status({ fill: "red", shape: "ring", text: "missing payload" });
72
+ if (done) done();
73
+ return;
74
+ }
75
+
76
+ if (typeof msg.payload !== "boolean") {
77
+ node.status({ fill: "red", shape: "ring", text: "invalid input" });
78
+ if (done) done();
79
+ return;
80
+ }
81
+
82
+ const currentValue = msg.payload;
83
+ const lastValue = node.runtime.lastValue;
84
+
85
+ // Check for transition
86
+ let isTransition = false;
87
+ if (lastValue !== null && lastValue !== undefined) {
88
+ if (node.runtime.algorithm === "true-to-false" && lastValue === true && currentValue === false) {
89
+ isTransition = true;
90
+ } else if (node.runtime.algorithm === "false-to-true" && lastValue === false && currentValue === true) {
91
+ isTransition = true;
92
+ }
93
+ }
94
+
95
+ if (isTransition) {
96
+ node.status({
97
+ fill: "blue",
98
+ shape: "dot",
99
+ text: `in: ${currentValue}, out: true`
100
+ });
101
+ send({ payload: true });
102
+ } else {
103
+ node.status({
104
+ fill: "blue",
105
+ shape: "ring",
106
+ text: `in: ${currentValue}, out: none`
107
+ });
108
+ }
109
+
110
+ node.runtime.lastValue = currentValue;
111
+ if (done) done();
112
+ });
113
+
114
+ node.on("close", function(done) {
115
+ done();
116
+ });
117
+ }
118
+
119
+ RED.nodes.registerType("edge-block", EdgeBlockNode);
120
+ };