@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
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
<label for="node-input-seriesName" title="Specify the measurement series name"><i class="fa fa-list"></i> Series</label>
|
|
12
12
|
<input type="text" id="node-input-seriesName">
|
|
13
13
|
</div>
|
|
14
|
+
<div class="form-row">
|
|
15
|
+
<label for="node-input-inputProperty" title="Source for value extraction (msg property or jsonata expression)"><i class="fa fa-folder-open"></i> Input Value</label>
|
|
16
|
+
<input type="text" id="node-input-inputProperty" placeholder="payload">
|
|
17
|
+
<input type="hidden" id="node-input-inputPropertyType">
|
|
18
|
+
</div>
|
|
14
19
|
<div class="form-row">
|
|
15
20
|
<label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
|
|
16
21
|
<input type="hidden" id="node-input-tags">
|
|
@@ -49,12 +54,14 @@
|
|
|
49
54
|
defaults: {
|
|
50
55
|
historyConfig: { value: "", type: "history-config", required: true },
|
|
51
56
|
seriesName: { value: "", required: true },
|
|
57
|
+
inputProperty: { value: "payload" },
|
|
58
|
+
inputPropertyType: { value: "msg" },
|
|
52
59
|
tags: { value: "" },
|
|
53
60
|
storageType: { value: "batchObject" },
|
|
54
61
|
name: { value: "" }
|
|
55
62
|
},
|
|
56
63
|
inputs: 1,
|
|
57
|
-
outputs:
|
|
64
|
+
outputs: 0,
|
|
58
65
|
icon: "file.png",
|
|
59
66
|
paletteLabel: "history collector",
|
|
60
67
|
label: function() {
|
|
@@ -67,6 +74,17 @@
|
|
|
67
74
|
const tagContainer = $("#node-input-tag-container");
|
|
68
75
|
const hiddenTagInput = $("#node-input-tags");
|
|
69
76
|
|
|
77
|
+
// Initialize typed input for input property (msg or jsonata)
|
|
78
|
+
try {
|
|
79
|
+
$("#node-input-inputProperty").typedInput({
|
|
80
|
+
default: "msg",
|
|
81
|
+
types: ["msg", "jsonata"],
|
|
82
|
+
typeField: "#node-input-inputPropertyType"
|
|
83
|
+
}).typedInput("type", node.inputPropertyType || "msg").typedInput("value", node.inputProperty);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error("Error initializing inputProperty typedInput:", err);
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
// Initialize empty typedInputs first
|
|
71
89
|
seriesInput.typedInput({
|
|
72
90
|
types: [{ value: "series", options: [] }]
|
|
@@ -147,7 +165,10 @@
|
|
|
147
165
|
},
|
|
148
166
|
oneditsave: function() {
|
|
149
167
|
const seriesInput = $("#node-input-seriesName");
|
|
168
|
+
const inputProperty = $("#node-input-inputProperty");
|
|
150
169
|
this.seriesName = seriesInput.typedInput('value');
|
|
170
|
+
this.inputProperty = inputProperty.typedInput('value');
|
|
171
|
+
this.inputPropertyType = inputProperty.typedInput('type');
|
|
151
172
|
this.storageType = $("#node-input-storageType").val();
|
|
152
173
|
// tags are already in the hidden input thanks to the checkbox change handler
|
|
153
174
|
}
|
|
@@ -275,6 +296,13 @@ The node supports five `storageType` options, each producing a unique output for
|
|
|
275
296
|
- Timestamp: Generated as nanoseconds (ms * 1e6) for InfluxDB v2 precision.
|
|
276
297
|
- Error Handling: Invalid configurations or payloads trigger warnings and status updates (red ring).
|
|
277
298
|
|
|
299
|
+
### Status
|
|
300
|
+
- Green (dot): Configuration update
|
|
301
|
+
- Blue (dot): Data collected successfully
|
|
302
|
+
- Blue (ring): No state change
|
|
303
|
+
- Red (ring): Configuration error or invalid payload
|
|
304
|
+
- Yellow (ring): Warning (validation issue)
|
|
305
|
+
|
|
278
306
|
### References
|
|
279
307
|
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
280
308
|
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control)
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require("./utils")(RED);
|
|
3
|
+
|
|
2
4
|
function HistoryCollectorNode(config) {
|
|
3
5
|
RED.nodes.createNode(this, config);
|
|
4
6
|
this.historyConfig = RED.nodes.getNode(config.historyConfig);
|
|
5
7
|
this.seriesName = config.seriesName;
|
|
6
8
|
this.storageType = config.storageType || 'memory';
|
|
7
9
|
this.tags = config.tags || '';
|
|
10
|
+
this.inputProperty = config.inputProperty || "payload";
|
|
11
|
+
this.inputPropertyType = config.inputPropertyType || "msg";
|
|
8
12
|
const node = this;
|
|
9
13
|
|
|
10
14
|
// Parse tags into key-value object
|
|
@@ -24,33 +28,48 @@ module.exports = function(RED) {
|
|
|
24
28
|
return tags;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
node.on('input', function(msg) {
|
|
31
|
+
node.on('input', async function(msg, send, done) {
|
|
32
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
33
|
+
|
|
28
34
|
// Guard against invalid message
|
|
29
35
|
if (!msg) {
|
|
30
|
-
|
|
36
|
+
utils.setStatusError(node, "invalid message");
|
|
31
37
|
node.error('Invalid message received');
|
|
38
|
+
if (done) done();
|
|
32
39
|
return;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
// Validate configuration
|
|
36
43
|
if (!node.historyConfig) {
|
|
37
|
-
|
|
44
|
+
utils.setStatusError(node, "missing history config");
|
|
38
45
|
node.error('Missing history configuration', msg);
|
|
46
|
+
if (done) done();
|
|
39
47
|
return;
|
|
40
48
|
}
|
|
41
49
|
if (!node.seriesName) {
|
|
42
|
-
|
|
50
|
+
utils.setStatusError(node, "missing series name");
|
|
43
51
|
node.error('Missing series name', msg);
|
|
52
|
+
if (done) done();
|
|
44
53
|
return;
|
|
45
54
|
}
|
|
46
55
|
if (!node.historyConfig.name) {
|
|
47
|
-
|
|
56
|
+
utils.setStatusError(node, "missing bucket name");
|
|
48
57
|
node.error('Missing bucket name in history configuration', msg);
|
|
58
|
+
if (done) done();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Evaluate input property (msg or jsonata)
|
|
63
|
+
let payloadValue;
|
|
64
|
+
try {
|
|
65
|
+
payloadValue = await utils.evaluateNodeProperty(node.inputProperty, node.inputPropertyType, node, msg);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
utils.setStatusError(node, "input evaluation error");
|
|
68
|
+
if (done) done();
|
|
49
69
|
return;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
// Validate payload
|
|
53
|
-
let payloadValue = msg.payload;
|
|
54
73
|
let formattedValue;
|
|
55
74
|
if (typeof payloadValue === 'number') {
|
|
56
75
|
formattedValue = isNaN(payloadValue) ? null : payloadValue;
|
|
@@ -62,14 +81,14 @@ module.exports = function(RED) {
|
|
|
62
81
|
formattedValue = parseInt(payloadValue); // Handle InfluxDB integer format
|
|
63
82
|
}
|
|
64
83
|
} else {
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
utils.setStatusError(node, "invalid payload");
|
|
85
|
+
if (done) done();
|
|
67
86
|
return;
|
|
68
87
|
}
|
|
69
88
|
|
|
70
89
|
if (formattedValue === null) {
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
utils.setStatusError(node, "invalid payload");
|
|
91
|
+
if (done) done();
|
|
73
92
|
return;
|
|
74
93
|
}
|
|
75
94
|
|
|
@@ -85,7 +104,7 @@ module.exports = function(RED) {
|
|
|
85
104
|
const line = `${escapedMeasurementName}${tagsString ? ',' + tagsString : ''} value=${valueString} ${timestamp}`;
|
|
86
105
|
|
|
87
106
|
// Set initial status
|
|
88
|
-
|
|
107
|
+
utils.setStatusOK(node, "configuration received");
|
|
89
108
|
|
|
90
109
|
// Handle storage type
|
|
91
110
|
if (node.storageType === 'memory') {
|
|
@@ -101,12 +120,12 @@ module.exports = function(RED) {
|
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
node.context().global.set(contextKey, bucketData);
|
|
104
|
-
|
|
123
|
+
utils.setStatusChanged(node, `stored: ${valueString}`);
|
|
105
124
|
} else if (node.storageType === 'lineProtocol') {
|
|
106
125
|
msg.measurement = escapedMeasurementName;
|
|
107
126
|
msg.payload = line;
|
|
108
127
|
node.send(msg);
|
|
109
|
-
|
|
128
|
+
utils.setStatusChanged(node, `sent: ${valueString}`);
|
|
110
129
|
} else if (node.storageType === 'object') {
|
|
111
130
|
msg.measurement = escapedMeasurementName;
|
|
112
131
|
msg.payload = {
|
|
@@ -116,7 +135,7 @@ module.exports = function(RED) {
|
|
|
116
135
|
timestamp: timestamp
|
|
117
136
|
};
|
|
118
137
|
node.send(msg);
|
|
119
|
-
|
|
138
|
+
utils.setStatusChanged(node, `sent: ${valueString}`);
|
|
120
139
|
} else if (node.storageType === 'objectArray') {
|
|
121
140
|
msg.measurement = escapedMeasurementName;
|
|
122
141
|
msg.timestamp = timestamp;
|
|
@@ -127,7 +146,7 @@ module.exports = function(RED) {
|
|
|
127
146
|
tagsObj
|
|
128
147
|
]
|
|
129
148
|
node.send(msg);
|
|
130
|
-
|
|
149
|
+
utils.setStatusChanged(node, `sent: ${valueString}`);
|
|
131
150
|
} else if (node.storageType === 'batchObject') {
|
|
132
151
|
msg.payload = {
|
|
133
152
|
measurement: escapedMeasurementName,
|
|
@@ -138,8 +157,10 @@ module.exports = function(RED) {
|
|
|
138
157
|
tags: tagsObj
|
|
139
158
|
}
|
|
140
159
|
node.send(msg);
|
|
141
|
-
|
|
160
|
+
utils.setStatusChanged(node, `sent: ${valueString}`);
|
|
142
161
|
}
|
|
162
|
+
|
|
163
|
+
if (done) done();
|
|
143
164
|
});
|
|
144
165
|
|
|
145
166
|
node.on("close", function(done) {
|
|
@@ -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.
|