@bldgblocks/node-red-contrib-control 0.1.5 → 0.1.17

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # node-red-contrib-buildingblocks-control
1
+ # @bldgblocks/node-red-contrib-control
2
2
  Sedona-inspired control nodes for stateful logic.
3
3
 
4
4
  This is a rather large node collection. Contributions are appreciated.
@@ -45,11 +45,11 @@ Counts rising edges (false-to-true transitions) in `msg.payload` (boolean), incr
45
45
  Resets count to 0 via `msg.context = "reset"` with `msg.payload = true`.
46
46
 
47
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
48
+ - Green (dot): Configuration update
49
+ - Blue (dot): State changed
50
+ - Blue (ring): State unchanged
51
+ - Red (ring): Error
52
+ - Yellow (ring): Warning
53
53
 
54
54
  ### References
55
55
  - [Node-RED Documentation](https://nodered.org/docs/)
@@ -6,25 +6,27 @@
6
6
  </div>
7
7
  <div class="form-row">
8
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">
9
+ <input type="text" id="node-input-delayOn" placeholder="1000" min="0" step="1">
10
+ <input type="hidden" id="node-input-delayOnType">
11
+ </div>
12
+ <div>
10
13
  <select id="node-input-delayOnUnits">
11
14
  <option value="milliseconds">Milliseconds</option>
12
15
  <option value="seconds">Seconds</option>
13
16
  <option value="minutes">Minutes</option>
14
17
  </select>
15
- <input type="hidden" id="node-input-delayOnType">
16
-
17
18
  </div>
18
19
  <div class="form-row">
19
20
  <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
+ <input type="text" id="node-input-delayOff" placeholder="1000" min="0" step="1">
22
+ <input type="hidden" id="node-input-delayOffType">
23
+ </div>
24
+ <div>
21
25
  <select id="node-input-delayOffUnits">
22
26
  <option value="milliseconds">Milliseconds</option>
23
27
  <option value="seconds">Seconds</option>
24
28
  <option value="minutes">Minutes</option>
25
29
  </select>
26
- <input type="hidden" id="node-input-delayOffType">
27
-
28
30
  </div>
29
31
  </script>
30
32
 
@@ -52,6 +54,7 @@
52
54
  return this.name || "delay";
53
55
  },
54
56
  oneditprepare: function() {
57
+ const node = this;
55
58
  try {
56
59
  // Initialize typed inputs
57
60
  $("#node-input-delayOn").typedInput({
@@ -79,7 +82,7 @@ Delays boolean state transitions with configurable on/off delays.
79
82
 
80
83
  ### Inputs
81
84
  : 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`.
85
+ : payload (boolean | number) : Boolean for state change, number for delay config with `msg.context`.
83
86
  : *units* (string) : Units for delay context config (`"milliseconds"`, `"seconds"`, `"minutes"`).
84
87
 
85
88
  ### Outputs
@@ -89,14 +92,14 @@ Delays boolean state transitions with configurable on/off delays.
89
92
  ### Details
90
93
  Delays `msg.payload` boolean transitions, outputting `true` after `delayOn` ms for false-to-true or `false` after `delayOff` ms for true-to-false,
91
94
  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.
95
+ Non-transition inputs (e.g., `true` when `state=true`) do not cancel pending delays.
93
96
 
94
97
  ### 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
98
+ - Green (dot): Configuration update
99
+ - Blue (dot): State changed
100
+ - Blue (ring): State unchanged
101
+ - Red (ring): Error
102
+ - Yellow (ring): Warning
100
103
 
101
104
  ### References
102
105
  - [Node-RED Documentation](https://nodered.org/docs/)
@@ -5,25 +5,10 @@ module.exports = function(RED) {
5
5
 
6
6
  node.runtime = {
7
7
  name: config.name || "",
8
- state: false
8
+ state: false,
9
+ desired: false
9
10
  };
10
11
 
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
12
  let timeoutId = null;
28
13
 
29
14
  node.on("input", function(msg, send, done) {
@@ -56,6 +41,15 @@ module.exports = function(RED) {
56
41
  return;
57
42
  }
58
43
 
44
+ if (isNaN(node.runtime.delayOn) || node.runtime.delayOn < 0) {
45
+ node.runtime.delayOn = 1000;
46
+ node.status({ fill: "red", shape: "ring", text: "invalid delayOn" });
47
+ }
48
+ if (isNaN(node.runtime.delayOff) || node.runtime.delayOff < 0) {
49
+ node.runtime.delayOff = 1000;
50
+ node.status({ fill: "red", shape: "ring", text: "invalid delayOff" });
51
+ }
52
+
59
53
  if (msg.hasOwnProperty("context")) {
60
54
  if (msg.context === "reset") {
61
55
  if (!msg.hasOwnProperty("payload") || typeof msg.payload !== "boolean") {
@@ -129,10 +123,15 @@ module.exports = function(RED) {
129
123
  }
130
124
 
131
125
  if (!node.runtime.state && inputValue === true) {
126
+ if (node.runtime.desired) {
127
+ if (done) done();
128
+ return;
129
+ }
132
130
  if (timeoutId) {
133
131
  clearTimeout(timeoutId);
134
132
  }
135
133
  node.status({ fill: "blue", shape: "ring", text: `awaiting true` });
134
+ node.runtime.desired = true;
136
135
  timeoutId = setTimeout(() => {
137
136
  node.runtime.state = true;
138
137
  msg.payload = true;
@@ -142,10 +141,15 @@ module.exports = function(RED) {
142
141
  timeoutId = null;
143
142
  }, node.runtime.delayOn);
144
143
  } else if (node.runtime.state && inputValue === false) {
144
+ if (node.runtime.desired === false) {
145
+ if (done) done();
146
+ return;
147
+ }
145
148
  if (timeoutId) {
146
149
  clearTimeout(timeoutId);
147
150
  }
148
151
  node.status({ fill: "blue", shape: "ring", text: `awaiting false` });
152
+ node.runtime.desired = false;
149
153
  timeoutId = setTimeout(() => {
150
154
  node.runtime.state = false;
151
155
  msg.payload = false;
@@ -160,7 +164,7 @@ module.exports = function(RED) {
160
164
  timeoutId = null;
161
165
  node.status({ fill: "blue", shape: "ring", text: `canceled awaiting ${node.runtime.state}` });
162
166
  } else {
163
- node.status({ fill: "blue", shape: "ring", text: `awaiting ${inputValue}` });
167
+ node.status({ fill: "blue", shape: "ring", text: `no change` });
164
168
  }
165
169
  }
166
170
 
@@ -8,7 +8,7 @@ module.exports = function(RED) {
8
8
  node.runtime = {
9
9
  name: config.name,
10
10
  slots: parseInt(config.slots),
11
- inputs: Array(node.runtime.slots).fill(1).map(x => parseFloat(x)),
11
+ inputs: Array(config.slots).fill(1).map(x => parseFloat(x)),
12
12
  lastResult: null
13
13
  };
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.1.5",
3
+ "version": "0.1.17",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [ "node-red", "sedona", "control", "hvac" ],
6
6
  "files": ["nodes/*.js", "nodes/*.html"],
@@ -26,7 +26,6 @@
26
26
  "contextual-label-block": "nodes/contextual-label-block.js",
27
27
  "convert-block": "nodes/convert-block.js",
28
28
  "count-block": "nodes/count-block.js",
29
- "debounce-block": "nodes/debounce-block.js",
30
29
  "delay-block": "nodes/delay-block.js",
31
30
  "divide-block": "nodes/divide-block.js",
32
31
  "edge-block": "nodes/edge-block.js",
@@ -1,64 +0,0 @@
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>
@@ -1,140 +0,0 @@
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
- };