@bldgblocks/node-red-contrib-control 0.1.16 → 0.1.18
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 +2 -0
- package/nodes/count-block.html +5 -5
- package/nodes/delay-block.html +16 -13
- package/nodes/delay-block.js +22 -18
- package/nodes/divide-block.js +1 -1
- package/package.json +1 -2
- package/nodes/debounce-block.html +0 -64
- package/nodes/debounce-block.js +0 -140
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@ Sedona-inspired control nodes for stateful logic.
|
|
|
3
3
|
|
|
4
4
|
This is a rather large node collection. Contributions are appreciated.
|
|
5
5
|
|
|
6
|
+
*** If you are reading this, the package was posted very recently and changes will be flowing as I get examples updated. ***
|
|
7
|
+
|
|
6
8
|
## Intro
|
|
7
9
|
This is intended for HVAC usage but the logic applies to anything.
|
|
8
10
|
|
package/nodes/count-block.html
CHANGED
|
@@ -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):
|
|
50
|
-
-
|
|
51
|
-
- Red (ring):
|
|
52
|
-
- Yellow (ring):
|
|
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/)
|
package/nodes/delay-block.html
CHANGED
|
@@ -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="
|
|
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="
|
|
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
|
|
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`)
|
|
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):
|
|
97
|
-
-
|
|
98
|
-
- Red (ring):
|
|
99
|
-
- Yellow (ring):
|
|
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/)
|
package/nodes/delay-block.js
CHANGED
|
@@ -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: `
|
|
167
|
+
node.status({ fill: "blue", shape: "ring", text: `no change` });
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
|
package/nodes/divide-block.js
CHANGED
|
@@ -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(
|
|
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.
|
|
3
|
+
"version": "0.1.18",
|
|
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>
|
package/nodes/debounce-block.js
DELETED
|
@@ -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
|
-
};
|