@bldgblocks/node-red-contrib-control 0.1.32 → 0.1.33
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 +1 -1
- package/nodes/add-block.html +1 -1
- package/nodes/analog-switch-block.html +1 -1
- package/nodes/and-block.html +1 -1
- package/nodes/average-block.html +1 -1
- package/nodes/boolean-switch-block.html +1 -1
- package/nodes/boolean-to-number-block.html +1 -1
- package/nodes/cache-block.html +1 -1
- package/nodes/call-status-block.html +1 -1
- package/nodes/changeover-block.html +1 -1
- package/nodes/comment-block.html +1 -1
- package/nodes/compare-block.html +1 -1
- package/nodes/contextual-label-block.html +1 -1
- package/nodes/convert-block.html +1 -1
- package/nodes/count-block.html +2 -2
- package/nodes/delay-block.html +1 -1
- package/nodes/divide-block.html +1 -1
- package/nodes/edge-block.html +1 -1
- package/nodes/enum-switch-block.html +1 -1
- package/nodes/frequency-block.html +1 -1
- package/nodes/global-getter.html +34 -3
- package/nodes/global-getter.js +71 -45
- package/nodes/global-setter.html +21 -10
- package/nodes/global-setter.js +154 -79
- package/nodes/history-collector.html +283 -0
- package/nodes/history-collector.js +150 -0
- package/nodes/history-config.html +236 -0
- package/nodes/history-config.js +8 -0
- package/nodes/hysteresis-block.html +1 -1
- package/nodes/interpolate-block.html +1 -1
- package/nodes/latch-block.html +1 -1
- package/nodes/load-sequence-block.html +1 -1
- package/nodes/max-block.html +1 -1
- package/nodes/memory-block.html +1 -1
- package/nodes/min-block.html +1 -1
- package/nodes/minmax-block.html +1 -1
- package/nodes/modulo-block.html +1 -1
- package/nodes/multiply-block.html +1 -1
- package/nodes/negate-block.html +1 -1
- package/nodes/network-point-registry.html +86 -0
- package/nodes/network-point-registry.js +90 -0
- package/nodes/network-read.html +56 -0
- package/nodes/network-read.js +59 -0
- package/nodes/network-register.html +110 -0
- package/nodes/network-register.js +161 -0
- package/nodes/network-write.html +64 -0
- package/nodes/network-write.js +126 -0
- package/nodes/nullify-block.html +1 -1
- package/nodes/on-change-block.html +1 -1
- package/nodes/oneshot-block.html +1 -1
- package/nodes/or-block.html +1 -1
- package/nodes/pid-block.html +1 -1
- package/nodes/priority-block.html +1 -1
- package/nodes/rate-limit-block.html +2 -2
- package/nodes/rate-of-change-block.html +1 -1
- package/nodes/rate-of-change-block.js +5 -2
- package/nodes/round-block.html +6 -5
- package/nodes/round-block.js +5 -3
- package/nodes/saw-tooth-wave-block.html +2 -2
- package/nodes/scale-range-block.html +1 -1
- package/nodes/sine-wave-block.html +2 -2
- package/nodes/string-builder-block.html +1 -1
- package/nodes/subtract-block.html +1 -1
- package/nodes/thermistor-block.html +1 -1
- package/nodes/tick-tock-block.html +2 -2
- package/nodes/time-sequence-block.html +1 -1
- package/nodes/triangle-wave-block.html +2 -2
- package/nodes/tstat-block.html +1 -1
- package/nodes/units-block.html +8 -38
- package/nodes/units-block.js +3 -42
- package/package.json +11 -4
package/nodes/global-setter.js
CHANGED
|
@@ -12,6 +12,7 @@ module.exports = function(RED) {
|
|
|
12
12
|
node.inputProperty = config.property;
|
|
13
13
|
node.defaultValue = config.defaultValue;
|
|
14
14
|
node.writePriority = config.writePriority;
|
|
15
|
+
node.type = config.defaultValueType;
|
|
15
16
|
|
|
16
17
|
// Cast default value logic
|
|
17
18
|
if(!isNaN(node.defaultValue) && node.defaultValue !== "") node.defaultValue = Number(node.defaultValue);
|
|
@@ -22,16 +23,71 @@ module.exports = function(RED) {
|
|
|
22
23
|
function calculateWinner(state) {
|
|
23
24
|
for (let i = 1; i <= 16; i++) {
|
|
24
25
|
if (state.priority[i] !== undefined && state.priority[i] !== null) {
|
|
25
|
-
return state.priority[i];
|
|
26
|
+
return { value: state.priority[i], priority: `${i}` };
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
return state.defaultValue;
|
|
29
|
+
return { value: state.defaultValue, priority: "default" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getState() {
|
|
33
|
+
if (!node.varName) {
|
|
34
|
+
node.status({ fill: "red", shape: "ring", text: "no variable defined" });
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let state = node.context().global.get(node.varName, node.storeName);
|
|
39
|
+
if (!state || typeof state !== 'object' || !state.priority) {
|
|
40
|
+
state = {
|
|
41
|
+
payload: node.defaultValue,
|
|
42
|
+
value: node.defaultValue,
|
|
43
|
+
defaultValue: node.defaultValue,
|
|
44
|
+
activePriority: "default",
|
|
45
|
+
units: null,
|
|
46
|
+
priority: {
|
|
47
|
+
1: null,
|
|
48
|
+
2: null,
|
|
49
|
+
3: null,
|
|
50
|
+
4: null,
|
|
51
|
+
5: null,
|
|
52
|
+
6: null,
|
|
53
|
+
7: null,
|
|
54
|
+
8: null,
|
|
55
|
+
9: null,
|
|
56
|
+
10: null,
|
|
57
|
+
11: null,
|
|
58
|
+
12: null,
|
|
59
|
+
13: null,
|
|
60
|
+
14: null,
|
|
61
|
+
15: null,
|
|
62
|
+
16: null,
|
|
63
|
+
},
|
|
64
|
+
metadata: {
|
|
65
|
+
sourceId: node.id,
|
|
66
|
+
lastSet: new Date().toISOString(),
|
|
67
|
+
name: node.name || config.path,
|
|
68
|
+
path: node.varName,
|
|
69
|
+
store: node.storeName || 'default',
|
|
70
|
+
type: typeof(node.defaultValue)
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return { state: state, existing: false };
|
|
74
|
+
}
|
|
75
|
+
return { state: state, existing: true };
|
|
29
76
|
}
|
|
30
77
|
|
|
31
78
|
node.isBusy = false;
|
|
32
79
|
|
|
80
|
+
const st = getState();
|
|
81
|
+
if (st !== null && st.existing === false) {
|
|
82
|
+
// Initialize global variable
|
|
83
|
+
node.context().global.set(node.varName, st.state, node.storeName);
|
|
84
|
+
node.status({ fill: "grey", shape: "dot", text: `initialized: default:${node.defaultValue}` });
|
|
85
|
+
}
|
|
86
|
+
|
|
33
87
|
node.on('input', async function(msg, send, done) {
|
|
34
88
|
send = send || function() { node.send.apply(node, arguments); };
|
|
89
|
+
let prefix = '';
|
|
90
|
+
let valPretty = '';
|
|
35
91
|
|
|
36
92
|
// Guard against invalid msg
|
|
37
93
|
if (!msg) {
|
|
@@ -76,98 +132,117 @@ module.exports = function(RED) {
|
|
|
76
132
|
node.isBusy = false;
|
|
77
133
|
}
|
|
78
134
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
10: null,
|
|
100
|
-
11: null,
|
|
101
|
-
12: null,
|
|
102
|
-
13: null,
|
|
103
|
-
14: null,
|
|
104
|
-
15: null,
|
|
105
|
-
16: null,
|
|
106
|
-
},
|
|
107
|
-
metadata: {}
|
|
108
|
-
};
|
|
135
|
+
// Get existing state or initialize new
|
|
136
|
+
let state = {};
|
|
137
|
+
state = getState().state;
|
|
138
|
+
|
|
139
|
+
if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
|
|
140
|
+
if (msg.context === "reload") {
|
|
141
|
+
// Fire Event
|
|
142
|
+
RED.events.emit("bldgblocks-global-update", {
|
|
143
|
+
key: node.varName,
|
|
144
|
+
store: node.storeName,
|
|
145
|
+
data: state
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Send flow
|
|
149
|
+
node.context().global.set(node.varName, state, node.storeName);
|
|
150
|
+
prefix = state.activePriority === 'default' ? '' : 'P';
|
|
151
|
+
node.status({ fill: "green", shape: "dot", text: `reload: ${prefix}${state.activePriority}:${state.value}${state.units}` });
|
|
152
|
+
node.send({ ...state });
|
|
153
|
+
if (done) done();
|
|
154
|
+
return;
|
|
109
155
|
}
|
|
156
|
+
}
|
|
110
157
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
158
|
+
const inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
159
|
+
try {
|
|
160
|
+
if (inputValue === undefined) {
|
|
161
|
+
node.status({ fill: "red", shape: "ring", text: `msg.${node.inputProperty} not found` });
|
|
162
|
+
if (done) done();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
node.status({ fill: "red", shape: "ring", text: `Error accessing msg.${node.inputProperty}` });
|
|
167
|
+
if (done) done();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// Update Default, can not be set null
|
|
173
|
+
if (node.writePriority === 'default') {
|
|
174
|
+
state.defaultValue = inputValue === null || inputValue === "null" ? node.defaultValue : inputValue;
|
|
175
|
+
} else {
|
|
176
|
+
const priority = parseInt(node.writePriority, 10);
|
|
177
|
+
if (isNaN(priority) || priority < 1 || priority > 16) {
|
|
178
|
+
node.status({ fill: "red", shape: "ring", text: `Invalid priority: ${node.writePriority}` });
|
|
179
|
+
if (done) done();
|
|
180
|
+
return;
|
|
125
181
|
}
|
|
126
182
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
state.defaultValue = node.defaultValue;
|
|
183
|
+
if (inputValue !== undefined) {
|
|
184
|
+
state.priority[node.writePriority] = inputValue;
|
|
130
185
|
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Ensure defaultValue always has a value
|
|
189
|
+
if (state.defaultValue === null || state.defaultValue === "null" || state.defaultValue === undefined) {
|
|
190
|
+
state.defaultValue = node.defaultValue;
|
|
191
|
+
}
|
|
131
192
|
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
// Calculate Winner
|
|
194
|
+
const { value, priority } = calculateWinner(state);
|
|
195
|
+
if (value === state.value && priority === state.activePriority) {
|
|
196
|
+
// No change, exit early
|
|
197
|
+
prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
|
|
198
|
+
node.status({ fill: "green", shape: "dot", text: `no change: ${prefix}${node.writePriority}:${state.value}${state.units}` });
|
|
199
|
+
if (done) done();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
state.payload = value;
|
|
203
|
+
state.value = value;
|
|
204
|
+
state.activePriority = priority;
|
|
205
|
+
|
|
206
|
+
// Update Metadata
|
|
207
|
+
state.metadata.sourceId = node.id;
|
|
208
|
+
state.metadata.lastSet = new Date().toISOString();
|
|
209
|
+
state.metadata.name = node.name || config.path;
|
|
210
|
+
state.metadata.path = node.varName;
|
|
211
|
+
state.metadata.store = node.storeName || 'default';
|
|
212
|
+
state.metadata.type = typeof(value) || node.type;
|
|
213
|
+
|
|
214
|
+
// Units logic
|
|
215
|
+
let capturedUnits = null;
|
|
216
|
+
if (msg.units !== undefined) {
|
|
217
|
+
capturedUnits = msg.units;
|
|
218
|
+
} else if (inputValue !== null && typeof inputValue === 'object' && inputValue.units) {
|
|
219
|
+
capturedUnits = inputValue.units;
|
|
220
|
+
}
|
|
134
221
|
|
|
135
|
-
|
|
136
|
-
state.metadata.sourceId = node.id;
|
|
137
|
-
state.metadata.lastSet = new Date().toISOString();
|
|
138
|
-
state.metadata.sourceName = node.name || config.path;
|
|
139
|
-
state.metadata.sourcePath = node.varName;
|
|
140
|
-
state.metadata.store = node.storeName;
|
|
222
|
+
state.units = capturedUnits;
|
|
141
223
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if(capturedUnits) state.units = capturedUnits;
|
|
224
|
+
// Save & Emit
|
|
225
|
+
node.context().global.set(node.varName, state, node.storeName);
|
|
226
|
+
prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
|
|
227
|
+
const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
|
|
228
|
+
node.status({ fill: "blue", shape: "dot", text: `write: ${prefix}${node.writePriority}:${inputValue}${state.units} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units}` });
|
|
148
229
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
RED.events.emit("bldgblocks-global-update", {
|
|
156
|
-
key: node.varName,
|
|
157
|
-
store: node.storeName,
|
|
158
|
-
data: state
|
|
159
|
-
});
|
|
160
|
-
}
|
|
230
|
+
// Fire Event
|
|
231
|
+
RED.events.emit("bldgblocks-global-update", {
|
|
232
|
+
key: node.varName,
|
|
233
|
+
store: node.storeName,
|
|
234
|
+
data: state
|
|
235
|
+
});
|
|
161
236
|
|
|
162
|
-
|
|
237
|
+
// Send copy
|
|
238
|
+
node.send({ ...state });
|
|
163
239
|
if (done) done();
|
|
164
240
|
});
|
|
165
241
|
|
|
166
242
|
node.on('close', function(removed, done) {
|
|
167
243
|
if (removed && node.varName) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
globalContext.set(node.varName, undefined, node.storeName);
|
|
244
|
+
RED.events.removeAllListeners("bldgblocks-global-update");
|
|
245
|
+
node.context().global.set(node.varName, undefined, node.storeName);
|
|
171
246
|
}
|
|
172
247
|
done();
|
|
173
248
|
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="history-collector">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
4
|
+
<input type="text" id="node-input-name">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-historyConfig" title="Select the history configuration node"><i class="fa fa-book"></i> History Config</label>
|
|
8
|
+
<input type="text" id="node-input-historyConfig">
|
|
9
|
+
</div>
|
|
10
|
+
<div class="form-row">
|
|
11
|
+
<label for="node-input-seriesName" title="Specify the measurement series name"><i class="fa fa-list"></i> Series</label>
|
|
12
|
+
<input type="text" id="node-input-seriesName">
|
|
13
|
+
</div>
|
|
14
|
+
<div class="form-row">
|
|
15
|
+
<label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
|
|
16
|
+
<input type="hidden" id="node-input-tags">
|
|
17
|
+
|
|
18
|
+
<!-- Visible Container for Checkboxes -->
|
|
19
|
+
<div id="node-input-tag-container-div" style="display: inline-block; width: 70%; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
|
|
20
|
+
<!-- Header for 'Select All' or filtering could go here -->
|
|
21
|
+
<div id="node-input-tag-container" style="height: 150px; overflow-y: scroll; padding: 5px; background: #00000000;">
|
|
22
|
+
<div style="color: #00000000; padding: 5px;">Select a History Config...</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="form-row">
|
|
27
|
+
<label for="node-input-storageType" title="Choose the storage output format"><i class="fa fa-database"></i> Storage Type</label>
|
|
28
|
+
<select id="node-input-storageType">
|
|
29
|
+
<option value="memory">Memory (Flow Context)</option>
|
|
30
|
+
<option value="lineProtocol">Line Protocol</option>
|
|
31
|
+
<option value="object">Object</option>
|
|
32
|
+
<option value="objectArray">ObjectArray (InfluxDB v2)</option>
|
|
33
|
+
<option value="batchObject">BatchObject (InfluxDB v2 Batch)</option>
|
|
34
|
+
</select>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="form-row node-changed-values" style="display: none;" id="node-changed-values">
|
|
38
|
+
<label><i class="fa fa-info-circle"></i> Changed Values</label>
|
|
39
|
+
<div style="margin-left: 10px;">
|
|
40
|
+
<p>Tags: <span id="node-changed-tags"></span></p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<script type="text/javascript">
|
|
46
|
+
RED.nodes.registerType('history-collector', {
|
|
47
|
+
category: 'bldgblocks history',
|
|
48
|
+
color: '#b9f2ff',
|
|
49
|
+
defaults: {
|
|
50
|
+
historyConfig: { value: "", type: "history-config", required: true },
|
|
51
|
+
seriesName: { value: "", required: true },
|
|
52
|
+
tags: { value: "" },
|
|
53
|
+
storageType: { value: "batchObject" },
|
|
54
|
+
name: { value: "" }
|
|
55
|
+
},
|
|
56
|
+
inputs: 1,
|
|
57
|
+
outputs: 1,
|
|
58
|
+
icon: "file.png",
|
|
59
|
+
paletteLabel: "history collector",
|
|
60
|
+
label: function() {
|
|
61
|
+
return this.name || this.seriesName || "history collector";
|
|
62
|
+
},
|
|
63
|
+
oneditprepare: function() {
|
|
64
|
+
const node = this;
|
|
65
|
+
const seriesInput = $("#node-input-seriesName");
|
|
66
|
+
const storageType = $("#node-input-storageType");
|
|
67
|
+
const tagContainer = $("#node-input-tag-container");
|
|
68
|
+
const hiddenTagInput = $("#node-input-tags");
|
|
69
|
+
|
|
70
|
+
// Initialize empty typedInputs first
|
|
71
|
+
seriesInput.typedInput({
|
|
72
|
+
types: [{ value: "series", options: [] }]
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function updateConfigOptions(configId) {
|
|
76
|
+
const configNode = RED.nodes.node(configId);
|
|
77
|
+
|
|
78
|
+
// 1. Update Series Dropdown
|
|
79
|
+
const seriesOptions = configNode && configNode.series ?
|
|
80
|
+
configNode.series.map(s => ({ value: s.seriesName, label: s.seriesName })) :
|
|
81
|
+
[];
|
|
82
|
+
|
|
83
|
+
seriesInput.typedInput('types', [{
|
|
84
|
+
value: "series",
|
|
85
|
+
options: seriesOptions
|
|
86
|
+
}]);
|
|
87
|
+
|
|
88
|
+
if (node.seriesName && seriesOptions.find(o => o.value === node.seriesName)) {
|
|
89
|
+
seriesInput.typedInput('value', node.seriesName);
|
|
90
|
+
} else {
|
|
91
|
+
seriesInput.typedInput('value', '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Update Tags Multi-Select List
|
|
95
|
+
tagContainer.empty();
|
|
96
|
+
|
|
97
|
+
if (!configNode || !configNode.tags || configNode.tags.length === 0) {
|
|
98
|
+
tagContainer.append('<div style="color: #999; padding: 5px;">No tags defined in config</div>');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Parse currently selected tags (CSV string -> Array)
|
|
103
|
+
const currentTags = (hiddenTagInput.val() || "").split(',').map(t => t.trim()).filter(t => t);
|
|
104
|
+
const currentTagSet = new Set(currentTags);
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < configNode.tags.length; i++) {
|
|
107
|
+
const t = configNode.tags[i];
|
|
108
|
+
|
|
109
|
+
const tagKey = "tag" + i;
|
|
110
|
+
const tagString = tagKey + (t.tagValue ? '=' + t.tagValue : '');
|
|
111
|
+
|
|
112
|
+
const isChecked = currentTagSet.has(tagString);
|
|
113
|
+
|
|
114
|
+
const row = $('<div style="padding: 2px 0;"></div>').appendTo(tagContainer);
|
|
115
|
+
const label = $('<label style="width: 100%; display: flex; align-items: center; margin: 0; cursor: pointer;"></label>').appendTo(row);
|
|
116
|
+
|
|
117
|
+
const checkbox = $('<input type="checkbox" style="width: auto; margin: 0 6px 0 0; vertical-align: middle;">')
|
|
118
|
+
.attr('value', tagString)
|
|
119
|
+
.prop('checked', isChecked)
|
|
120
|
+
.on('change', function() {
|
|
121
|
+
updateHiddenTagInput();
|
|
122
|
+
})
|
|
123
|
+
.appendTo(label);
|
|
124
|
+
|
|
125
|
+
$('<span>').text(tagString).appendTo(label);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Function to gather checked boxes and update hidden input
|
|
130
|
+
function updateHiddenTagInput() {
|
|
131
|
+
const selected = [];
|
|
132
|
+
tagContainer.find('input[type="checkbox"]:checked').each(function() {
|
|
133
|
+
selected.push($(this).val());
|
|
134
|
+
});
|
|
135
|
+
hiddenTagInput.val(selected.join(','));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
$("#node-input-historyConfig").on("change", function() {
|
|
139
|
+
updateConfigOptions($(this).val());
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (node.historyConfig) {
|
|
143
|
+
updateConfigOptions(node.historyConfig);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
storageType.val(node.storageType || 'memory');
|
|
147
|
+
},
|
|
148
|
+
oneditsave: function() {
|
|
149
|
+
const seriesInput = $("#node-input-seriesName");
|
|
150
|
+
this.seriesName = seriesInput.typedInput('value');
|
|
151
|
+
this.storageType = $("#node-input-storageType").val();
|
|
152
|
+
// tags are already in the hidden input thanks to the checkbox change handler
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<script type="text/markdown" data-help-name="history-collector">
|
|
158
|
+
Receives data points to be tagged and bundled for storage in various formats, preparing them for a history collector or custom storage solution.
|
|
159
|
+
|
|
160
|
+
### Inputs
|
|
161
|
+
- payload (any): The data point to store (number, boolean, or string). Numbers are validated to exclude `NaN`, strings ending in `i` are parsed as integers (e.g., `123i` → `123`), and invalid values are rejected with a warning.
|
|
162
|
+
|
|
163
|
+
### Outputs
|
|
164
|
+
- payload (string | object | array): The formatted data package, depending on the selected `storageType`. See **Data Package Formats** for details.
|
|
165
|
+
- measurement (string): The measurement name (e.g., `Return_Temp`), set for `lineProtocol`, `object`, and `objectArray` formats.
|
|
166
|
+
- timestamp (number): Nanosecond timestamp, set for `objectArray` format.
|
|
167
|
+
|
|
168
|
+
### Details
|
|
169
|
+
The `history-collector` node is the entry point for collecting and formatting time-series data before storage. It validates the input payload, applies tags, and formats the output based on the configured `storageType`. The node supports integration with InfluxDB v2 (via `node-red-contrib-influxdb`) and other storage systems.
|
|
170
|
+
|
|
171
|
+
### Configuration
|
|
172
|
+
- History Config: Links to a `history-config` node defining the bucket or storage context.
|
|
173
|
+
- Series: The measurement name (e.g., `Return_Temp`) for the data series.
|
|
174
|
+
- Tags: Comma-separated key-value pairs (e.g., `tag0=physical,location=attic`) or single values (e.g., `physical,attic`). Automatically includes `historyGroup` with the `history-config` name.
|
|
175
|
+
- Storage Type: Determines the output format (see below).
|
|
176
|
+
|
|
177
|
+
### Data Package Formats
|
|
178
|
+
The node supports five `storageType` options, each producing a unique output format for `msg.payload` and related fields:
|
|
179
|
+
|
|
180
|
+
1. **Memory (Flow Context)**:
|
|
181
|
+
- Description: Stores data in Node-RED’s global context (`history_data_${historyConfig.name}`) as an array of InfluxDB line protocol strings. No message is sent to the output.
|
|
182
|
+
- Structure: Line protocol strings, e.g., `Return_Temp,historyGroup=ConfigName,tag0=physical value=74.096 1749782400000000000`.
|
|
183
|
+
- Fields: None (no output `msg`).
|
|
184
|
+
- Use Case: Temporary in-memory storage for later processing by a `history-collector` or custom logic.
|
|
185
|
+
- InfluxDB v2 Compatibility: Requires conversion to line protocol or batch format for writing.
|
|
186
|
+
- Example: Data is appended to a global array.
|
|
187
|
+
|
|
188
|
+
2. **Line Protocol**:
|
|
189
|
+
- Description: Outputs an InfluxDB line protocol string, suitable for direct writing to InfluxDB v1.x or v2.
|
|
190
|
+
- Structure:
|
|
191
|
+
```javascript
|
|
192
|
+
msg = {
|
|
193
|
+
measurement: "Return_Temp",
|
|
194
|
+
payload: "Return_Temp,historyGroup=ConfigName,tag0=physical value=74.096 1749782400000000000"
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
- Fields:
|
|
198
|
+
- `msg.measurement`: Escaped measurement name.
|
|
199
|
+
- `msg.payload`: Line protocol string with measurement, tags, `value`, and nanosecond timestamp.
|
|
200
|
+
- Use Case: Direct write to InfluxDB using `influxdb out` node in line protocol mode.
|
|
201
|
+
- InfluxDB v2 Compatibility: Works with `influxdb out` (v1.x mode), but less efficient for v2 batch writes.
|
|
202
|
+
- Example: `msg.payload` is ready for an `influxdb out` node to write a single point.
|
|
203
|
+
|
|
204
|
+
3. **Object**:
|
|
205
|
+
- Description: Outputs a structured object with measurement, tags, value, and timestamp, ideal for custom processing.
|
|
206
|
+
- Structure:
|
|
207
|
+
```javascript
|
|
208
|
+
msg = {
|
|
209
|
+
measurement: "Return_Temp",
|
|
210
|
+
payload: {
|
|
211
|
+
measurement: "Return_Temp",
|
|
212
|
+
tags: ["historyGroup=ConfigName", "tag0=physical"],
|
|
213
|
+
value: 74.096,
|
|
214
|
+
timestamp: 1749782400000000000
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
- Fields:
|
|
219
|
+
- `msg.measurement`: Escaped measurement name.
|
|
220
|
+
- `msg.payload.measurement`: Same as `msg.measurement`.
|
|
221
|
+
- `msg.payload.tags`: Array of `key=value` strings.
|
|
222
|
+
- `msg.payload.value`: Number, boolean, or string.
|
|
223
|
+
- `msg.payload.timestamp`: Nanosecond timestamp.
|
|
224
|
+
- Use Case: Custom storage or processing (e.g., JSON-based databases, MQTT).
|
|
225
|
+
- InfluxDB v2 Compatibility: Requires transformation to line protocol or batch format.
|
|
226
|
+
- Example: Useful for logging or non-InfluxDB storage systems.
|
|
227
|
+
|
|
228
|
+
4. **ObjectArray (InfluxDB v2)**:
|
|
229
|
+
- Description: Outputs an array with a value object and tags object, designed for InfluxDB v2 batch processing after transformation.
|
|
230
|
+
- Structure:
|
|
231
|
+
```javascript
|
|
232
|
+
msg = {
|
|
233
|
+
measurement: "Return_Temp",
|
|
234
|
+
timestamp: 1749782400000000000,
|
|
235
|
+
payload: [
|
|
236
|
+
{ value: 74.096 },
|
|
237
|
+
{ historyGroup: "ConfigName", tag0: "physical" }
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
- Fields:
|
|
242
|
+
- `msg.measurement`: Escaped measurement name.
|
|
243
|
+
- `msg.timestamp`: Nanosecond timestamp.
|
|
244
|
+
- `msg.payload[0]`: Object with `value` (number, boolean, or string).
|
|
245
|
+
- `msg.payload[1]`: Object with tag key-value pairs.
|
|
246
|
+
- Use Case: Prepares data for InfluxDB v2 batch writes via `influxdb batch` node after a function node converts to `[{ measurement, fields, tags, timestamp }]`.
|
|
247
|
+
- InfluxDB v2 Compatibility: Common in your flow, needs transformation for `influxdb batch`.
|
|
248
|
+
- Example: Matches your flow’s input format for batching (e.g., `Return_Temp`).
|
|
249
|
+
|
|
250
|
+
5. **BatchObject (InfluxDB v2 Batch)**:
|
|
251
|
+
- Description: Outputs an object formatted for direct use with `node-red-contrib-influxdb`’s `influxdb batch` node for InfluxDB v2.
|
|
252
|
+
- Structure:
|
|
253
|
+
```javascript
|
|
254
|
+
msg = {
|
|
255
|
+
payload: {
|
|
256
|
+
measurement: "Return_Temp",
|
|
257
|
+
timestamp: 1749782400000000000,
|
|
258
|
+
fields: { value: 74.096 },
|
|
259
|
+
tags: { historyGroup: "ConfigName", tag0: "physical" }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
- Fields:
|
|
264
|
+
- `msg.payload.measurement`: Escaped measurement name.
|
|
265
|
+
- `msg.payload.timestamp`: Nanosecond timestamp.
|
|
266
|
+
- `msg.payload.fields`: Object with `value` (number, boolean, or string).
|
|
267
|
+
- `msg.payload.tags`: Object with tag key-value pairs.
|
|
268
|
+
- Use Case: Direct input to `influxdb batch` node for efficient batch writes to InfluxDB v2.
|
|
269
|
+
- InfluxDB v2 Compatibility: Ideal for your setup, matches `influxdb batch` requirements.
|
|
270
|
+
- Example: Streamlines batch writes for measurements like `Stats_Compressor_PPH`.
|
|
271
|
+
|
|
272
|
+
### Notes
|
|
273
|
+
- Validation: The node validates `payload` to ensure it’s a number (excluding `NaN`), boolean, or string. Strings ending in `i` are parsed as integers.
|
|
274
|
+
- Tags: Tags are parsed from the `Tags` field as key-value pairs (e.g., `key=value`) or indexed tags (e.g., `tag0=physical`). "historyGroup":<node.historyConfig.name> is always included in tags. Special characters are escaped.
|
|
275
|
+
- Timestamp: Generated as nanoseconds (ms * 1e6) for InfluxDB v2 precision.
|
|
276
|
+
- Error Handling: Invalid configurations or payloads trigger warnings and status updates (red ring).
|
|
277
|
+
|
|
278
|
+
### References
|
|
279
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
280
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control)
|
|
281
|
+
- [InfluxDB Line Protocol](https://docs.influxdata.com/influxdb/v2.0/reference/syntax/line-protocol/)
|
|
282
|
+
- [node-red-contrib-influxdb](https://flows.nodered.org/node/node-red-contrib-influxdb)
|
|
283
|
+
</script>
|