@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
package/nodes/global-setter.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
1
|
module.exports = function(RED) {
|
|
3
2
|
const utils = require('./utils')(RED);
|
|
4
|
-
|
|
5
3
|
function GlobalSetterNode(config) {
|
|
6
4
|
RED.nodes.createNode(this, config);
|
|
7
5
|
const node = this;
|
|
@@ -13,238 +11,220 @@ module.exports = function(RED) {
|
|
|
13
11
|
node.defaultValue = config.defaultValue;
|
|
14
12
|
node.writePriority = config.writePriority;
|
|
15
13
|
node.type = config.defaultValueType;
|
|
14
|
+
node.isBusy = false;
|
|
16
15
|
|
|
17
|
-
// Cast default value logic
|
|
18
16
|
if(!isNaN(node.defaultValue) && node.defaultValue !== "") node.defaultValue = Number(node.defaultValue);
|
|
19
17
|
if(node.defaultValue === "true") node.defaultValue = true;
|
|
20
18
|
if(node.defaultValue === "false") node.defaultValue = false;
|
|
21
19
|
|
|
22
|
-
//
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
// Helper to generate the data structure
|
|
21
|
+
function buildDefaultState() {
|
|
22
|
+
return {
|
|
23
|
+
payload: node.defaultValue,
|
|
24
|
+
value: node.defaultValue,
|
|
25
|
+
defaultValue: node.defaultValue,
|
|
26
|
+
activePriority: "default",
|
|
27
|
+
units: null,
|
|
28
|
+
priority: { 1: null, 2: null, 3: null, 4: null, 5: null, 6: null, 7: null, 8: null, 9: null, 10: null, 11: null, 12: null, 13: null, 14: null, 15: null, 16: null },
|
|
29
|
+
metadata: {
|
|
30
|
+
sourceId: node.id,
|
|
31
|
+
lastSet: new Date().toISOString(),
|
|
32
|
+
name: node.name || config.path,
|
|
33
|
+
path: node.varName,
|
|
34
|
+
store: node.storeName || 'default',
|
|
35
|
+
type: typeof(node.defaultValue)
|
|
27
36
|
}
|
|
28
|
-
}
|
|
29
|
-
return { value: state.defaultValue, priority: "default" };
|
|
37
|
+
};
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
// --- ASYNC INITIALIZATION (IIFE) ---
|
|
41
|
+
// This runs in background immediately after deployment
|
|
42
|
+
(async function initialize() {
|
|
33
43
|
if (!node.varName) {
|
|
34
|
-
|
|
35
|
-
return
|
|
44
|
+
utils.setStatusError(node, "no variable defined");
|
|
45
|
+
return;
|
|
36
46
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
state
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 };
|
|
47
|
+
try {
|
|
48
|
+
// Check if data exists
|
|
49
|
+
let state = await utils.getGlobalState(node, node.varName, node.storeName);
|
|
50
|
+
if (!state || typeof state !== 'object' || !state.priority) {
|
|
51
|
+
// If not, set default
|
|
52
|
+
state = buildDefaultState();
|
|
53
|
+
await utils.setGlobalState(node, node.varName, node.storeName, state);
|
|
54
|
+
utils.setStatusOK(node, `initialized: default:${node.defaultValue}`);
|
|
55
|
+
} else {
|
|
56
|
+
utils.setStatusOK(node, `loaded: ${state.value}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Send properly formed state object downstream after full initialization
|
|
60
|
+
// Allows network-register and other downstream nodes to register on startup
|
|
61
|
+
// Use setTimeout with delay to allow getter nodes time to establish their event listeners
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
// Emit event so getter nodes with 'always' update mode receive initial value
|
|
64
|
+
RED.events.emit("bldgblocks:global:value-changed", {
|
|
65
|
+
key: node.varName,
|
|
66
|
+
store: node.storeName,
|
|
67
|
+
data: state
|
|
68
|
+
});
|
|
69
|
+
node.send(state);
|
|
70
|
+
}, 500);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Silently fail or log if init fails (DB down on boot?)
|
|
73
|
+
node.error(`Init Error: ${err.message}`);
|
|
74
|
+
utils.setStatusError(node, "Init Error");
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
node.isBusy = false;
|
|
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
|
-
}
|
|
76
|
+
})();
|
|
86
77
|
|
|
78
|
+
// --- INPUT HANDLER ---
|
|
87
79
|
node.on('input', async function(msg, send, done) {
|
|
88
80
|
send = send || function() { node.send.apply(node, arguments); };
|
|
89
81
|
let prefix = '';
|
|
90
|
-
let valPretty = '';
|
|
91
|
-
|
|
92
|
-
// Guard against invalid msg
|
|
93
|
-
if (!msg) {
|
|
94
|
-
node.status({ fill: "red", shape: "ring", text: "invalid message" });
|
|
95
|
-
if (done) done();
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
82
|
|
|
99
|
-
// Evaluate dynamic properties
|
|
100
83
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
84
|
+
// Basic Validation
|
|
85
|
+
if (!msg) return utils.sendError(node, msg, done, "invalid message");
|
|
86
|
+
|
|
103
87
|
if (node.isBusy) {
|
|
104
|
-
|
|
105
|
-
node.status({ fill: "yellow", shape: "ring", text: "busy - dropped msg" });
|
|
88
|
+
utils.setStatusBusy(node, "busy - dropped msg");
|
|
106
89
|
if (done) done();
|
|
107
90
|
return;
|
|
108
91
|
}
|
|
109
|
-
|
|
110
|
-
// Lock node during evaluation
|
|
111
92
|
node.isBusy = true;
|
|
112
93
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (done) done();
|
|
129
|
-
return;
|
|
130
|
-
} finally {
|
|
131
|
-
// Release, all synchronous from here on
|
|
132
|
-
node.isBusy = false;
|
|
133
|
-
}
|
|
94
|
+
// Evaluate Dynamic Properties (Exact same logic as before)
|
|
95
|
+
try {
|
|
96
|
+
const evaluations = [];
|
|
97
|
+
evaluations.push(
|
|
98
|
+
utils.requiresEvaluation(config.writePriorityType)
|
|
99
|
+
? utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg)
|
|
100
|
+
: Promise.resolve(node.writePriority)
|
|
101
|
+
);
|
|
102
|
+
const results = await Promise.all(evaluations);
|
|
103
|
+
node.writePriority = results[0];
|
|
104
|
+
} catch (err) {
|
|
105
|
+
throw new Error(`Property Eval Error: ${err.message}`);
|
|
106
|
+
} finally {
|
|
107
|
+
node.isBusy = false;
|
|
108
|
+
}
|
|
134
109
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
110
|
+
// Get State (Async)
|
|
111
|
+
let state = await utils.getGlobalState(node, node.varName, node.storeName);
|
|
112
|
+
if (!state || typeof state !== 'object' || !state.priority) {
|
|
113
|
+
// Fallback if data is missing (e.g., if message arrives before init finishes)
|
|
114
|
+
state = buildDefaultState();
|
|
115
|
+
}
|
|
138
116
|
|
|
139
|
-
|
|
117
|
+
// Handle Reload
|
|
140
118
|
if (msg.context === "reload") {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
key: node.varName,
|
|
144
|
-
store: node.storeName,
|
|
145
|
-
data: state
|
|
146
|
-
});
|
|
119
|
+
RED.events.emit("bldgblocks:global:value-changed", { key: node.varName, store: node.storeName, data: state });
|
|
120
|
+
await utils.setGlobalState(node, node.varName, node.storeName, state);
|
|
147
121
|
|
|
148
|
-
// Send flow
|
|
149
|
-
node.context().global.set(node.varName, state, node.storeName);
|
|
150
122
|
prefix = state.activePriority === 'default' ? '' : 'P';
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return;
|
|
123
|
+
const statusText = `reload: ${prefix}${state.activePriority}:${state.value}${state.units}`;
|
|
124
|
+
|
|
125
|
+
return utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
|
|
155
126
|
}
|
|
156
|
-
}
|
|
157
127
|
|
|
158
|
-
|
|
159
|
-
|
|
128
|
+
// Get Input Value
|
|
129
|
+
let inputValue;
|
|
130
|
+
try {
|
|
131
|
+
inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
inputValue = undefined;
|
|
134
|
+
}
|
|
160
135
|
if (inputValue === undefined) {
|
|
161
|
-
|
|
162
|
-
if (done) done();
|
|
163
|
-
return;
|
|
136
|
+
return utils.sendError(node, msg, done, `msg.${node.inputProperty} not found or invalid property path`);
|
|
164
137
|
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
node.status({ fill: "red", shape: "ring", text: `Error accessing msg.${node.inputProperty}` });
|
|
167
|
-
if (done) done();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
138
|
|
|
139
|
+
// Update State
|
|
140
|
+
if (node.writePriority === 'default') {
|
|
141
|
+
state.defaultValue = inputValue === null || inputValue === "null" ? node.defaultValue : inputValue;
|
|
142
|
+
} else {
|
|
143
|
+
const priority = parseInt(node.writePriority, 10);
|
|
144
|
+
if (isNaN(priority) || priority < 1 || priority > 16) {
|
|
145
|
+
return utils.sendError(node, msg, done, `Invalid priority: ${node.writePriority}`);
|
|
146
|
+
}
|
|
147
|
+
if (inputValue !== undefined) {
|
|
148
|
+
state.priority[node.writePriority] = inputValue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (state.defaultValue === null || state.defaultValue === "null" || state.defaultValue === undefined) {
|
|
153
|
+
state.defaultValue = node.defaultValue;
|
|
154
|
+
}
|
|
171
155
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
156
|
+
// Calculate Winner
|
|
157
|
+
const { value, priority } = utils.getHighestPriority(state);
|
|
158
|
+
|
|
159
|
+
// Check for change
|
|
160
|
+
if (value === state.value && priority === state.activePriority) {
|
|
161
|
+
prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
|
|
162
|
+
const noChangeText = `no change: ${prefix}${node.writePriority}:${state.value}${state.units}`;
|
|
163
|
+
utils.setStatusUnchanged(node, noChangeText);
|
|
164
|
+
// Pass message through even if no context change
|
|
165
|
+
send({ ...state });
|
|
179
166
|
if (done) done();
|
|
180
167
|
return;
|
|
181
168
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
169
|
+
|
|
170
|
+
// Update Values
|
|
171
|
+
state.payload = value;
|
|
172
|
+
state.value = value;
|
|
173
|
+
state.activePriority = priority;
|
|
174
|
+
|
|
175
|
+
state.metadata.sourceId = node.id;
|
|
176
|
+
state.metadata.lastSet = new Date().toISOString();
|
|
177
|
+
state.metadata.name = node.name || config.path;
|
|
178
|
+
state.metadata.path = node.varName;
|
|
179
|
+
state.metadata.store = node.storeName || 'default';
|
|
180
|
+
state.metadata.type = typeof(value) || node.type;
|
|
181
|
+
|
|
182
|
+
// Capture Units
|
|
183
|
+
let capturedUnits = null;
|
|
184
|
+
if (msg.units !== undefined) {
|
|
185
|
+
capturedUnits = msg.units;
|
|
186
|
+
} else if (inputValue !== null && typeof inputValue === 'object' && inputValue.units) {
|
|
187
|
+
capturedUnits = inputValue.units;
|
|
188
|
+
}
|
|
189
|
+
state.units = capturedUnits;
|
|
190
|
+
|
|
191
|
+
// Save (Async) and Emit
|
|
192
|
+
await utils.setGlobalState(node, node.varName, node.storeName, state);
|
|
193
|
+
// *** REQUIRE DEFAULT STORE ***
|
|
194
|
+
// Require default store to keep values in memory for polled getter nodes so they are not constantly re-reading from DB
|
|
195
|
+
// to avoid hammering edge devices with repeated reads. Writes are only on change. On event (reactive) sends the data in the event.
|
|
196
|
+
if (node.storeName !== 'default') {
|
|
197
|
+
await utils.setGlobalState(node, node.varName, 'default', state);
|
|
185
198
|
}
|
|
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
|
-
}
|
|
192
199
|
|
|
193
|
-
// Calculate Winner
|
|
194
|
-
const { value, priority } = calculateWinner(state);
|
|
195
|
-
if (value === state.value && priority === state.activePriority) {
|
|
196
|
-
// No change, exit early
|
|
197
200
|
prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
}
|
|
201
|
+
const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
|
|
202
|
+
const statusText = `write: ${prefix}${node.writePriority}:${inputValue}${state.units} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units}`;
|
|
203
|
+
|
|
204
|
+
RED.events.emit("bldgblocks:global:value-changed", {
|
|
205
|
+
key: node.varName,
|
|
206
|
+
store: node.storeName,
|
|
207
|
+
data: state
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
|
|
221
211
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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}` });
|
|
229
|
-
|
|
230
|
-
// Fire Event
|
|
231
|
-
RED.events.emit("bldgblocks-global-update", {
|
|
232
|
-
key: node.varName,
|
|
233
|
-
store: node.storeName,
|
|
234
|
-
data: state
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Send copy
|
|
238
|
-
node.send({ ...state });
|
|
239
|
-
if (done) done();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
node.error(err);
|
|
214
|
+
utils.sendError(node, msg, done, `Internal Error: ${err.message}`);
|
|
215
|
+
}
|
|
240
216
|
});
|
|
241
217
|
|
|
242
218
|
node.on('close', function(removed, done) {
|
|
243
219
|
if (removed && node.varName) {
|
|
244
|
-
RED.events.removeAllListeners("bldgblocks
|
|
245
|
-
|
|
220
|
+
RED.events.removeAllListeners("bldgblocks:global:value-changed");
|
|
221
|
+
// Callback style safe for close
|
|
222
|
+
node.context().global.set(node.varName, undefined, node.storeName, function() {
|
|
223
|
+
done();
|
|
224
|
+
});
|
|
225
|
+
} else {
|
|
226
|
+
done();
|
|
246
227
|
}
|
|
247
|
-
done();
|
|
248
228
|
});
|
|
249
229
|
}
|
|
250
230
|
RED.nodes.registerType("global-setter", GlobalSetterNode);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="history-buffer">
|
|
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="Trend History">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="form-row">
|
|
7
|
+
<label for="node-input-bufferHours" title="How many hours of history to retain (default 3)"><i class="fa fa-clock-o"></i> Buffer Hours</label>
|
|
8
|
+
<input type="number" id="node-input-bufferHours" placeholder="3" min="0.01" max="24" step="0.01">
|
|
9
|
+
</div>
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script type="text/javascript">
|
|
13
|
+
RED.nodes.registerType("history-buffer", {
|
|
14
|
+
category: "bldgblocks history",
|
|
15
|
+
color: "#b9f2ff",
|
|
16
|
+
defaults: {
|
|
17
|
+
name: { value: "" },
|
|
18
|
+
bufferHours: {
|
|
19
|
+
value: 3,
|
|
20
|
+
required: true,
|
|
21
|
+
validate: function(v) {
|
|
22
|
+
const val = parseFloat(v);
|
|
23
|
+
return !isNaN(val) && val >= 0.01 && val <= 24;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
inputs: 1,
|
|
28
|
+
outputs: 1,
|
|
29
|
+
inputLabels: ["data"],
|
|
30
|
+
outputLabels: ["chart"],
|
|
31
|
+
icon: "font-awesome/fa-history",
|
|
32
|
+
paletteLabel: "history buffer",
|
|
33
|
+
label: function() {
|
|
34
|
+
return this.name ? `${this.name} (${this.bufferHours}h)` : `history buffer (${this.bufferHours}h)`;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<script type="text/markdown" data-help-name="history-buffer">
|
|
40
|
+
Persistent trend history for FlowFuse Dashboard 2.0 `ui-chart` nodes.
|
|
41
|
+
|
|
42
|
+
Stores time-series data as hourly files on disk, surviving Node-RED restarts without blocking startup or using global context.
|
|
43
|
+
|
|
44
|
+
### Inputs
|
|
45
|
+
: topic (string) : Series identifier (e.g., "Return Temp").
|
|
46
|
+
: payload (number) : Data value for this series.
|
|
47
|
+
: ts (number) : Timestamp in milliseconds (optional, auto-filled).
|
|
48
|
+
|
|
49
|
+
### Outputs
|
|
50
|
+
: topic (string) : Series identifier.
|
|
51
|
+
: payload (number|array) : Single value (append) or array of points (replace).
|
|
52
|
+
: ts (number) : Timestamp in milliseconds.
|
|
53
|
+
: action (string) : `"replace"` on startup, `"append"` for live data.
|
|
54
|
+
|
|
55
|
+
### ui-chart Configuration (IMPORTANT)
|
|
56
|
+
|
|
57
|
+
The `ui-chart` node **must** be configured to read data from the correct keys.
|
|
58
|
+
|
|
59
|
+
In the chart's **Properties** section, set:
|
|
60
|
+
|
|
61
|
+
| Property | Type | Value |
|
|
62
|
+
|----------|------|-------|
|
|
63
|
+
| **Series** | `key` | `topic` |
|
|
64
|
+
| **X** | `key` | `ts` |
|
|
65
|
+
| **Y** | `key` | `payload` |
|
|
66
|
+
|
|
67
|
+
**Critical:** The type must be `key` (not `msg`). This tells the chart to look *inside* each array element for these fields.
|
|
68
|
+
|
|
69
|
+
Also set:
|
|
70
|
+
- **X-Axis Type**: `Timescale`
|
|
71
|
+
- **X-Axis Limit**: Match your buffer hours or leave default
|
|
72
|
+
|
|
73
|
+
### Storage
|
|
74
|
+
|
|
75
|
+
Data is stored as Line-Delimited JSON in `~/.node-red/.bldgblocks/trends/`:
|
|
76
|
+
- `buffer_<nodeid>.json` : Active buffer, committed every 30 seconds
|
|
77
|
+
- `trend_<timestamp>_<nodeid>.json` : Hourly rotation files
|
|
78
|
+
|
|
79
|
+
On startup:
|
|
80
|
+
1. Waits 5 seconds for Node-RED to stabilize
|
|
81
|
+
2. Loads all files within retention window
|
|
82
|
+
3. Sends historical data with `action: "replace"`
|
|
83
|
+
4. Begins accepting live data
|
|
84
|
+
|
|
85
|
+
Supports 0.01 to 24 hours retention. Old files pruned automatically.
|
|
86
|
+
|
|
87
|
+
### Status
|
|
88
|
+
- **Green** : Ready
|
|
89
|
+
- **Blue** : Active, receiving data
|
|
90
|
+
- **Yellow** : Warning (file issue)
|
|
91
|
+
- **Red** : Error
|
|
92
|
+
|
|
93
|
+
### References
|
|
94
|
+
- [ui-chart Documentation](https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html)
|
|
95
|
+
- [GitHub](https://github.com/BldgBlocks/node-red-contrib-control.git)
|
|
96
|
+
</script>
|