@bldgblocks/node-red-contrib-control 0.1.37 → 0.2.0
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/LICENSE.md +38 -5
- package/README.md +10 -0
- package/nodes/accumulate-block.html +3 -3
- package/nodes/alarm-collector.html +11 -0
- package/nodes/alarm-collector.js +31 -5
- package/nodes/alarm-config.html +11 -6
- package/nodes/alarm-config.js +34 -35
- package/nodes/alarm-service.js +2 -2
- package/nodes/and-block.js +1 -1
- package/nodes/boolean-switch-block.html +27 -14
- package/nodes/boolean-switch-block.js +22 -12
- package/nodes/call-status-block.html +83 -56
- package/nodes/call-status-block.js +335 -248
- package/nodes/changeover-block.html +30 -31
- package/nodes/changeover-block.js +287 -389
- package/nodes/contextual-label-block.js +3 -3
- package/nodes/delay-block.js +74 -13
- package/nodes/global-getter.html +173 -12
- package/nodes/global-getter.js +29 -14
- package/nodes/global-setter.html +37 -0
- package/nodes/global-setter.js +96 -14
- package/nodes/history-buffer.js +32 -27
- package/nodes/history-collector.html +3 -1
- package/nodes/history-collector.js +4 -4
- package/nodes/history-config.html +8 -2
- package/nodes/network-point-read.js +6 -1
- package/nodes/network-point-register.html +1 -1
- package/nodes/network-service-bridge.js +43 -11
- package/nodes/network-service-registry.html +236 -27
- package/nodes/network-service-registry.js +1 -1
- package/nodes/or-block.js +1 -1
- package/nodes/priority-block.js +1 -1
- package/nodes/tstat-block.html +34 -79
- package/nodes/tstat-block.js +223 -345
- package/nodes/utils.js +1 -1
- package/package.json +90 -75
package/LICENSE.md
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
|
|
2
|
+
# Commons Clause License Condition v1.0
|
|
3
|
+
|
|
4
|
+
The Software is provided to you by the Licensor under the License, as
|
|
5
|
+
defined below, subject to the following condition.
|
|
6
|
+
|
|
7
|
+
Without limiting other conditions in the License, the grant of rights
|
|
8
|
+
under the License will not include, and the License does not grant to
|
|
9
|
+
you, the right to Sell the Software.
|
|
10
|
+
|
|
11
|
+
For purposes of the foregoing, "Sell" means practicing any or all of
|
|
12
|
+
the rights granted to you under the License to provide to third parties,
|
|
13
|
+
for a fee or other consideration (including without limitation fees for
|
|
14
|
+
hosting or consulting/support services related to the Software), a
|
|
15
|
+
product or service whose value derives, entirely or substantially, from
|
|
16
|
+
the functionality of the Software. Any license notice or attribution
|
|
17
|
+
required by the License must also include this Commons Clause License
|
|
18
|
+
Condition notice.
|
|
19
|
+
|
|
20
|
+
**Software:** node-red-contrib-control
|
|
21
|
+
|
|
22
|
+
**License:** Apache 2.0
|
|
23
|
+
|
|
24
|
+
**Licensor:** buildingblocks
|
|
25
|
+
|
|
26
|
+
For commercial licensing inquiries, contact the Licensor via the
|
|
27
|
+
repository at https://github.com/BldgBlocks/node-red-contrib-control
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
2
31
|
Apache License
|
|
3
32
|
Version 2.0, January 2004
|
|
4
33
|
http://www.apache.org/licenses/
|
|
@@ -187,11 +216,12 @@
|
|
|
187
216
|
same "printed page" as the copyright notice for easier
|
|
188
217
|
identification within third-party archives.
|
|
189
218
|
|
|
190
|
-
Copyright
|
|
219
|
+
Copyright 2024-2026 buildingblocks
|
|
191
220
|
|
|
192
|
-
Licensed under the Apache License, Version 2.0
|
|
193
|
-
you may not use this file
|
|
194
|
-
You may obtain a copy of the
|
|
221
|
+
Licensed under the Apache License, Version 2.0, with Commons Clause
|
|
222
|
+
License Condition v1.0 (the "License"); you may not use this file
|
|
223
|
+
except in compliance with the License. You may obtain a copy of the
|
|
224
|
+
License at
|
|
195
225
|
|
|
196
226
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
227
|
|
|
@@ -199,4 +229,7 @@
|
|
|
199
229
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
230
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
231
|
See the License for the specific language governing permissions and
|
|
202
|
-
limitations under the License.
|
|
232
|
+
limitations under the License.
|
|
233
|
+
|
|
234
|
+
This software is subject to the Commons Clause License Condition v1.0
|
|
235
|
+
as described at the top of this file.
|
package/README.md
CHANGED
|
@@ -43,3 +43,13 @@ Search for the package name and add to your project.
|
|
|
43
43
|
- $ npm install node-red-contrib-buildingblocks-control
|
|
44
44
|
# then restart node-red
|
|
45
45
|
```
|
|
46
|
+
|
|
47
|
+
## Testing
|
|
48
|
+
Tests use [Mocha](https://mochajs.org/) and [node-red-node-test-helper](https://github.com/node-red/node-red-node-test-helper) to run nodes in an isolated, in-memory Node-RED runtime. Your live Node-RED instance is never touched.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install # install dev dependencies (mocha, test helper)
|
|
52
|
+
npm test # run all tests
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Test files live in `test/` and follow the naming convention `*_spec.js`. Shared utilities are in `test/test-helpers.js`.
|
|
@@ -61,9 +61,9 @@ Counts consecutive inputs from a configured property based on the selected mode,
|
|
|
61
61
|
|
|
62
62
|
### Details
|
|
63
63
|
Counts inputs according to the selected mode, reading from the configured **Input Property** (default: `msg.payload`):
|
|
64
|
-
- **Accumulate True
|
|
65
|
-
- **Accumulate False
|
|
66
|
-
- **Accumulate Flows
|
|
64
|
+
- **Accumulate True** counts consecutive `true` values (resets on `false` or explicit reset)
|
|
65
|
+
- **Accumulate False** counts consecutive `false` values (resets on `true` or explicit reset)
|
|
66
|
+
- **Accumulate Flows** counts all valid input messages (resets only on explicit reset)
|
|
67
67
|
|
|
68
68
|
Reset via `msg.context = "reset"` with `msg.payload = true`.
|
|
69
69
|
|
|
@@ -96,6 +96,7 @@
|
|
|
96
96
|
<div class="form-row">
|
|
97
97
|
<label for="node-input-message"><i class="fa fa-comment"></i> Message</label>
|
|
98
98
|
<input type="text" id="node-input-message" placeholder="Zone exceeds setpoint">
|
|
99
|
+
<input type="hidden" id="node-input-messageType">
|
|
99
100
|
</div>
|
|
100
101
|
|
|
101
102
|
<div class="form-row">
|
|
@@ -131,6 +132,7 @@
|
|
|
131
132
|
topic: { value: "Alarms_Default" },
|
|
132
133
|
title: { value: "Alarm" },
|
|
133
134
|
message: { value: "Condition triggered" },
|
|
135
|
+
messageType: { value: "str" },
|
|
134
136
|
tags: { value: "" },
|
|
135
137
|
units: { value: "°F" }
|
|
136
138
|
},
|
|
@@ -197,6 +199,15 @@
|
|
|
197
199
|
typeField: "#node-input-inputFieldType"
|
|
198
200
|
}).typedInput("type", node.inputFieldType || "msg").typedInput("value", node.inputField || "payload");
|
|
199
201
|
|
|
202
|
+
// Setup message typedInput: static string or from msg property
|
|
203
|
+
$("#node-input-message").typedInput({
|
|
204
|
+
types: [
|
|
205
|
+
"str",
|
|
206
|
+
"msg"
|
|
207
|
+
],
|
|
208
|
+
typeField: "#node-input-messageType"
|
|
209
|
+
}).typedInput("type", node.messageType || "str").typedInput("value", node.message || "Condition triggered");
|
|
210
|
+
|
|
200
211
|
// Show/hide sections based on inputMode
|
|
201
212
|
const updateDisplay = () => {
|
|
202
213
|
let mode = $("#node-input-inputMode").val();
|
package/nodes/alarm-collector.js
CHANGED
|
@@ -20,6 +20,7 @@ module.exports = function(RED) {
|
|
|
20
20
|
node.topic = config.topic || "Alarms_Default";
|
|
21
21
|
node.title = config.title || "Alarm";
|
|
22
22
|
node.message = config.message || "Condition triggered";
|
|
23
|
+
node.messageType = config.messageType || "str";
|
|
23
24
|
node.tags = config.tags || "";
|
|
24
25
|
node.units = config.units || "";
|
|
25
26
|
|
|
@@ -45,6 +46,20 @@ module.exports = function(RED) {
|
|
|
45
46
|
node.conditionMet = false;
|
|
46
47
|
node.valueChangedListener = null;
|
|
47
48
|
|
|
49
|
+
// Register with alarm-config at startup so the registry knows about this collector
|
|
50
|
+
if (node.alarmConfig) {
|
|
51
|
+
node.alarmConfig.register(node.id, {
|
|
52
|
+
name: node.name,
|
|
53
|
+
severity: node.priority,
|
|
54
|
+
status: 'cleared',
|
|
55
|
+
title: node.title,
|
|
56
|
+
message: node.message,
|
|
57
|
+
topic: node.topic,
|
|
58
|
+
value: null,
|
|
59
|
+
timestamp: new Date().toISOString()
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
utils.setStatusOK(node, `idle`);
|
|
49
64
|
|
|
50
65
|
// ====================================================================
|
|
@@ -179,15 +194,14 @@ module.exports = function(RED) {
|
|
|
179
194
|
|
|
180
195
|
// Register/update alarm in registry
|
|
181
196
|
if (node.alarmConfig) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
nodeId: node.id,
|
|
185
|
-
pointId: node.currentValue,
|
|
197
|
+
node.alarmConfig.register(node.id, {
|
|
198
|
+
name: node.name,
|
|
186
199
|
severity: node.priority,
|
|
187
200
|
status: node.alarmState ? 'active' : 'cleared',
|
|
188
201
|
title: node.title,
|
|
189
202
|
message: node.message,
|
|
190
203
|
topic: node.topic,
|
|
204
|
+
value: node.currentValue,
|
|
191
205
|
timestamp: new Date().toISOString()
|
|
192
206
|
});
|
|
193
207
|
}
|
|
@@ -257,6 +271,18 @@ module.exports = function(RED) {
|
|
|
257
271
|
}
|
|
258
272
|
}
|
|
259
273
|
|
|
274
|
+
// Resolve message dynamically if configured as msg property
|
|
275
|
+
if (node.messageType === "msg") {
|
|
276
|
+
try {
|
|
277
|
+
const resolved = await utils.evaluateNodeProperty(config.message, "msg", node, msg);
|
|
278
|
+
if (resolved !== undefined && resolved !== null) {
|
|
279
|
+
node.message = String(resolved);
|
|
280
|
+
}
|
|
281
|
+
} catch (e) {
|
|
282
|
+
// Keep existing message on error
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
260
286
|
evaluateAndEmit(inputValue);
|
|
261
287
|
} catch (err) {
|
|
262
288
|
utils.setStatusError(node, `Error reading input: ${err.message}`);
|
|
@@ -275,7 +301,7 @@ module.exports = function(RED) {
|
|
|
275
301
|
|
|
276
302
|
// Unregister alarm from registry
|
|
277
303
|
if (node.alarmConfig) {
|
|
278
|
-
node.alarmConfig.unregister(node.
|
|
304
|
+
node.alarmConfig.unregister(node.id);
|
|
279
305
|
}
|
|
280
306
|
|
|
281
307
|
// Remove global value-changed listener
|
package/nodes/alarm-config.html
CHANGED
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
const $list = $("#node-input-alarm-list");
|
|
37
37
|
|
|
38
38
|
function loadAlarms() {
|
|
39
|
-
$.getJSON(
|
|
39
|
+
$.getJSON(`alarm-config/list/${node.id}`, function(data) {
|
|
40
40
|
$list.empty();
|
|
41
41
|
if (!data.length) {
|
|
42
|
-
return $list.append('<li style="color:#999;">No alarms registered</li>');
|
|
42
|
+
return $list.append('<li style="color:#999;">No alarms registered (deploy first if newly added)</li>');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Data already sorted by server
|
|
@@ -63,9 +63,10 @@
|
|
|
63
63
|
width: '16px'
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
// Alarm info (name, severity,
|
|
66
|
+
// Alarm info (name, severity, topic)
|
|
67
|
+
const valuePart = alarm.value != null ? ` val:${alarm.value}` : '';
|
|
67
68
|
const infoText = $('<span>')
|
|
68
|
-
.html(`<strong>${alarm.name}</strong> <span style="color:#666;font-size:0.9em;">[${alarm.severity}]
|
|
69
|
+
.html(`<strong>${alarm.name}</strong> <span style="color:#666;font-size:0.9em;">[${alarm.severity}] ${alarm.topic}${valuePart}</span>`)
|
|
69
70
|
.css({ flexGrow: 1 });
|
|
70
71
|
|
|
71
72
|
// Reveal button (find node on canvas)
|
|
@@ -80,8 +81,12 @@
|
|
|
80
81
|
li.append(statusDot, infoText, revealBtn);
|
|
81
82
|
$list.append(li);
|
|
82
83
|
});
|
|
83
|
-
}).fail(function() {
|
|
84
|
-
|
|
84
|
+
}).fail(function(jqXHR) {
|
|
85
|
+
if (jqXHR.status === 401 || jqXHR.status === 403) {
|
|
86
|
+
$list.html('<li style="color:red;">Permission denied (alarm-config.read)</li>');
|
|
87
|
+
} else {
|
|
88
|
+
$list.html('<li style="color:red;">Could not load alarms (HTTP ' + jqXHR.status + ')</li>');
|
|
89
|
+
}
|
|
85
90
|
});
|
|
86
91
|
}
|
|
87
92
|
|
package/nodes/alarm-config.js
CHANGED
|
@@ -8,45 +8,39 @@ module.exports = function(RED) {
|
|
|
8
8
|
const utils = require('./utils')(RED);
|
|
9
9
|
utils.registerRegistryNode(node);
|
|
10
10
|
|
|
11
|
-
// The Map: stores alarm metadata by
|
|
12
|
-
// Format: { "
|
|
11
|
+
// The Map: stores alarm metadata keyed by collector node ID (always unique)
|
|
12
|
+
// Format: { "nodeId": { name: "Alarm Name", severity: "high", status: "active", ... } }
|
|
13
13
|
node.alarms = new Map();
|
|
14
14
|
|
|
15
|
-
// Register an alarm in the registry
|
|
16
|
-
node.register = function(
|
|
17
|
-
if (!
|
|
15
|
+
// Register an alarm in the registry (keyed by nodeId for uniqueness)
|
|
16
|
+
node.register = function(nodeId, meta) {
|
|
17
|
+
if (!nodeId || typeof nodeId !== 'string') {
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
if (node.alarms.has(
|
|
22
|
-
const existing = node.alarms.get(
|
|
23
|
-
//
|
|
24
|
-
if (existing.nodeId !== meta.nodeId) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
// Merge updates (preserving status if provided)
|
|
21
|
+
if (node.alarms.has(nodeId)) {
|
|
22
|
+
const existing = node.alarms.get(nodeId);
|
|
23
|
+
// Merge updates (preserving existing fields not in new meta)
|
|
28
24
|
meta = Object.assign({}, existing, meta);
|
|
29
25
|
}
|
|
30
|
-
node.alarms.set(
|
|
26
|
+
node.alarms.set(nodeId, meta);
|
|
31
27
|
return true;
|
|
32
28
|
};
|
|
33
29
|
|
|
34
|
-
// Unregister an alarm
|
|
35
|
-
node.unregister = function(
|
|
36
|
-
|
|
37
|
-
node.alarms.delete(alarmName);
|
|
38
|
-
}
|
|
30
|
+
// Unregister an alarm by node ID
|
|
31
|
+
node.unregister = function(nodeId) {
|
|
32
|
+
node.alarms.delete(nodeId);
|
|
39
33
|
};
|
|
40
34
|
|
|
41
|
-
// Lookup an alarm by
|
|
42
|
-
node.lookup = function(
|
|
43
|
-
return node.alarms.get(
|
|
35
|
+
// Lookup an alarm by node ID
|
|
36
|
+
node.lookup = function(nodeId) {
|
|
37
|
+
return node.alarms.get(nodeId);
|
|
44
38
|
};
|
|
45
39
|
|
|
46
|
-
// Update alarm status
|
|
47
|
-
node.updateStatus = function(
|
|
48
|
-
if (node.alarms.has(
|
|
49
|
-
const alarm = node.alarms.get(
|
|
40
|
+
// Update alarm status by node ID
|
|
41
|
+
node.updateStatus = function(nodeId, status) {
|
|
42
|
+
if (node.alarms.has(nodeId)) {
|
|
43
|
+
const alarm = node.alarms.get(nodeId);
|
|
50
44
|
alarm.status = status; // 'active' or 'cleared'
|
|
51
45
|
alarm.lastUpdate = new Date().toISOString();
|
|
52
46
|
return true;
|
|
@@ -57,8 +51,8 @@ module.exports = function(RED) {
|
|
|
57
51
|
// Get all alarms
|
|
58
52
|
node.getAll = function() {
|
|
59
53
|
const arr = [];
|
|
60
|
-
for (const [
|
|
61
|
-
arr.push({
|
|
54
|
+
for (const [nodeId, meta] of node.alarms.entries()) {
|
|
55
|
+
arr.push({ nodeId, ...meta });
|
|
62
56
|
}
|
|
63
57
|
return arr;
|
|
64
58
|
};
|
|
@@ -73,7 +67,8 @@ module.exports = function(RED) {
|
|
|
73
67
|
// Find the alarm-config node
|
|
74
68
|
const configNode = RED.nodes.getNode(configId);
|
|
75
69
|
if (!configNode) {
|
|
76
|
-
return
|
|
70
|
+
// Not deployed yet — return empty list so the editor can show a friendly message
|
|
71
|
+
return res.json([]);
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
// Get all alarms from this config
|
|
@@ -104,20 +99,24 @@ module.exports = function(RED) {
|
|
|
104
99
|
return res.json({ status: result, warning: "Configuration not deployed" });
|
|
105
100
|
}
|
|
106
101
|
|
|
107
|
-
// Check for the alarm
|
|
108
|
-
entry = configNode.lookup(
|
|
102
|
+
// Check for the alarm — map is keyed by nodeId
|
|
103
|
+
entry = configNode.lookup(checkNodeId);
|
|
104
|
+
|
|
109
105
|
if (entry) {
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
result = "assigned";
|
|
107
|
+
} else {
|
|
108
|
+
// Check if any other node has the same alarm name (name collision check)
|
|
109
|
+
const allAlarms = configNode.getAll();
|
|
110
|
+
const nameMatch = allAlarms.find(a => a.name === alarmName && a.nodeId !== checkNodeId);
|
|
111
|
+
if (nameMatch) {
|
|
112
112
|
collision = true;
|
|
113
|
+
entry = nameMatch;
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
if (collision) {
|
|
117
118
|
result = "collision";
|
|
118
|
-
} else if (!
|
|
119
|
-
result = "assigned";
|
|
120
|
-
} else {
|
|
119
|
+
} else if (!entry) {
|
|
121
120
|
result = "available";
|
|
122
121
|
}
|
|
123
122
|
|
package/nodes/alarm-service.js
CHANGED
|
@@ -48,7 +48,7 @@ module.exports = function(RED) {
|
|
|
48
48
|
|
|
49
49
|
// Send alarm message with status
|
|
50
50
|
const msg = {
|
|
51
|
-
|
|
51
|
+
alarm: eventData,
|
|
52
52
|
status: { state: "triggered", transition: eventData.transition },
|
|
53
53
|
activeAlarmCount: activeCount,
|
|
54
54
|
alarmKey: key
|
|
@@ -71,7 +71,7 @@ module.exports = function(RED) {
|
|
|
71
71
|
|
|
72
72
|
// Send clear message with status
|
|
73
73
|
const msg = {
|
|
74
|
-
|
|
74
|
+
alarm: eventData,
|
|
75
75
|
status: { state: "cleared", transition: eventData.transition },
|
|
76
76
|
activeAlarmCount: activeCount,
|
|
77
77
|
alarmKey: key
|
package/nodes/and-block.js
CHANGED
|
@@ -48,7 +48,7 @@ module.exports = function(RED) {
|
|
|
48
48
|
node.inputs[slotVal.index - 1] = Boolean(msg.payload);
|
|
49
49
|
const result = node.inputs.every(v => v === true);
|
|
50
50
|
const isUnchanged = result === lastResult && node.inputs.every((v, i) => v === lastInputs[i]);
|
|
51
|
-
const statusText = `
|
|
51
|
+
const statusText = `[${node.inputs.join(", ")}] -> ${result}`;
|
|
52
52
|
|
|
53
53
|
// ================================================================
|
|
54
54
|
// Debounce: Suppress consecutive same outputs within 500ms
|
|
@@ -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-state" title="Initial switch state"><i class="fa fa-toggle-on"></i> State</label>
|
|
9
|
+
<input type="checkbox" id="node-input-state" style="width: auto;">
|
|
10
|
+
</div>
|
|
7
11
|
</script>
|
|
8
12
|
|
|
9
13
|
<!-- JavaScript Section -->
|
|
@@ -29,30 +33,39 @@
|
|
|
29
33
|
|
|
30
34
|
<!-- Help Section -->
|
|
31
35
|
<script type="text/markdown" data-help-name="boolean-switch-block">
|
|
32
|
-
|
|
36
|
+
A boolean gate that routes messages based on a `true`/`false` switch state.
|
|
33
37
|
|
|
34
38
|
### Inputs
|
|
35
|
-
: context (string) :
|
|
36
|
-
: payload (any) : Flow data for `"inTrue"`
|
|
39
|
+
: context (string) : Message tag: `"in"`, `"inTrue"`, `"inFalse"`, `"toggle"`, or `"switch"`.
|
|
40
|
+
: payload (any) : Flow data for `"in"` / `"inTrue"` / `"inFalse"`; boolean for `"switch"`.
|
|
41
|
+
|
|
42
|
+
#### Contexts
|
|
43
|
+
- **`"in"`** — Routes `msg` to `outTrue` when state is `true`, or `outFalse` when state is `false`. Always flows through.
|
|
44
|
+
- **`"inTrue"`** — Passes `msg` to `outTrue` only when state is `true`. Blocked with warning when `false`.
|
|
45
|
+
- **`"inFalse"`** — Passes `msg` to `outFalse` only when state is `false`. Blocked with warning when `true`.
|
|
46
|
+
- **`"toggle"`** — Flips the switch state and emits new state on `outControl`.
|
|
47
|
+
- **`"switch"`** — Sets the switch state to `!!msg.payload` and emits on `outControl`.
|
|
37
48
|
|
|
38
49
|
### Outputs
|
|
39
|
-
: outTrue (msg) : Receives `msg`
|
|
40
|
-
: outFalse (msg) : Receives `msg`
|
|
41
|
-
: outControl (msg) : `
|
|
50
|
+
: outTrue (msg) : Receives `msg` when state is `true` (via `"in"` or `"inTrue"`).
|
|
51
|
+
: outFalse (msg) : Receives `msg` when state is `false` (via `"in"` or `"inFalse"`).
|
|
52
|
+
: outControl (msg) : Emits `{ payload: <boolean> }` on `"toggle"` or `"switch"` only.
|
|
42
53
|
|
|
43
54
|
### Details
|
|
44
|
-
|
|
55
|
+
This node acts as a boolean gate/router.
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
Use `"in"` to auto-route messages to the correct output based on state — messages always flow through.
|
|
58
|
+
Use `"inTrue"`/`"inFalse"` for conditional gating — messages are blocked (with yellow warning) when state doesn't match.
|
|
47
59
|
|
|
48
|
-
State can be controlled
|
|
60
|
+
State can be controlled with `"toggle"` (flip) or `"switch"` (set explicitly via payload).
|
|
61
|
+
The `outControl` port fires only on state commands, not on routed data messages.
|
|
49
62
|
|
|
50
63
|
### Status
|
|
51
|
-
- Green (dot)
|
|
52
|
-
- Blue (dot)
|
|
53
|
-
- Blue (ring)
|
|
54
|
-
-
|
|
55
|
-
-
|
|
64
|
+
- **Green (dot)**: Message routed successfully
|
|
65
|
+
- **Blue (dot)**: State changed (`toggle` / `switch`)
|
|
66
|
+
- **Blue (ring)**: State unchanged (`switch` with same value)
|
|
67
|
+
- **Yellow (ring)**: Message blocked (`inTrue` when false, `inFalse` when true) or unknown context
|
|
68
|
+
- **Red (ring)**: Error (missing context, invalid message)
|
|
56
69
|
|
|
57
70
|
### References
|
|
58
71
|
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
@@ -5,8 +5,8 @@ module.exports = function(RED) {
|
|
|
5
5
|
RED.nodes.createNode(this, config);
|
|
6
6
|
const node = this;
|
|
7
7
|
|
|
8
|
-
// Initialize state from config
|
|
9
|
-
node.state = config.state;
|
|
8
|
+
// Initialize state from config (coerce to boolean)
|
|
9
|
+
node.state = !!config.state;
|
|
10
10
|
|
|
11
11
|
// Set initial status
|
|
12
12
|
utils.setStatusOK(node, `state: ${node.state}`);
|
|
@@ -30,31 +30,41 @@ module.exports = function(RED) {
|
|
|
30
30
|
|
|
31
31
|
// Handle context commands
|
|
32
32
|
switch (msg.context) {
|
|
33
|
-
case "toggle":
|
|
33
|
+
case "toggle": {
|
|
34
34
|
node.state = !node.state;
|
|
35
35
|
utils.setStatusChanged(node, `state: ${node.state}`);
|
|
36
36
|
send([null, null, { payload: node.state }]);
|
|
37
37
|
break;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case "switch": {
|
|
41
|
+
const newState = !!msg.payload;
|
|
42
|
+
if (newState === node.state) {
|
|
43
|
+
utils.setStatusUnchanged(node, `state: ${node.state}`);
|
|
44
|
+
} else {
|
|
45
|
+
node.state = newState;
|
|
46
|
+
utils.setStatusChanged(node, `state: ${node.state}`);
|
|
47
|
+
}
|
|
41
48
|
send([null, null, { payload: node.state }]);
|
|
42
49
|
break;
|
|
50
|
+
}
|
|
43
51
|
case "inTrue":
|
|
44
52
|
if (node.state) {
|
|
45
|
-
utils.setStatusOK(node, `
|
|
46
|
-
send([msg, null,
|
|
53
|
+
utils.setStatusOK(node, `outTrue: ${msg.payload}`);
|
|
54
|
+
send([msg, null, null]);
|
|
47
55
|
}
|
|
48
56
|
break;
|
|
57
|
+
|
|
49
58
|
case "inFalse":
|
|
50
59
|
if (!node.state) {
|
|
51
|
-
utils.setStatusOK(node, `
|
|
52
|
-
send([null, msg,
|
|
60
|
+
utils.setStatusOK(node, `outFalse: ${msg.payload}`);
|
|
61
|
+
send([null, msg, null]);
|
|
53
62
|
}
|
|
54
63
|
break;
|
|
64
|
+
|
|
55
65
|
default:
|
|
56
|
-
utils.setStatusWarn(node,
|
|
57
|
-
if (done) done("Unknown context");
|
|
66
|
+
utils.setStatusWarn(node, `unknown context: ${msg.context}`);
|
|
67
|
+
if (done) done("Unknown context: " + msg.context);
|
|
58
68
|
return;
|
|
59
69
|
}
|
|
60
70
|
if (done) done();
|