@bldgblocks/node-red-contrib-control 0.1.34 → 0.1.36
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/nodes/accumulate-block.html +18 -8
- package/nodes/accumulate-block.js +39 -44
- package/nodes/add-block.html +1 -1
- package/nodes/add-block.js +18 -11
- package/nodes/alarm-collector.html +260 -0
- package/nodes/alarm-collector.js +292 -0
- package/nodes/alarm-config.html +129 -0
- package/nodes/alarm-config.js +126 -0
- package/nodes/alarm-service.html +96 -0
- package/nodes/alarm-service.js +142 -0
- package/nodes/analog-switch-block.js +25 -36
- package/nodes/and-block.js +44 -15
- package/nodes/average-block.js +46 -41
- package/nodes/boolean-switch-block.js +10 -28
- package/nodes/boolean-to-number-block.html +18 -5
- package/nodes/boolean-to-number-block.js +24 -16
- package/nodes/cache-block.js +24 -37
- package/nodes/call-status-block.html +91 -32
- package/nodes/call-status-block.js +398 -115
- package/nodes/changeover-block.html +5 -0
- package/nodes/changeover-block.js +167 -162
- package/nodes/comment-block.html +1 -1
- package/nodes/comment-block.js +14 -9
- package/nodes/compare-block.html +14 -4
- package/nodes/compare-block.js +23 -18
- package/nodes/contextual-label-block.html +5 -0
- package/nodes/contextual-label-block.js +6 -16
- package/nodes/convert-block.html +25 -39
- package/nodes/convert-block.js +31 -16
- package/nodes/count-block.html +11 -5
- package/nodes/count-block.js +34 -32
- package/nodes/delay-block.js +58 -53
- package/nodes/divide-block.js +43 -45
- package/nodes/edge-block.html +17 -10
- package/nodes/edge-block.js +43 -41
- package/nodes/enum-switch-block.js +6 -6
- package/nodes/frequency-block.html +6 -1
- package/nodes/frequency-block.js +64 -74
- package/nodes/global-getter.html +51 -15
- package/nodes/global-getter.js +43 -13
- package/nodes/global-setter.html +1 -1
- package/nodes/global-setter.js +40 -12
- package/nodes/history-buffer.html +96 -0
- package/nodes/history-buffer.js +461 -0
- package/nodes/history-collector.html +29 -1
- package/nodes/history-collector.js +37 -16
- package/nodes/history-config.html +13 -1
- package/nodes/history-service.html +84 -0
- package/nodes/history-service.js +52 -0
- package/nodes/hysteresis-block.html +5 -0
- package/nodes/hysteresis-block.js +13 -16
- package/nodes/interpolate-block.html +20 -2
- package/nodes/interpolate-block.js +39 -50
- package/nodes/join.html +78 -0
- package/nodes/join.js +78 -0
- package/nodes/latch-block.js +12 -14
- package/nodes/load-sequence-block.js +102 -110
- package/nodes/max-block.js +26 -26
- package/nodes/memory-block.js +57 -58
- package/nodes/min-block.js +26 -25
- package/nodes/minmax-block.js +35 -34
- package/nodes/modulo-block.js +45 -43
- package/nodes/multiply-block.js +43 -41
- package/nodes/negate-block.html +17 -7
- package/nodes/negate-block.js +25 -19
- package/nodes/network-point-read.html +128 -0
- package/nodes/network-point-read.js +230 -0
- package/nodes/{network-register.html → network-point-register.html} +94 -7
- package/nodes/{network-register.js → network-point-register.js} +18 -4
- package/nodes/network-point-write.html +149 -0
- package/nodes/network-point-write.js +222 -0
- package/nodes/network-service-bridge.html +131 -0
- package/nodes/network-service-bridge.js +376 -0
- package/nodes/network-service-read.html +81 -0
- package/nodes/{network-read.js → network-service-read.js} +4 -3
- package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
- package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
- package/nodes/network-service-write.html +89 -0
- package/nodes/{network-write.js → network-service-write.js} +3 -3
- package/nodes/nullify-block.js +13 -15
- package/nodes/on-change-block.html +17 -9
- package/nodes/on-change-block.js +49 -46
- package/nodes/oneshot-block.html +13 -10
- package/nodes/oneshot-block.js +57 -75
- package/nodes/or-block.js +44 -15
- package/nodes/pid-block.html +54 -4
- package/nodes/pid-block.js +459 -248
- package/nodes/priority-block.js +24 -35
- package/nodes/rate-limit-block.js +70 -72
- package/nodes/rate-of-change-block.html +33 -14
- package/nodes/rate-of-change-block.js +74 -62
- package/nodes/round-block.html +14 -9
- package/nodes/round-block.js +32 -25
- package/nodes/saw-tooth-wave-block.js +49 -76
- package/nodes/scale-range-block.html +12 -6
- package/nodes/scale-range-block.js +46 -39
- package/nodes/sine-wave-block.js +49 -57
- package/nodes/string-builder-block.js +6 -6
- package/nodes/subtract-block.js +38 -34
- package/nodes/thermistor-block.js +44 -44
- package/nodes/tick-tock-block.js +32 -32
- package/nodes/time-sequence-block.js +30 -42
- package/nodes/triangle-wave-block.js +49 -69
- package/nodes/tstat-block.js +34 -44
- package/nodes/units-block.html +90 -69
- package/nodes/units-block.js +22 -30
- package/nodes/utils.js +206 -3
- package/package.json +14 -6
- package/nodes/network-read.html +0 -56
- package/nodes/network-write.html +0 -65
package/nodes/nullify-block.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
2
3
|
function NullifyBlockNode(config) {
|
|
3
4
|
RED.nodes.createNode(this, config);
|
|
4
5
|
const node = this;
|
|
5
6
|
|
|
6
|
-
// Initialize
|
|
7
|
-
node.
|
|
8
|
-
name: config.name,
|
|
9
|
-
rules: config.rules
|
|
10
|
-
};
|
|
7
|
+
// Initialize state
|
|
8
|
+
node.rules = config.rules || [];
|
|
11
9
|
|
|
12
10
|
// Validate configuration
|
|
13
11
|
let valid = true;
|
|
14
|
-
node.
|
|
12
|
+
node.rules = node.rules.map(rule => {
|
|
15
13
|
if (rule.propertyType !== "msg" || !rule.property || typeof rule.property !== "string" || !rule.property.trim()) {
|
|
16
14
|
valid = false;
|
|
17
15
|
return { property: "payload", propertyType: "msg" };
|
|
@@ -19,9 +17,9 @@ module.exports = function(RED) {
|
|
|
19
17
|
return rule;
|
|
20
18
|
});
|
|
21
19
|
if (!valid) {
|
|
22
|
-
|
|
20
|
+
utils.setStatusError(node, "invalid rules, using defaults");
|
|
23
21
|
} else {
|
|
24
|
-
|
|
22
|
+
utils.setStatusOK(node, `rules: ${node.rules.map(r => r.property).join(", ")}`);
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
node.on("input", function(msg, send, done) {
|
|
@@ -29,7 +27,7 @@ module.exports = function(RED) {
|
|
|
29
27
|
|
|
30
28
|
// Guard against invalid message
|
|
31
29
|
if (!msg) {
|
|
32
|
-
|
|
30
|
+
utils.setStatusError(node, "missing message");
|
|
33
31
|
if (done) done();
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
@@ -37,18 +35,18 @@ module.exports = function(RED) {
|
|
|
37
35
|
// Handle configuration messages
|
|
38
36
|
if (msg.context) {
|
|
39
37
|
if (typeof msg.context !== "string" || !msg.context.trim()) {
|
|
40
|
-
|
|
38
|
+
utils.setStatusWarn(node, "unknown context");
|
|
41
39
|
if (done) done();
|
|
42
40
|
return;
|
|
43
41
|
}
|
|
44
42
|
if (msg.context === "rules") {
|
|
45
43
|
if (!msg.hasOwnProperty("payload") || !Array.isArray(msg.payload) || !msg.payload.every(r => r.property && typeof r.property === "string" && r.propertyType === "msg")) {
|
|
46
|
-
|
|
44
|
+
utils.setStatusError(node, "invalid rules");
|
|
47
45
|
if (done) done();
|
|
48
46
|
return;
|
|
49
47
|
}
|
|
50
|
-
node.
|
|
51
|
-
|
|
48
|
+
node.rules = msg.payload;
|
|
49
|
+
utils.setStatusOK(node, `rules: ${node.rules.map(r => r.property).join(", ")}`);
|
|
52
50
|
if (done) done();
|
|
53
51
|
return;
|
|
54
52
|
}
|
|
@@ -57,13 +55,13 @@ module.exports = function(RED) {
|
|
|
57
55
|
// Apply nullification rules
|
|
58
56
|
const outputMsg = RED.util.cloneMessage(msg);
|
|
59
57
|
const nullified = [];
|
|
60
|
-
node.
|
|
58
|
+
node.rules.forEach(rule => {
|
|
61
59
|
RED.util.setMessageProperty(outputMsg, rule.property, null);
|
|
62
60
|
nullified.push(rule.property);
|
|
63
61
|
});
|
|
64
62
|
|
|
65
63
|
// Update status and send output
|
|
66
|
-
|
|
64
|
+
utils.setStatusOK(node, `nullified: ${nullified.join(", ")}`);
|
|
67
65
|
send(outputMsg);
|
|
68
66
|
|
|
69
67
|
if (done) done();
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
5
5
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
6
6
|
</div>
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
|
|
9
|
+
<input type="text" id="node-input-inputProperty" placeholder="payload">
|
|
10
|
+
</div>
|
|
7
11
|
<div class="form-row">
|
|
8
12
|
<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
13
|
<input type="text" id="node-input-period" placeholder="0">
|
|
@@ -18,6 +22,7 @@
|
|
|
18
22
|
color: "#301934",
|
|
19
23
|
defaults: {
|
|
20
24
|
name: { value: "" },
|
|
25
|
+
inputProperty: { value: "payload" },
|
|
21
26
|
period: { value: 0 },
|
|
22
27
|
periodType: { value: "num" }
|
|
23
28
|
},
|
|
@@ -42,10 +47,10 @@
|
|
|
42
47
|
|
|
43
48
|
<!-- Help Section -->
|
|
44
49
|
<script type="text/markdown" data-help-name="on-change-block">
|
|
45
|
-
Filters redundant messages based on value changes
|
|
50
|
+
Filters redundant messages based on value changes from a configured property.
|
|
46
51
|
|
|
47
52
|
### Inputs
|
|
48
|
-
:
|
|
53
|
+
: input-property (any) : Value to monitor for changes, read from the configured Input Property.
|
|
49
54
|
: context (string, optional) : Configures period (`"period"`) or queries state (`"status"`).
|
|
50
55
|
: payload (number | any, for `context`) : Non-negative number for `"period"` (ms), any for `"status"`.
|
|
51
56
|
|
|
@@ -54,16 +59,19 @@ Filters redundant messages based on value changes and a configurable period.
|
|
|
54
59
|
: payload (object, for `msg.context = "status"`) : `{ period, periodType }`.
|
|
55
60
|
|
|
56
61
|
### Properties
|
|
57
|
-
: name (string) : Display name in editor.
|
|
58
|
-
:
|
|
62
|
+
: name (string) : Display name in editor.
|
|
63
|
+
: inputProperty (string) : Message property to monitor for changes (default: `payload`). Supports nested properties (e.g., `data.value`).
|
|
64
|
+
: period (number) : Filter period in milliseconds (≥ 0). Default 0.
|
|
59
65
|
: periodType (string) : Source of period (`"num"`, `"msg"`, `"flow"`, `"global"`). Default `"num"`.
|
|
60
66
|
|
|
61
67
|
### Details
|
|
62
|
-
Filters messages based on value changes (deep comparison)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
Filters messages based on value changes (deep comparison) to the input property and an optional filter period.
|
|
69
|
+
When `period = 0`, outputs the input message only if the input property differs from the last output value.
|
|
70
|
+
When `period > 0`, outputs the first message, then suppresses all messages during the period.
|
|
71
|
+
|
|
72
|
+
Supports complex payloads (objects, arrays) via deep comparison.
|
|
73
|
+
|
|
74
|
+
Configuration via `msg.context = "period"` with numeric `msg.payload` sets the filter period in milliseconds (no output).
|
|
67
75
|
|
|
68
76
|
### Status
|
|
69
77
|
- Green (dot): Configuration update
|
package/nodes/on-change-block.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
-
const utils = require(
|
|
2
|
+
const utils = require("./utils")(RED);
|
|
3
3
|
|
|
4
4
|
function OnChangeBlockNode(config) {
|
|
5
5
|
RED.nodes.createNode(this, config);
|
|
@@ -7,19 +7,19 @@ module.exports = function(RED) {
|
|
|
7
7
|
node.isBusy = false;
|
|
8
8
|
|
|
9
9
|
// Initialize runtime state
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
// Initialize state
|
|
11
|
+
node.name = config.name;
|
|
12
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
13
|
+
node.lastValue = null;
|
|
14
|
+
node.blockTimer = null;
|
|
15
|
+
node.period = parseFloat(config.period);
|
|
16
16
|
|
|
17
17
|
node.on("input", async function(msg, send, done) {
|
|
18
18
|
send = send || function() { node.send.apply(node, arguments); };
|
|
19
19
|
|
|
20
20
|
// Guard against invalid message
|
|
21
21
|
if (!msg) {
|
|
22
|
-
|
|
22
|
+
utils.setStatusError(node, "invalid message");
|
|
23
23
|
if (done) done();
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
@@ -30,7 +30,7 @@ module.exports = function(RED) {
|
|
|
30
30
|
// Check busy lock
|
|
31
31
|
if (node.isBusy) {
|
|
32
32
|
// Update status to let user know they are pushing too fast
|
|
33
|
-
|
|
33
|
+
utils.setStatusBusy(node, "busy - dropped msg");
|
|
34
34
|
if (done) done();
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
@@ -45,13 +45,13 @@ module.exports = function(RED) {
|
|
|
45
45
|
utils.requiresEvaluation(config.periodType)
|
|
46
46
|
? utils.evaluateNodeProperty(config.period, config.periodType, node, msg)
|
|
47
47
|
.then(val => parseFloat(val))
|
|
48
|
-
: Promise.resolve(node.
|
|
48
|
+
: Promise.resolve(node.period),
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
const results = await Promise.all(evaluations);
|
|
52
52
|
|
|
53
53
|
// Update runtime with evaluated values
|
|
54
|
-
if (!isNaN(results[0])) node.
|
|
54
|
+
if (!isNaN(results[0])) node.period = results[0];
|
|
55
55
|
} catch (err) {
|
|
56
56
|
node.error(`Error evaluating properties: ${err.message}`);
|
|
57
57
|
if (done) done();
|
|
@@ -62,32 +62,28 @@ module.exports = function(RED) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Acceptable fallbacks
|
|
65
|
-
if (isNaN(node.
|
|
66
|
-
node.
|
|
67
|
-
|
|
65
|
+
if (isNaN(node.period) || node.period < 0) {
|
|
66
|
+
node.period = config.period;
|
|
67
|
+
utils.setStatusError(node, "invalid period, using 0");
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// Handle context updates
|
|
71
71
|
if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
|
|
72
72
|
if (msg.context === "period") {
|
|
73
73
|
if (!msg.hasOwnProperty("payload")) {
|
|
74
|
-
|
|
74
|
+
utils.setStatusError(node, "missing payload for period");
|
|
75
75
|
if (done) done();
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
const newPeriod = parseFloat(msg.payload);
|
|
79
79
|
if (isNaN(newPeriod) || newPeriod < 0) {
|
|
80
|
-
|
|
80
|
+
utils.setStatusError(node, "invalid period");
|
|
81
81
|
if (done) done();
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
|
-
node.
|
|
85
|
-
node.
|
|
86
|
-
node.
|
|
87
|
-
fill: "green",
|
|
88
|
-
shape: "dot",
|
|
89
|
-
text: `period: ${node.runtime.period.toFixed(0)} ms`
|
|
90
|
-
});
|
|
84
|
+
node.period = newPeriod;
|
|
85
|
+
node.periodType = "num";
|
|
86
|
+
utils.setStatusOK(node, `period: ${node.period.toFixed(0)} ms`);
|
|
91
87
|
if (done) done();
|
|
92
88
|
return;
|
|
93
89
|
}
|
|
@@ -96,13 +92,26 @@ module.exports = function(RED) {
|
|
|
96
92
|
|
|
97
93
|
// Validate input payload
|
|
98
94
|
if (!msg.hasOwnProperty("payload")) {
|
|
99
|
-
|
|
95
|
+
utils.setStatusError(node, "missing payload");
|
|
100
96
|
send(msg);
|
|
101
97
|
if (done) done();
|
|
102
98
|
return;
|
|
103
99
|
}
|
|
104
100
|
|
|
105
|
-
|
|
101
|
+
let inputValue;
|
|
102
|
+
try {
|
|
103
|
+
inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
inputValue = undefined;
|
|
106
|
+
}
|
|
107
|
+
if (inputValue === undefined) {
|
|
108
|
+
utils.setStatusError(node, "missing or invalid input property");
|
|
109
|
+
send(msg);
|
|
110
|
+
if (done) done();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const currentValue = inputValue;
|
|
106
115
|
|
|
107
116
|
// Deep comparison function
|
|
108
117
|
function isEqual(a, b) {
|
|
@@ -122,46 +131,40 @@ module.exports = function(RED) {
|
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
// Block if in filter period
|
|
125
|
-
if (node.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
shape: "ring",
|
|
129
|
-
text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)} |`
|
|
130
|
-
});
|
|
134
|
+
if (node.blockTimer) {
|
|
135
|
+
const filteredText = `filtered: ${JSON.stringify(currentValue).slice(0, 20)} |`;
|
|
136
|
+
utils.setStatusUnchanged(node, filteredText);
|
|
131
137
|
if (done) done();
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
// period === 0 means only ever on change, not equal outside of filter period sends an update message
|
|
136
|
-
if (isEqual(currentValue, node.
|
|
137
|
-
if (node.
|
|
142
|
+
if (isEqual(currentValue, node.lastValue)) {
|
|
143
|
+
if (node.period === 0) {
|
|
138
144
|
if (done) done();
|
|
139
145
|
return;
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
|
|
143
|
-
node.
|
|
149
|
+
node.lastValue = currentValue;
|
|
144
150
|
send(msg);
|
|
145
151
|
|
|
146
152
|
// Start filter period if applicable
|
|
147
|
-
if (node.
|
|
148
|
-
node.
|
|
149
|
-
node.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
text: `filtered: ${JSON.stringify(currentValue).slice(0, 20)}` // remove ' |' to indicate end of filter period
|
|
154
|
-
});
|
|
155
|
-
}, node.runtime.period);
|
|
153
|
+
if (node.period > 0) {
|
|
154
|
+
node.blockTimer = setTimeout(() => {
|
|
155
|
+
node.blockTimer = null;
|
|
156
|
+
const endFilterText = `filtered: ${JSON.stringify(currentValue).slice(0, 20)}`;
|
|
157
|
+
utils.setStatusUnchanged(node, endFilterText);
|
|
158
|
+
}, node.period);
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
if (done) done();
|
|
159
162
|
});
|
|
160
163
|
|
|
161
164
|
node.on("close", function(done) {
|
|
162
|
-
if (node.
|
|
163
|
-
clearTimeout(node.
|
|
164
|
-
node.
|
|
165
|
+
if (node.blockTimer) {
|
|
166
|
+
clearTimeout(node.blockTimer);
|
|
167
|
+
node.blockTimer = null;
|
|
165
168
|
}
|
|
166
169
|
done();
|
|
167
170
|
});
|
package/nodes/oneshot-block.html
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
5
5
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
6
6
|
</div>
|
|
7
|
-
<div class="form-row">
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
|
|
9
|
+
<input type="text" id="node-input-inputProperty" placeholder="payload">
|
|
10
|
+
</div>
|
|
11
|
+
<div class="form-row">
|
|
8
12
|
<label for="node-input-duration" title="Duration of true pulse (positive number)"><i class="fa fa-clock-o"></i> Duration</label>
|
|
9
13
|
<input type="number" id="node-input-duration" placeholder="1000" min="0" step="any">
|
|
10
14
|
<select id="node-input-durationUnits">
|
|
@@ -30,6 +34,7 @@
|
|
|
30
34
|
color: "#301934",
|
|
31
35
|
defaults: {
|
|
32
36
|
name: { value: "" },
|
|
37
|
+
inputProperty: { value: "payload" },
|
|
33
38
|
duration: {
|
|
34
39
|
value: 1000,
|
|
35
40
|
required: true,
|
|
@@ -53,9 +58,10 @@
|
|
|
53
58
|
|
|
54
59
|
<!-- Help Section -->
|
|
55
60
|
<script type="text/markdown" data-help-name="oneshot-block">
|
|
56
|
-
Outputs a true pulse for a configurable duration when triggered
|
|
61
|
+
Outputs a true pulse for a configurable duration when triggered, then locks until reset.
|
|
57
62
|
|
|
58
63
|
### Inputs
|
|
64
|
+
: input-property (boolean) : Trigger signal to initiate pulse, read from the configured Input Property.
|
|
59
65
|
: context (string) : Configures node (`"reset"`, `"duration"`). Unmatched values trigger error.
|
|
60
66
|
: payload (boolean | number) : `true` triggers pulse if not locked; number for `"duration"`; boolean for `"reset"`.
|
|
61
67
|
: units (string) : Units for `"duration"` (`"milliseconds"`, `"seconds"`, `"minutes"`).
|
|
@@ -65,23 +71,20 @@ Outputs a true pulse for a configurable duration when triggered with `msg.payloa
|
|
|
65
71
|
|
|
66
72
|
### Properties
|
|
67
73
|
: name (string) : Display name in editor.
|
|
74
|
+
: inputProperty (string) : Message property to read trigger signal from (default: `payload`). Supports nested properties (e.g., `data.value`).
|
|
68
75
|
: duration (number) : Pulse duration (positive number).
|
|
69
76
|
: durationUnits (string) : Units for duration (`"milliseconds"`, `"seconds"`, `"minutes"`).
|
|
70
77
|
: resetRequireTrue (boolean) : Require `msg.payload = true` for reset.
|
|
71
78
|
: resetOnComplete (boolean) : Automatically reset (unlock) after pulse duration.
|
|
72
79
|
|
|
73
80
|
### Details
|
|
74
|
-
Generates a `true` pulse for `duration` when triggered with `msg.payload
|
|
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.
|
|
81
|
+
Generates a `true` pulse for `duration` when triggered with input property = true (read from configured **Input Property**, default: `msg.payload`), then outputs `false`.
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
Locks until reset via `msg.context = "reset"` (requires `msg.payload = true` if `resetRequireTrue = true`) or automatically if `resetOnComplete = true`.
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
Non-`true` inputs are ignored with no output. Duration configurable via editor or `msg.context = "duration"` with `msg.units` (`"milliseconds"`, `"seconds"`, `"minutes"`).
|
|
83
86
|
|
|
84
|
-
Outputs
|
|
87
|
+
Outputs are written to `msg.payload` as boolean values.
|
|
85
88
|
|
|
86
89
|
### Status
|
|
87
90
|
- Green (dot): Configuration update
|
package/nodes/oneshot-block.js
CHANGED
|
@@ -1,44 +1,41 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
2
3
|
function OneshotBlockNode(config) {
|
|
3
4
|
RED.nodes.createNode(this, config);
|
|
4
5
|
const node = this;
|
|
5
6
|
|
|
6
7
|
// Initialize runtime state
|
|
7
8
|
const durationMultiplier = config.durationUnits === "seconds" ? 1000 : config.durationUnits === "minutes" ? 60000 : 1;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
// Initialize state
|
|
10
|
+
node.name = config.name;
|
|
11
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
12
|
+
node.duration = (parseFloat(config.duration)) * durationMultiplier;
|
|
13
|
+
node.durationUnits = config.durationUnits;
|
|
14
|
+
node.resetRequireTrue = config.resetRequireTrue;
|
|
15
|
+
node.resetOnComplete = config.resetOnComplete;
|
|
16
|
+
node.triggerCount = 0;
|
|
17
|
+
node.locked = false;
|
|
18
|
+
node.output = false;
|
|
18
19
|
|
|
19
20
|
// Validate initial config
|
|
20
|
-
if (isNaN(node.
|
|
21
|
-
node.
|
|
22
|
-
node.
|
|
23
|
-
|
|
21
|
+
if (isNaN(node.duration) || node.duration < 1) {
|
|
22
|
+
node.duration = 1000;
|
|
23
|
+
node.durationUnits = "milliseconds";
|
|
24
|
+
utils.setStatusError(node, "invalid duration");
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// Timer for pulse
|
|
27
28
|
let timer = null;
|
|
28
29
|
|
|
29
30
|
// Set initial status
|
|
30
|
-
|
|
31
|
-
fill: "blue",
|
|
32
|
-
shape: "ring",
|
|
33
|
-
text: `triggers: ${node.runtime.triggerCount}, ${node.runtime.locked ? "locked" : "unlocked"}`
|
|
34
|
-
});
|
|
31
|
+
utils.setStatusOK(node, `triggers: ${node.triggerCount}, ${node.locked ? "locked" : "unlocked"}`);
|
|
35
32
|
|
|
36
33
|
node.on("input", function(msg, send, done) {
|
|
37
34
|
send = send || function() { node.send.apply(node, arguments); };
|
|
38
35
|
|
|
39
36
|
// Guard against invalid message
|
|
40
37
|
if (!msg) {
|
|
41
|
-
|
|
38
|
+
utils.setStatusError(node, "invalid message");
|
|
42
39
|
if (done) done();
|
|
43
40
|
return;
|
|
44
41
|
}
|
|
@@ -46,8 +43,8 @@ module.exports = function(RED) {
|
|
|
46
43
|
// Handle context updates
|
|
47
44
|
if (msg.hasOwnProperty("context")) {
|
|
48
45
|
if (msg.context === "reset") {
|
|
49
|
-
if (node.
|
|
50
|
-
|
|
46
|
+
if (node.resetRequireTrue && msg.payload !== true) {
|
|
47
|
+
utils.setStatusError(node, "invalid reset payload");
|
|
51
48
|
if (done) done();
|
|
52
49
|
return;
|
|
53
50
|
}
|
|
@@ -55,20 +52,16 @@ module.exports = function(RED) {
|
|
|
55
52
|
clearTimeout(timer);
|
|
56
53
|
timer = null;
|
|
57
54
|
}
|
|
58
|
-
node.
|
|
59
|
-
node.
|
|
60
|
-
|
|
61
|
-
fill: "blue",
|
|
62
|
-
shape: "dot",
|
|
63
|
-
text: `triggers: ${node.runtime.triggerCount}, reset`
|
|
64
|
-
});
|
|
55
|
+
node.locked = false;
|
|
56
|
+
node.output = false;
|
|
57
|
+
utils.setStatusChanged(node, `triggers: ${node.triggerCount}, reset`);
|
|
65
58
|
send({ payload: false });
|
|
66
59
|
if (done) done();
|
|
67
60
|
return;
|
|
68
61
|
}
|
|
69
62
|
if (msg.context === "duration") {
|
|
70
63
|
if (!msg.hasOwnProperty("payload")) {
|
|
71
|
-
|
|
64
|
+
utils.setStatusError(node, "missing payload for duration");
|
|
72
65
|
if (done) done();
|
|
73
66
|
return;
|
|
74
67
|
}
|
|
@@ -77,81 +70,70 @@ module.exports = function(RED) {
|
|
|
77
70
|
const multiplier = newDurationUnits === "seconds" ? 1000 : newDurationUnits === "minutes" ? 60000 : 1;
|
|
78
71
|
newDuration *= multiplier;
|
|
79
72
|
if (isNaN(newDuration) || newDuration < 1) {
|
|
80
|
-
|
|
73
|
+
utils.setStatusError(node, "invalid duration");
|
|
81
74
|
if (done) done();
|
|
82
75
|
return;
|
|
83
76
|
}
|
|
84
|
-
node.
|
|
85
|
-
node.
|
|
86
|
-
node.
|
|
87
|
-
fill: "green",
|
|
88
|
-
shape: "dot",
|
|
89
|
-
text: `duration: ${node.runtime.duration.toFixed(0)} ms`
|
|
90
|
-
});
|
|
77
|
+
node.duration = newDuration;
|
|
78
|
+
node.durationUnits = newDurationUnits;
|
|
79
|
+
utils.setStatusOK(node, `duration: ${node.duration.toFixed(0)} ms`);
|
|
91
80
|
if (done) done();
|
|
92
81
|
return;
|
|
93
82
|
}
|
|
94
|
-
|
|
83
|
+
utils.setStatusWarn(node, "unknown context");
|
|
95
84
|
if (done) done("Unknown context");
|
|
96
85
|
return;
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
// Get trigger input from configured property
|
|
89
|
+
let triggerValue;
|
|
90
|
+
try {
|
|
91
|
+
triggerValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
triggerValue = undefined;
|
|
94
|
+
}
|
|
95
|
+
if (triggerValue === undefined) {
|
|
96
|
+
utils.setStatusError(node, "missing or invalid input property");
|
|
97
|
+
if (done) done();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Validate trigger input
|
|
102
|
+
if (triggerValue !== true) {
|
|
103
|
+
utils.setStatusWarn(node, `ignored: non-true`);
|
|
106
104
|
if (done) done();
|
|
107
105
|
return;
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
// Check if locked
|
|
111
|
-
if (node.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
shape: "ring",
|
|
115
|
-
text: `triggers: ${node.runtime.triggerCount}, locked`
|
|
116
|
-
});
|
|
117
|
-
send({ payload: node.runtime.output });
|
|
109
|
+
if (node.locked) {
|
|
110
|
+
utils.setStatusError(node, `triggers: ${node.triggerCount}, locked`);
|
|
111
|
+
send({ payload: node.output });
|
|
118
112
|
if (done) done();
|
|
119
113
|
return;
|
|
120
114
|
}
|
|
121
115
|
|
|
122
116
|
// Trigger pulse
|
|
123
|
-
node.
|
|
124
|
-
node.
|
|
125
|
-
node.
|
|
117
|
+
node.triggerCount++;
|
|
118
|
+
node.locked = true;
|
|
119
|
+
node.output = true;
|
|
126
120
|
|
|
127
121
|
// Send true pulse
|
|
128
|
-
|
|
129
|
-
fill: "green",
|
|
130
|
-
shape: "dot",
|
|
131
|
-
text: `triggers: ${node.runtime.triggerCount}, out: true`
|
|
132
|
-
});
|
|
122
|
+
utils.setStatusOK(node, `triggers: ${node.triggerCount}, out: true`);
|
|
133
123
|
send({ payload: true });
|
|
134
124
|
|
|
135
125
|
// Schedule false output
|
|
136
126
|
timer = setTimeout(() => {
|
|
137
|
-
node.
|
|
138
|
-
if (node.
|
|
139
|
-
node.
|
|
140
|
-
|
|
141
|
-
fill: "blue",
|
|
142
|
-
shape: "ring",
|
|
143
|
-
text: `triggers: ${node.runtime.triggerCount}, unlocked`
|
|
144
|
-
});
|
|
127
|
+
node.output = false;
|
|
128
|
+
if (node.resetOnComplete) {
|
|
129
|
+
node.locked = false;
|
|
130
|
+
utils.setStatusOK(node, `triggers: ${node.triggerCount}, unlocked`);
|
|
145
131
|
} else {
|
|
146
|
-
|
|
147
|
-
fill: "red",
|
|
148
|
-
shape: "ring",
|
|
149
|
-
text: `triggers: ${node.runtime.triggerCount}, locked`
|
|
150
|
-
});
|
|
132
|
+
utils.setStatusError(node, `triggers: ${node.triggerCount}, locked`);
|
|
151
133
|
}
|
|
152
134
|
send({ payload: false });
|
|
153
135
|
timer = null;
|
|
154
|
-
}, node.
|
|
136
|
+
}, node.duration);
|
|
155
137
|
|
|
156
138
|
if (done) done();
|
|
157
139
|
});
|