@bldgblocks/node-red-contrib-control 0.1.33 → 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 +74 -67
- package/nodes/global-setter.html +1 -1
- package/nodes/global-setter.js +168 -188
- 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-point-register.js +126 -0
- 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-service-read.js +58 -0
- 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-service-write.js +83 -0
- 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 +275 -3
- package/package.json +14 -6
- package/nodes/network-read.html +0 -56
- package/nodes/network-read.js +0 -59
- package/nodes/network-register.js +0 -161
- package/nodes/network-write.html +0 -64
- package/nodes/network-write.js +0 -126
|
@@ -230,7 +230,19 @@
|
|
|
230
230
|
</script>
|
|
231
231
|
|
|
232
232
|
<script type="text/markdown" data-help-name="history-config">
|
|
233
|
-
|
|
233
|
+
Configuration node for the History Collector. Defines storage behavior and data retention policies.
|
|
234
234
|
|
|
235
|
+
### Details
|
|
235
236
|
|
|
237
|
+
This node does not process messages directly. It serves as a configuration reference for one or more History Collector nodes. The properties you set here determine how data is collected, stored, and aged out of the history buffer.
|
|
238
|
+
|
|
239
|
+
Configure the node in the editor panel on the right.
|
|
240
|
+
|
|
241
|
+
### Status
|
|
242
|
+
- Green (dot): Configuration valid
|
|
243
|
+
- Red (ring): Error in configuration
|
|
244
|
+
|
|
245
|
+
### References
|
|
246
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
247
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
236
248
|
</script>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="history-service">
|
|
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-historyConfig" title="History configuration to relay events from"><i class="fa fa-database"></i> History Config</label>
|
|
8
|
+
<input type="text" id="node-input-historyConfig">
|
|
9
|
+
</div>
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script type="text/javascript">
|
|
13
|
+
RED.nodes.registerType("history-service", {
|
|
14
|
+
category: "bldgblocks history",
|
|
15
|
+
color: "#b9f2ff",
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: "" },
|
|
18
|
+
historyConfig: {
|
|
19
|
+
value: "",
|
|
20
|
+
required: true,
|
|
21
|
+
type: "history-config"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
inputs: 0,
|
|
25
|
+
outputs: 1,
|
|
26
|
+
outputLabels: ["records"],
|
|
27
|
+
icon: "font-awesome/fa-database",
|
|
28
|
+
paletteLabel: "history-service",
|
|
29
|
+
label: function() {
|
|
30
|
+
const historyNode = RED.nodes.node(this.historyConfig);
|
|
31
|
+
const configName = historyNode ? historyNode.name : "unknown";
|
|
32
|
+
return this.name ? `${this.name} (${configName})` : `history-service (${configName})`;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<script type="text/markdown" data-help-name="history-service">
|
|
38
|
+
Event relay for history collectors.
|
|
39
|
+
|
|
40
|
+
### Purpose
|
|
41
|
+
Receives events emitted by **history-collector** nodes and outputs them as individual records. Acts as an event-to-message bridge, allowing collectors to emit data without needing output wires.
|
|
42
|
+
|
|
43
|
+
### Configuration
|
|
44
|
+
- **History Config**: The history-config node to relay events from
|
|
45
|
+
|
|
46
|
+
### Outputs
|
|
47
|
+
: payload (object) : History record object containing:
|
|
48
|
+
- `measurement` (string): Measurement name
|
|
49
|
+
- `timestamp` (number): Nanosecond Unix timestamp
|
|
50
|
+
- `fields` (object): Values (typically `{value: number}`)
|
|
51
|
+
- `tags` (object): Metadata tags including `historyGroup`
|
|
52
|
+
- `lineProtocol` (string): Pre-formatted InfluxDB line protocol
|
|
53
|
+
- `seriesName` (string): Original series name from collector
|
|
54
|
+
- `historyConfigId` (string): Config ID
|
|
55
|
+
- `historyConfigName` (string): Config name
|
|
56
|
+
|
|
57
|
+
### Details
|
|
58
|
+
This node listens for events emitted by all **history-collector** nodes configured with the same history-config. Each incoming record is output immediately.
|
|
59
|
+
|
|
60
|
+
**Typical wiring**:
|
|
61
|
+
```
|
|
62
|
+
[history-collector] ──(event)──> [history-service] ──> [join] ──> [influxdb batch]
|
|
63
|
+
(batches)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use the **join** node to batch records if needed:
|
|
67
|
+
- Set join mode: "Custom"
|
|
68
|
+
- Build: "Array"
|
|
69
|
+
- Count: number of records to batch (e.g., 5000)
|
|
70
|
+
- Or timeout: seconds to wait before sending partial batch
|
|
71
|
+
|
|
72
|
+
This design decouples collection from storage and lets you choose batching strategy.
|
|
73
|
+
|
|
74
|
+
### Status
|
|
75
|
+
- Green (dot): Ready and listening
|
|
76
|
+
- Blue (dot): Just relayed a record
|
|
77
|
+
- Red (ring): Configuration error
|
|
78
|
+
|
|
79
|
+
### References
|
|
80
|
+
- [history-collector](history-collector.html) - Emits events to this service
|
|
81
|
+
- [history-config](history-config.html) - Configuration node
|
|
82
|
+
- [Node-RED join node](https://nodered.org/docs/nodes/core/flow/join) - For batching
|
|
83
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
84
|
+
</script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require("./utils")(RED);
|
|
3
|
+
|
|
4
|
+
function HistoryServiceNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
this.historyConfig = RED.nodes.getNode(config.historyConfig);
|
|
7
|
+
const node = this;
|
|
8
|
+
|
|
9
|
+
// Validate configuration
|
|
10
|
+
if (!node.historyConfig) {
|
|
11
|
+
utils.setStatusError(node, "missing history config");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Generate matching event name based on history-config ID
|
|
16
|
+
const eventName = `bldgblocks:history:${node.historyConfig.id}`;
|
|
17
|
+
|
|
18
|
+
// Listen for events from history-collector nodes with this config
|
|
19
|
+
const eventListener = (eventData) => {
|
|
20
|
+
// Guard against invalid event data
|
|
21
|
+
if (!eventData || typeof eventData !== 'object') {
|
|
22
|
+
utils.setStatusError(node, "invalid event data");
|
|
23
|
+
node.warn("Invalid event data received");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Create output message with the event data as payload
|
|
28
|
+
// Preserve topic if it exists in the event data
|
|
29
|
+
const msg = {
|
|
30
|
+
payload: eventData,
|
|
31
|
+
topic: eventData.topic
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
node.send(msg);
|
|
35
|
+
|
|
36
|
+
// Update status
|
|
37
|
+
utils.setStatusChanged(node, `relayed: ${eventData.seriesName || 'data'}`);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Subscribe to events
|
|
41
|
+
RED.events.on(eventName, eventListener);
|
|
42
|
+
utils.setStatusOK(node, `listening on ${node.historyConfig.name}`);
|
|
43
|
+
|
|
44
|
+
node.on("close", function(done) {
|
|
45
|
+
// Unsubscribe from events on close
|
|
46
|
+
RED.events.off(eventName, eventListener);
|
|
47
|
+
done();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
RED.nodes.registerType("history-service", HistoryServiceNode);
|
|
52
|
+
};
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
<label for="node-input-name"><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-upperLimit"><i class="fa fa-arrow-up"></i> Upper Limit (turn on)</label>
|
|
9
13
|
<input type="text" id="node-input-upperLimit" placeholder="50">
|
|
@@ -34,6 +38,7 @@
|
|
|
34
38
|
color: "#301934",
|
|
35
39
|
defaults: {
|
|
36
40
|
name: { value: "" },
|
|
41
|
+
inputProperty: { value: "payload" },
|
|
37
42
|
upperLimit: { value: 50, required: true },
|
|
38
43
|
upperLimitType: { value: "num" },
|
|
39
44
|
lowerLimit: { value: 30, required: true },
|
|
@@ -5,6 +5,7 @@ module.exports = function(RED) {
|
|
|
5
5
|
RED.nodes.createNode(this, config);
|
|
6
6
|
const node = this;
|
|
7
7
|
node.name = config.name;
|
|
8
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
8
9
|
node.state = "within";
|
|
9
10
|
node.isBusy = false;
|
|
10
11
|
node.upperLimit = parseFloat(config.upperLimit);
|
|
@@ -16,7 +17,7 @@ module.exports = function(RED) {
|
|
|
16
17
|
send = send || function() { node.send.apply(node, arguments); };
|
|
17
18
|
|
|
18
19
|
if (!msg) {
|
|
19
|
-
|
|
20
|
+
utils.setStatusError(node, "invalid message");
|
|
20
21
|
if (done) done();
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
@@ -27,7 +28,7 @@ module.exports = function(RED) {
|
|
|
27
28
|
// Check busy lock
|
|
28
29
|
if (node.isBusy) {
|
|
29
30
|
// Update status to let user know they are pushing too fast
|
|
30
|
-
|
|
31
|
+
utils.setStatusBusy(node, "busy - dropped msg");
|
|
31
32
|
if (done) done();
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
@@ -107,27 +108,27 @@ module.exports = function(RED) {
|
|
|
107
108
|
const value = parseFloat(msg.payload);
|
|
108
109
|
if (!isNaN(value) && value >= 0) {
|
|
109
110
|
node.upperLimitThreshold = value;
|
|
110
|
-
|
|
111
|
+
utils.setStatusOK(node, `upperLimitThreshold: ${value}`);
|
|
111
112
|
}
|
|
112
113
|
} else if (msg.context === "lowerLimitThreshold") {
|
|
113
114
|
const value = parseFloat(msg.payload);
|
|
114
115
|
if (!isNaN(value) && value >= 0) {
|
|
115
116
|
node.lowerLimitThreshold = value;
|
|
116
|
-
|
|
117
|
+
utils.setStatusOK(node, `lowerLimitThreshold: ${value}`);
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
if (done) done();
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
let inputValue;
|
|
125
|
+
try {
|
|
126
|
+
inputValue = parseFloat(RED.util.getMessageProperty(msg, node.inputProperty));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
inputValue = NaN;
|
|
127
129
|
}
|
|
128
|
-
const inputValue = parseFloat(msg.payload);
|
|
129
130
|
if (isNaN(inputValue)) {
|
|
130
|
-
|
|
131
|
+
utils.setStatusError(node, "invalid input");
|
|
131
132
|
if (done) done();
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
@@ -140,7 +141,7 @@ module.exports = function(RED) {
|
|
|
140
141
|
|
|
141
142
|
// Add validation to ensure numbers
|
|
142
143
|
if (isNaN(upperTurnOn) || isNaN(upperTurnOff) || isNaN(lowerTurnOn) || isNaN(lowerTurnOff)) {
|
|
143
|
-
|
|
144
|
+
utils.setStatusError(node, "invalid limits calculation");
|
|
144
145
|
if (done) done();
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
@@ -179,11 +180,7 @@ module.exports = function(RED) {
|
|
|
179
180
|
{ payload: newState === "below" }
|
|
180
181
|
];
|
|
181
182
|
|
|
182
|
-
node.
|
|
183
|
-
fill: "blue",
|
|
184
|
-
shape: "dot",
|
|
185
|
-
text: `in: ${inputValue.toFixed(2)}, state: ${newState}`
|
|
186
|
-
});
|
|
183
|
+
utils.setStatusChanged(node, `in: ${inputValue.toFixed(2)}, state: ${newState}`);
|
|
187
184
|
|
|
188
185
|
node.state = newState;
|
|
189
186
|
send(output);
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
4
4
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
5
5
|
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
|
|
8
|
+
<input type="text" id="node-input-inputProperty" placeholder="payload">
|
|
9
|
+
</div>
|
|
6
10
|
<div class="form-row">
|
|
7
11
|
<label for="node-input-points" title="Default points table for interpolation (array of {x, y} objects, ≥2 points)"><i class="fa fa-table"></i> Points</label>
|
|
8
12
|
<textarea id="node-input-points" placeholder='[{"x": 0, "y": 0}, {"x": 100, "y": 100}]' style="height: 100px;"></textarea>
|
|
@@ -15,6 +19,7 @@
|
|
|
15
19
|
color: "#301934",
|
|
16
20
|
defaults: {
|
|
17
21
|
name: { value: "" },
|
|
22
|
+
inputProperty: { value: "payload" },
|
|
18
23
|
points: {
|
|
19
24
|
value: JSON.stringify([{ x: 0, y: 0 }, { x: 100, y: 100 }], null, 2),
|
|
20
25
|
required: true,
|
|
@@ -43,11 +48,24 @@
|
|
|
43
48
|
</script>
|
|
44
49
|
|
|
45
50
|
<script type="text/markdown" data-help-name="interpolate-block">
|
|
46
|
-
Linearly interpolates
|
|
51
|
+
Linearly interpolates numeric input from a configured property using a points table.
|
|
47
52
|
|
|
48
53
|
### Inputs
|
|
54
|
+
: input-property (number) : Numeric value to interpolate, read from the configured Input Property.
|
|
49
55
|
: context (string) : Configures points table (`"points"`).
|
|
50
|
-
: payload (
|
|
56
|
+
: payload (array, for context) : Array of `{x, y}` objects for points configuration.
|
|
57
|
+
|
|
58
|
+
### Outputs
|
|
59
|
+
: payload (number) : Interpolated output value.
|
|
60
|
+
|
|
61
|
+
### Properties
|
|
62
|
+
: name (string) : Display name in editor.
|
|
63
|
+
: inputProperty (string) : Message property to read input from (default: `payload`). Supports nested properties (e.g., `data.value`).
|
|
64
|
+
: points (array) : Array of `{x, y}` objects defining the interpolation curve (minimum 2 points).
|
|
65
|
+
|
|
66
|
+
### Details
|
|
67
|
+
Linearly interpolates numeric input (read from the configured **Input Property**, default: `msg.payload`) using a configurable points table.
|
|
68
|
+
Points can be updated dynamically via `msg.context = "points"` with an array of `{x, y}` objects in `msg.payload`.
|
|
51
69
|
|
|
52
70
|
### Outputs
|
|
53
71
|
: payload (number) : Interpolated output value.
|
|
@@ -1,34 +1,31 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
2
3
|
function InterpolateBlockNode(config) {
|
|
3
4
|
RED.nodes.createNode(this, config);
|
|
4
5
|
|
|
5
6
|
const node = this;
|
|
6
7
|
|
|
7
8
|
// Initialize runtime state
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
// Initialize state
|
|
10
|
+
node.name = config.name;
|
|
11
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
12
|
+
node.points = null;
|
|
13
|
+
node.lastOutput = null;
|
|
13
14
|
|
|
14
15
|
// Initialize points
|
|
15
16
|
try {
|
|
16
|
-
node.
|
|
17
|
-
if (!Array.isArray(node.
|
|
18
|
-
!node.
|
|
17
|
+
node.points = config.points ? JSON.parse(config.points) : [{ x: 0, y: 0 }, { x: 100, y: 100 }];
|
|
18
|
+
if (!Array.isArray(node.points) || node.points.length < 2 ||
|
|
19
|
+
!node.points.every(p => typeof p.x === "number" && !isNaN(p.x) &&
|
|
19
20
|
typeof p.y === "number" && !isNaN(p.y))) {
|
|
20
|
-
node.
|
|
21
|
-
|
|
21
|
+
node.points = [{ x: 0, y: 0 }, { x: 100, y: 100 }];
|
|
22
|
+
utils.setStatusError(node, "invalid points, using default");
|
|
22
23
|
} else {
|
|
23
|
-
|
|
24
|
-
fill: "green",
|
|
25
|
-
shape: "dot",
|
|
26
|
-
text: `name: ${node.runtime.name}, points: ${node.runtime.points.length}`
|
|
27
|
-
});
|
|
24
|
+
utils.setStatusOK(node, `name: ${node.name}, points: ${node.points.length}`);
|
|
28
25
|
}
|
|
29
26
|
} catch (e) {
|
|
30
|
-
node.
|
|
31
|
-
|
|
27
|
+
node.points = [{ x: 0, y: 0 }, { x: 100, y: 100 }];
|
|
28
|
+
utils.setStatusError(node, "invalid points, using default");
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
node.on("input", function(msg, send, done) {
|
|
@@ -36,7 +33,7 @@ module.exports = function(RED) {
|
|
|
36
33
|
|
|
37
34
|
// Guard against invalid msg
|
|
38
35
|
if (!msg) {
|
|
39
|
-
|
|
36
|
+
utils.setStatusError(node, "invalid message");
|
|
40
37
|
if (done) done();
|
|
41
38
|
return;
|
|
42
39
|
}
|
|
@@ -44,12 +41,12 @@ module.exports = function(RED) {
|
|
|
44
41
|
// Handle configuration messages
|
|
45
42
|
if (msg.context) {
|
|
46
43
|
if (typeof msg.context !== "string" || !msg.context.trim()) {
|
|
47
|
-
|
|
44
|
+
utils.setStatusWarn(node, "unknown context");
|
|
48
45
|
if (done) done();
|
|
49
46
|
return;
|
|
50
47
|
}
|
|
51
48
|
if (!msg.hasOwnProperty("payload")) {
|
|
52
|
-
|
|
49
|
+
utils.setStatusError(node, "missing payload");
|
|
53
50
|
if (done) done();
|
|
54
51
|
return;
|
|
55
52
|
}
|
|
@@ -59,49 +56,43 @@ module.exports = function(RED) {
|
|
|
59
56
|
if (Array.isArray(newPoints) && newPoints.length >= 2 &&
|
|
60
57
|
newPoints.every(p => typeof p.x === "number" && !isNaN(p.x) &&
|
|
61
58
|
typeof p.y === "number" && !isNaN(p.y))) {
|
|
62
|
-
node.
|
|
63
|
-
|
|
64
|
-
fill: "green",
|
|
65
|
-
shape: "dot",
|
|
66
|
-
text: `points: ${newPoints.length}`
|
|
67
|
-
});
|
|
59
|
+
node.points = newPoints;
|
|
60
|
+
utils.setStatusOK(node, `points: ${newPoints.length}`);
|
|
68
61
|
} else {
|
|
69
|
-
|
|
62
|
+
utils.setStatusError(node, "invalid points");
|
|
70
63
|
}
|
|
71
64
|
} catch (e) {
|
|
72
|
-
|
|
65
|
+
utils.setStatusError(node, "invalid points");
|
|
73
66
|
}
|
|
74
67
|
if (done) done();
|
|
75
68
|
return;
|
|
76
69
|
} else {
|
|
77
|
-
|
|
70
|
+
utils.setStatusWarn(node, "unknown context");
|
|
78
71
|
if (done) done();
|
|
79
72
|
return;
|
|
80
73
|
}
|
|
81
74
|
}
|
|
82
75
|
|
|
83
|
-
// Check for missing
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
// Check for missing input property
|
|
77
|
+
let inputValue;
|
|
78
|
+
try {
|
|
79
|
+
inputValue = parseFloat(RED.util.getMessageProperty(msg, node.inputProperty));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
inputValue = NaN;
|
|
88
82
|
}
|
|
89
|
-
|
|
90
|
-
// Process input
|
|
91
|
-
const inputValue = parseFloat(msg.payload);
|
|
92
83
|
if (isNaN(inputValue)) {
|
|
93
|
-
|
|
84
|
+
utils.setStatusError(node, "missing or invalid input property");
|
|
94
85
|
if (done) done();
|
|
95
86
|
return;
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
// Linear interpolation
|
|
99
90
|
let outputValue = NaN;
|
|
100
|
-
const isPositiveSlope = node.
|
|
91
|
+
const isPositiveSlope = node.points.length >= 2 && node.points[1].x > node.points[0].x;
|
|
101
92
|
|
|
102
|
-
for (let i = 0; i < node.
|
|
103
|
-
let x1 = node.
|
|
104
|
-
let x2 = node.
|
|
93
|
+
for (let i = 0; i < node.points.length - 1; i++) {
|
|
94
|
+
let x1 = node.points[i].x, y1 = node.points[i].y;
|
|
95
|
+
let x2 = node.points[i + 1].x, y2 = node.points[i + 1].y;
|
|
105
96
|
if (isPositiveSlope ? (inputValue >= x1 && inputValue <= x2) : (inputValue <= x1 && inputValue >= x2)) {
|
|
106
97
|
let m = (y2 - y1) / (x2 - x1);
|
|
107
98
|
let b = y1 - (m * x1);
|
|
@@ -111,21 +102,19 @@ module.exports = function(RED) {
|
|
|
111
102
|
}
|
|
112
103
|
|
|
113
104
|
if (isNaN(outputValue)) {
|
|
114
|
-
|
|
105
|
+
utils.setStatusError(node, "input out of range");
|
|
115
106
|
if (done) done();
|
|
116
107
|
return;
|
|
117
108
|
}
|
|
118
109
|
|
|
119
110
|
// Check if output value has changed
|
|
120
|
-
const isUnchanged = outputValue === node.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
text: `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`
|
|
125
|
-
});
|
|
111
|
+
const isUnchanged = outputValue === node.lastOutput;
|
|
112
|
+
const statusShape = isUnchanged ? "ring" : "dot";
|
|
113
|
+
utils.setStatusOK(node, `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`);
|
|
114
|
+
if (statusShape === "ring") utils.setStatusUnchanged(node, `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`);
|
|
126
115
|
|
|
127
116
|
if (!isUnchanged) {
|
|
128
|
-
node.
|
|
117
|
+
node.lastOutput = outputValue;
|
|
129
118
|
send({ payload: outputValue });
|
|
130
119
|
}
|
|
131
120
|
|
package/nodes/join.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!-- Data Template -->
|
|
2
|
+
<script type="text/html" data-template-name="bldgblocks-join">
|
|
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
|
+
|
|
8
|
+
<div class="form-row">
|
|
9
|
+
<label for="node-input-count"><i class="fa fa-sort-numeric-asc"></i> Key Count</label>
|
|
10
|
+
<input type="number" id="node-input-count" placeholder="4">
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="form-row">
|
|
14
|
+
<label for="node-input-excludedKeys"><i class="fa fa-ban"></i> Exclude</label>
|
|
15
|
+
<input type="text" id="node-input-excludedKeys" placeholder="status, req, res">
|
|
16
|
+
<div style="font-size: 0.8em; color: #888; margin-left: 105px;">Comma separated list of properties to ignore</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="form-tips">
|
|
20
|
+
<b>Note:</b> Works with root properties only and must all be unique. Nested properties are not supported.
|
|
21
|
+
Instead of incoming properties like 'outdoor.temp' (a nested object), consider using outdoor/temp or outdoorTemp as property names.
|
|
22
|
+
</div>
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<!-- Registration & logic -->
|
|
26
|
+
<script type="text/javascript">
|
|
27
|
+
RED.nodes.registerType('bldgblocks-join', {
|
|
28
|
+
category: "bldgblocks control",
|
|
29
|
+
color: '#301934',
|
|
30
|
+
defaults: {
|
|
31
|
+
name: { value: "" },
|
|
32
|
+
count: { value: 4, required: true, validate: RED.validators.number() },
|
|
33
|
+
excludedKeys: { value: "status" } // Default to 'status' to maintain previous behavior
|
|
34
|
+
},
|
|
35
|
+
inputs: 1,
|
|
36
|
+
outputs: 1,
|
|
37
|
+
icon: "font-awesome/fa-compress",
|
|
38
|
+
label: function() {
|
|
39
|
+
return this.name || "join (" + this.count + ")";
|
|
40
|
+
},
|
|
41
|
+
paletteLabel: "join",
|
|
42
|
+
oneditprepare: function() {
|
|
43
|
+
// Optional: visual tweaks when opening the edit dialog
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<!-- Help Section -->
|
|
49
|
+
<script type="text/markdown" data-help-name="bldgblocks-join">
|
|
50
|
+
Joins multiple messages into a single message by accumulating unique properties.
|
|
51
|
+
This differs from the standard Node-RED Join node by focusing on unique keys rather than message counts or parts
|
|
52
|
+
and combines to root level using property names.
|
|
53
|
+
|
|
54
|
+
### Inputs
|
|
55
|
+
: all (any) : All unique properties from incoming messages.
|
|
56
|
+
: Key Count (number) : The number of unique keys required before the node emits the accumulated message.
|
|
57
|
+
: Exclude (string) : A comma-separated list of properties to ignore (e.g., `status, topic`).
|
|
58
|
+
|
|
59
|
+
### Outputs
|
|
60
|
+
: msg (any) : All unique properties combined at root level into a single message.
|
|
61
|
+
|
|
62
|
+
### Details
|
|
63
|
+
1. The node stores a running list of properties from every `msg` it receives.
|
|
64
|
+
2. It ignores properties starting with `_` (like `_msgid`).
|
|
65
|
+
3. It ignores any properties listed in the `Exclude` configuration.
|
|
66
|
+
4. When the number of stored unique keys equals or exceeds the `Key Count`, it emits the combined object as a new message.
|
|
67
|
+
|
|
68
|
+
### Status
|
|
69
|
+
- Green (dot): Configuration update
|
|
70
|
+
- Blue (dot): State changed
|
|
71
|
+
- Blue (ring): State unchanged
|
|
72
|
+
- Red (ring): Error
|
|
73
|
+
- Yellow (ring): Warning
|
|
74
|
+
|
|
75
|
+
### References
|
|
76
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
77
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
78
|
+
</script>
|
package/nodes/join.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
function BldgBlocksJoinNode(config) {
|
|
4
|
+
RED.nodes.createNode(this, config);
|
|
5
|
+
const node = this;
|
|
6
|
+
|
|
7
|
+
// Get configuration from the UI
|
|
8
|
+
node.targetCount = parseInt(config.count) || 4;
|
|
9
|
+
|
|
10
|
+
// Parse excluded keys string into a Set for fast lookup
|
|
11
|
+
// Split by comma, trim whitespace, and remove empty entries
|
|
12
|
+
const exclusionString = config.excludedKeys || "";
|
|
13
|
+
const excludedSet = new Set(
|
|
14
|
+
exclusionString.split(',').map(s => s.trim()).filter(s => s.length > 0)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// --- INPUT HANDLER ---
|
|
18
|
+
node.on('input', function(msg, send, done) {
|
|
19
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
20
|
+
|
|
21
|
+
// Get current state from context
|
|
22
|
+
let valueMap = node.context().get("valueMap") || {};
|
|
23
|
+
|
|
24
|
+
// Add properties from incoming message to the state
|
|
25
|
+
Object.keys(msg).forEach(key => {
|
|
26
|
+
// Logic:
|
|
27
|
+
// 1. Value must exist (not undefined/null)
|
|
28
|
+
// 2. Key must NOT start with '_' (internal Node-RED props)
|
|
29
|
+
// 3. Key must NOT be in the user-defined excluded list
|
|
30
|
+
if (
|
|
31
|
+
msg[key] !== undefined &&
|
|
32
|
+
!key.startsWith('_') &&
|
|
33
|
+
!excludedSet.has(key)
|
|
34
|
+
) {
|
|
35
|
+
valueMap[key] = msg[key];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Calculate current unique key count
|
|
40
|
+
const currentCount = Object.keys(valueMap).length;
|
|
41
|
+
|
|
42
|
+
// Update status
|
|
43
|
+
if (currentCount >= node.targetCount) {
|
|
44
|
+
utils.setStatusOK(node, `${currentCount}/${node.targetCount} keys`);
|
|
45
|
+
} else {
|
|
46
|
+
utils.setStatusChanged(node, `${currentCount}/${node.targetCount} keys`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Save state back to context
|
|
50
|
+
node.context().set("valueMap", valueMap);
|
|
51
|
+
|
|
52
|
+
// Check if we hit the target
|
|
53
|
+
if (currentCount >= node.targetCount) {
|
|
54
|
+
// Clone the map to create the output message
|
|
55
|
+
const outputMsg = RED.util.cloneMessage(valueMap);
|
|
56
|
+
|
|
57
|
+
// Ensure we have a msgid
|
|
58
|
+
if (!outputMsg._msgid) {
|
|
59
|
+
outputMsg._msgid = RED.util.generateId();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
send(outputMsg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (done) {
|
|
66
|
+
done();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
node.on('close', function(removed, done) {
|
|
71
|
+
if (removed) {
|
|
72
|
+
node.context().set("valueMap", undefined);
|
|
73
|
+
}
|
|
74
|
+
done();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
RED.nodes.registerType("bldgblocks-join", BldgBlocksJoinNode);
|
|
78
|
+
}
|