@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.
Files changed (113) hide show
  1. package/nodes/accumulate-block.html +18 -8
  2. package/nodes/accumulate-block.js +39 -44
  3. package/nodes/add-block.html +1 -1
  4. package/nodes/add-block.js +18 -11
  5. package/nodes/alarm-collector.html +260 -0
  6. package/nodes/alarm-collector.js +292 -0
  7. package/nodes/alarm-config.html +129 -0
  8. package/nodes/alarm-config.js +126 -0
  9. package/nodes/alarm-service.html +96 -0
  10. package/nodes/alarm-service.js +142 -0
  11. package/nodes/analog-switch-block.js +25 -36
  12. package/nodes/and-block.js +44 -15
  13. package/nodes/average-block.js +46 -41
  14. package/nodes/boolean-switch-block.js +10 -28
  15. package/nodes/boolean-to-number-block.html +18 -5
  16. package/nodes/boolean-to-number-block.js +24 -16
  17. package/nodes/cache-block.js +24 -37
  18. package/nodes/call-status-block.html +91 -32
  19. package/nodes/call-status-block.js +398 -115
  20. package/nodes/changeover-block.html +5 -0
  21. package/nodes/changeover-block.js +167 -162
  22. package/nodes/comment-block.html +1 -1
  23. package/nodes/comment-block.js +14 -9
  24. package/nodes/compare-block.html +14 -4
  25. package/nodes/compare-block.js +23 -18
  26. package/nodes/contextual-label-block.html +5 -0
  27. package/nodes/contextual-label-block.js +6 -16
  28. package/nodes/convert-block.html +25 -39
  29. package/nodes/convert-block.js +31 -16
  30. package/nodes/count-block.html +11 -5
  31. package/nodes/count-block.js +34 -32
  32. package/nodes/delay-block.js +58 -53
  33. package/nodes/divide-block.js +43 -45
  34. package/nodes/edge-block.html +17 -10
  35. package/nodes/edge-block.js +43 -41
  36. package/nodes/enum-switch-block.js +6 -6
  37. package/nodes/frequency-block.html +6 -1
  38. package/nodes/frequency-block.js +64 -74
  39. package/nodes/global-getter.html +51 -15
  40. package/nodes/global-getter.js +74 -67
  41. package/nodes/global-setter.html +1 -1
  42. package/nodes/global-setter.js +168 -188
  43. package/nodes/history-buffer.html +96 -0
  44. package/nodes/history-buffer.js +461 -0
  45. package/nodes/history-collector.html +29 -1
  46. package/nodes/history-collector.js +37 -16
  47. package/nodes/history-config.html +13 -1
  48. package/nodes/history-service.html +84 -0
  49. package/nodes/history-service.js +52 -0
  50. package/nodes/hysteresis-block.html +5 -0
  51. package/nodes/hysteresis-block.js +13 -16
  52. package/nodes/interpolate-block.html +20 -2
  53. package/nodes/interpolate-block.js +39 -50
  54. package/nodes/join.html +78 -0
  55. package/nodes/join.js +78 -0
  56. package/nodes/latch-block.js +12 -14
  57. package/nodes/load-sequence-block.js +102 -110
  58. package/nodes/max-block.js +26 -26
  59. package/nodes/memory-block.js +57 -58
  60. package/nodes/min-block.js +26 -25
  61. package/nodes/minmax-block.js +35 -34
  62. package/nodes/modulo-block.js +45 -43
  63. package/nodes/multiply-block.js +43 -41
  64. package/nodes/negate-block.html +17 -7
  65. package/nodes/negate-block.js +25 -19
  66. package/nodes/network-point-read.html +128 -0
  67. package/nodes/network-point-read.js +230 -0
  68. package/nodes/{network-register.html → network-point-register.html} +94 -7
  69. package/nodes/network-point-register.js +126 -0
  70. package/nodes/network-point-write.html +149 -0
  71. package/nodes/network-point-write.js +222 -0
  72. package/nodes/network-service-bridge.html +131 -0
  73. package/nodes/network-service-bridge.js +376 -0
  74. package/nodes/network-service-read.html +81 -0
  75. package/nodes/network-service-read.js +58 -0
  76. package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
  77. package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
  78. package/nodes/network-service-write.html +89 -0
  79. package/nodes/network-service-write.js +83 -0
  80. package/nodes/nullify-block.js +13 -15
  81. package/nodes/on-change-block.html +17 -9
  82. package/nodes/on-change-block.js +49 -46
  83. package/nodes/oneshot-block.html +13 -10
  84. package/nodes/oneshot-block.js +57 -75
  85. package/nodes/or-block.js +44 -15
  86. package/nodes/pid-block.html +54 -4
  87. package/nodes/pid-block.js +459 -248
  88. package/nodes/priority-block.js +24 -35
  89. package/nodes/rate-limit-block.js +70 -72
  90. package/nodes/rate-of-change-block.html +33 -14
  91. package/nodes/rate-of-change-block.js +74 -62
  92. package/nodes/round-block.html +14 -9
  93. package/nodes/round-block.js +32 -25
  94. package/nodes/saw-tooth-wave-block.js +49 -76
  95. package/nodes/scale-range-block.html +12 -6
  96. package/nodes/scale-range-block.js +46 -39
  97. package/nodes/sine-wave-block.js +49 -57
  98. package/nodes/string-builder-block.js +6 -6
  99. package/nodes/subtract-block.js +38 -34
  100. package/nodes/thermistor-block.js +44 -44
  101. package/nodes/tick-tock-block.js +32 -32
  102. package/nodes/time-sequence-block.js +30 -42
  103. package/nodes/triangle-wave-block.js +49 -69
  104. package/nodes/tstat-block.js +34 -44
  105. package/nodes/units-block.html +90 -69
  106. package/nodes/units-block.js +22 -30
  107. package/nodes/utils.js +275 -3
  108. package/package.json +14 -6
  109. package/nodes/network-read.html +0 -56
  110. package/nodes/network-read.js +0 -59
  111. package/nodes/network-register.js +0 -161
  112. package/nodes/network-write.html +0 -64
  113. package/nodes/network-write.js +0 -126
@@ -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
- // --- HELPER: Calculate Winner ---
23
- function calculateWinner(state) {
24
- for (let i = 1; i <= 16; i++) {
25
- if (state.priority[i] !== undefined && state.priority[i] !== null) {
26
- return { value: state.priority[i], priority: `${i}` };
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
- function getState() {
40
+ // --- ASYNC INITIALIZATION (IIFE) ---
41
+ // This runs in background immediately after deployment
42
+ (async function initialize() {
33
43
  if (!node.varName) {
34
- node.status({ fill: "red", shape: "ring", text: "no variable defined" });
35
- return null;
44
+ utils.setStatusError(node, "no variable defined");
45
+ return;
36
46
  }
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 };
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
- return { state: state, existing: true };
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
- // Check busy lock
84
+ // Basic Validation
85
+ if (!msg) return utils.sendError(node, msg, done, "invalid message");
86
+
103
87
  if (node.isBusy) {
104
- // Update status to let user know they are pushing too fast
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
- // Begin evaluations
114
- const evaluations = [];
115
-
116
- evaluations.push(
117
- utils.requiresEvaluation(config.writePriorityType)
118
- ? utils.evaluateNodeProperty( config.writePriority, config.writePriorityType, node, msg )
119
- : Promise.resolve(node.writePriority),
120
- );
121
-
122
- const results = await Promise.all(evaluations);
123
-
124
- // Update runtime with evaluated values
125
- node.writePriority = results[0];
126
- } catch (err) {
127
- node.error(`Error evaluating properties: ${err.message}`);
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
- // Get existing state or initialize new
136
- let state = {};
137
- state = getState().state;
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
- if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
117
+ // Handle Reload
140
118
  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
- });
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
- 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;
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
- const inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
159
- try {
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
- node.status({ fill: "red", shape: "ring", text: `msg.${node.inputProperty} not found` });
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
- // 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}` });
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
- if (inputValue !== undefined) {
184
- state.priority[node.writePriority] = inputValue;
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
- 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
- }
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
- state.units = capturedUnits;
223
-
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}` });
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-global-update");
245
- node.context().global.set(node.varName, undefined, node.storeName);
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>