@bldgblocks/node-red-contrib-control 0.1.36 → 0.1.38

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.
@@ -8,7 +8,7 @@ module.exports = function(RED) {
8
8
  node.contextPropertyName = config.contextPropertyName || "in1";
9
9
  node.removeLabel = config.removeLabel || false;
10
10
 
11
- utils.setStatusOK(node, `mode: ${node.removeLabel ? "remove" : "set"}, property: ${node.contextPropertyName}`);
11
+ utils.setStatusOK(node, node.removeLabel ? "remove" : `set -> ${node.contextPropertyName}`);
12
12
 
13
13
  node.on("input", function(msg, send, done) {
14
14
  send = send || function() { node.send.apply(node, arguments); };
@@ -23,10 +23,10 @@ module.exports = function(RED) {
23
23
  // Set or remove context property
24
24
  if (node.removeLabel) {
25
25
  delete msg.context;
26
- utils.setStatusChanged(node, `in: ${msg.payload}, out: removed context`);
26
+ utils.setStatusChanged(node, `${msg.payload} -> removed`);
27
27
  } else {
28
28
  msg.context = node.contextPropertyName;
29
- utils.setStatusChanged(node, `in: ${msg.payload}, out: ${node.contextPropertyName}`);
29
+ utils.setStatusChanged(node, `${msg.payload} -> ${node.contextPropertyName}`);
30
30
  }
31
31
 
32
32
  send(msg);
@@ -5,14 +5,19 @@ module.exports = function(RED) {
5
5
  RED.nodes.createNode(this, config);
6
6
  const node = this;
7
7
 
8
+ // Unit multipliers (constant, computed once)
9
+ const delayOnMultiplier = config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1;
10
+ const delayOffMultiplier = config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1;
11
+
8
12
  // Initialize state
9
13
  node.name = config.name;
10
14
  node.state = false;
11
15
  node.desired = false;
12
- node.delayOn = parseFloat(config.delayOn) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
13
- node.delayOff = parseFloat(config.delayOff) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
16
+ node.delayOn = parseFloat(config.delayOn) * delayOnMultiplier;
17
+ node.delayOff = parseFloat(config.delayOff) * delayOffMultiplier;
14
18
 
15
19
  let timeoutId = null;
20
+ let pendingDone = null; // Track deferred done() for in-flight timers
16
21
  node.isBusy = false;
17
22
 
18
23
  node.on("input", async function(msg, send, done) {
@@ -46,21 +51,21 @@ module.exports = function(RED) {
46
51
  utils.requiresEvaluation(config.delayOnType)
47
52
  ? utils.evaluateNodeProperty(config.delayOn, config.delayOnType, node, msg)
48
53
  .then(val => parseFloat(val))
49
- : Promise.resolve(node.delayOn),
54
+ : Promise.resolve(parseFloat(config.delayOn)),
50
55
  );
51
56
 
52
57
  evaluations.push(
53
58
  utils.requiresEvaluation(config.delayOffType)
54
59
  ? utils.evaluateNodeProperty(config.delayOff, config.delayOffType, node, msg)
55
60
  .then(val => parseFloat(val))
56
- : Promise.resolve(node.delayOff),
61
+ : Promise.resolve(parseFloat(config.delayOff)),
57
62
  );
58
63
 
59
64
  const results = await Promise.all(evaluations);
60
65
 
61
- // Update runtime with evaluated values
62
- if (!isNaN(results[0])) node.delayOn = results[0] * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
63
- if (!isNaN(results[1])) node.delayOff = results[1] * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
66
+ // Update runtime with evaluated values (apply unit multiplier to raw config value)
67
+ if (!isNaN(results[0])) node.delayOn = results[0] * delayOnMultiplier;
68
+ if (!isNaN(results[1])) node.delayOff = results[1] * delayOffMultiplier;
64
69
  } catch (err) {
65
70
  node.error(`Error evaluating properties: ${err.message}`);
66
71
  if (done) done();
@@ -98,7 +103,13 @@ module.exports = function(RED) {
98
103
  clearTimeout(timeoutId);
99
104
  timeoutId = null;
100
105
  }
106
+ // Complete any deferred done from a cancelled timer
107
+ if (pendingDone) {
108
+ pendingDone();
109
+ pendingDone = null;
110
+ }
101
111
  node.state = false;
112
+ node.desired = false;
102
113
  utils.setStatusOK(node, "reset");
103
114
  }
104
115
  if (done) done();
@@ -160,44 +171,89 @@ module.exports = function(RED) {
160
171
 
161
172
  if (!node.state && inputValue === true) {
162
173
  if (node.desired) {
174
+ // Already awaiting true, ignore duplicate
163
175
  if (done) done();
164
176
  return;
165
177
  }
166
178
  if (timeoutId) {
167
179
  clearTimeout(timeoutId);
180
+ timeoutId = null;
181
+ }
182
+ // Complete any prior deferred done before starting new timer
183
+ if (pendingDone) {
184
+ pendingDone();
185
+ pendingDone = null;
168
186
  }
169
187
  utils.setStatusUnchanged(node, "awaiting true");
170
188
  node.desired = true;
189
+
190
+ // Clone msg for the timer callback so we don't hold the original
191
+ const delayedMsg = RED.util.cloneMessage(msg);
192
+ // Defer done — this message isn't complete until the timer fires or is cancelled
193
+ pendingDone = done;
194
+
171
195
  timeoutId = setTimeout(() => {
172
196
  node.state = true;
173
- msg.payload = true;
174
- delete msg.context;
197
+ delayedMsg.payload = true;
198
+ delete delayedMsg.context;
175
199
  utils.setStatusChanged(node, "in: true, out: true");
176
- send(msg);
200
+ send(delayedMsg);
177
201
  timeoutId = null;
202
+ if (pendingDone) {
203
+ pendingDone();
204
+ pendingDone = null;
205
+ }
178
206
  }, node.delayOn);
207
+
208
+ // Don't call done() here — it's deferred to the timer callback
209
+ return;
179
210
  } else if (node.state && inputValue === false) {
180
211
  if (node.desired === false) {
212
+ // Already awaiting false, ignore duplicate
181
213
  if (done) done();
182
214
  return;
183
215
  }
184
216
  if (timeoutId) {
185
217
  clearTimeout(timeoutId);
218
+ timeoutId = null;
219
+ }
220
+ // Complete any prior deferred done before starting new timer
221
+ if (pendingDone) {
222
+ pendingDone();
223
+ pendingDone = null;
186
224
  }
187
225
  utils.setStatusUnchanged(node, "awaiting false");
188
226
  node.desired = false;
227
+
228
+ // Clone msg for the timer callback so we don't hold the original
229
+ const delayedMsg = RED.util.cloneMessage(msg);
230
+ // Defer done — this message isn't complete until the timer fires or is cancelled
231
+ pendingDone = done;
232
+
189
233
  timeoutId = setTimeout(() => {
190
234
  node.state = false;
191
- msg.payload = false;
192
- delete msg.context;
235
+ delayedMsg.payload = false;
236
+ delete delayedMsg.context;
193
237
  utils.setStatusChanged(node, "in: false, out: false");
194
- send(msg);
238
+ send(delayedMsg);
195
239
  timeoutId = null;
240
+ if (pendingDone) {
241
+ pendingDone();
242
+ pendingDone = null;
243
+ }
196
244
  }, node.delayOff);
245
+
246
+ // Don't call done() here — it's deferred to the timer callback
247
+ return;
197
248
  } else {
198
249
  if (timeoutId) {
199
250
  clearTimeout(timeoutId);
200
251
  timeoutId = null;
252
+ // Complete deferred done from the cancelled timer's message
253
+ if (pendingDone) {
254
+ pendingDone();
255
+ pendingDone = null;
256
+ }
201
257
  utils.setStatusUnchanged(node, `canceled awaiting ${node.state}`);
202
258
  } else {
203
259
  utils.setStatusUnchanged(node, "no change");
@@ -217,6 +273,11 @@ module.exports = function(RED) {
217
273
  clearTimeout(timeoutId);
218
274
  timeoutId = null;
219
275
  }
276
+ // Complete any deferred done on shutdown
277
+ if (pendingDone) {
278
+ pendingDone();
279
+ pendingDone = null;
280
+ }
220
281
  done();
221
282
  });
222
283
  }
@@ -10,10 +10,11 @@ module.exports = function(RED) {
10
10
  node.detail = config.detail;
11
11
 
12
12
  let setterNode = null;
13
- let retryAction = null;
14
- let healthCheckAction = null;
13
+ let retryTimeout = null;
14
+ let healthCheckTimeout = null;
15
15
  let updateListener = null;
16
16
  let retryCount = 0;
17
+ let closed = false;
17
18
  const retryDelays = [0, 100, 500, 1000, 2000, 4000, 8000, 16000];
18
19
  const maxRetries = retryDelays.length - 1;
19
20
 
@@ -85,9 +86,9 @@ module.exports = function(RED) {
85
86
 
86
87
  RED.events.on("bldgblocks:global:value-changed", updateListener);
87
88
 
88
- if (retryAction) {
89
- clearInterval(retryAction);
90
- retryAction = null;
89
+ if (retryTimeout) {
90
+ clearTimeout(retryTimeout);
91
+ retryTimeout = null;
91
92
  }
92
93
 
93
94
  utils.setStatusOK(node, "Connected");
@@ -98,6 +99,11 @@ module.exports = function(RED) {
98
99
 
99
100
  function startHealthCheck() {
100
101
  const check = () => {
102
+ if (closed) return;
103
+ if (!updateListener) {
104
+ healthCheckTimeout = setTimeout(check, 30000);
105
+ return;
106
+ }
101
107
  const listeners = RED.events.listeners("bldgblocks:global:value-changed");
102
108
  const hasOurListener = listeners.includes(updateListener);
103
109
  if (!hasOurListener) {
@@ -106,13 +112,14 @@ module.exports = function(RED) {
106
112
  utils.setStatusOK(node, "Reconnected");
107
113
  }
108
114
  }
109
- setTimeout(check, 30000);
115
+ healthCheckTimeout = setTimeout(check, 30000);
110
116
  };
111
- setTimeout(check, 30000);
117
+ healthCheckTimeout = setTimeout(check, 30000);
112
118
  }
113
119
 
114
120
  function subscribeWithRetry() {
115
- retryAction = () => {
121
+ function attempt() {
122
+ if (closed) return;
116
123
  if (retryCount >= maxRetries) {
117
124
  utils.sendError(node, null, null, "Connection failed");
118
125
  return;
@@ -122,9 +129,9 @@ module.exports = function(RED) {
122
129
  return;
123
130
  }
124
131
  retryCount++;
125
- setTimeout(retryAction, retryDelays[Math.min(retryCount, maxRetries - 1)]);
126
- };
127
- setTimeout(retryAction, retryDelays[0]);
132
+ retryTimeout = setTimeout(attempt, retryDelays[Math.min(retryCount, maxRetries - 1)]);
133
+ }
134
+ retryTimeout = setTimeout(attempt, retryDelays[0]);
128
135
  }
129
136
 
130
137
  // --- INPUT HANDLER ---
@@ -160,10 +167,18 @@ module.exports = function(RED) {
160
167
  }
161
168
 
162
169
  node.on('close', function(removed, done) {
163
- if (healthCheckAction) clearInterval(healthCheckAction);
164
- if (retryAction) clearInterval(retryAction);
165
- if (removed && updateListener) {
170
+ closed = true;
171
+ if (healthCheckTimeout) {
172
+ clearTimeout(healthCheckTimeout);
173
+ healthCheckTimeout = null;
174
+ }
175
+ if (retryTimeout) {
176
+ clearTimeout(retryTimeout);
177
+ retryTimeout = null;
178
+ }
179
+ if (updateListener) {
166
180
  RED.events.removeListener("bldgblocks:global:value-changed", updateListener);
181
+ updateListener = null;
167
182
  }
168
183
  done();
169
184
  });
@@ -59,7 +59,8 @@ module.exports = function(RED) {
59
59
  // Send properly formed state object downstream after full initialization
60
60
  // Allows network-register and other downstream nodes to register on startup
61
61
  // Use setTimeout with delay to allow getter nodes time to establish their event listeners
62
- setTimeout(() => {
62
+ initTimer = setTimeout(() => {
63
+ initTimer = null;
63
64
  // Emit event so getter nodes with 'always' update mode receive initial value
64
65
  RED.events.emit("bldgblocks:global:value-changed", {
65
66
  key: node.varName,
@@ -120,7 +121,7 @@ module.exports = function(RED) {
120
121
  await utils.setGlobalState(node, node.varName, node.storeName, state);
121
122
 
122
123
  prefix = state.activePriority === 'default' ? '' : 'P';
123
- const statusText = `reload: ${prefix}${state.activePriority}:${state.value}${state.units}`;
124
+ const statusText = `reload: ${prefix}${state.activePriority}:${state.value}${state.units || ''}`;
124
125
 
125
126
  return utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
126
127
  }
@@ -158,8 +159,10 @@ module.exports = function(RED) {
158
159
 
159
160
  // Check for change
160
161
  if (value === state.value && priority === state.activePriority) {
162
+ // Ensure payload stays in sync with value
163
+ state.payload = state.value;
161
164
  prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
162
- const noChangeText = `no change: ${prefix}${node.writePriority}:${state.value}${state.units}`;
165
+ const noChangeText = `no change: ${prefix}${node.writePriority}:${state.value}${state.units || ''}`;
163
166
  utils.setStatusUnchanged(node, noChangeText);
164
167
  // Pass message through even if no context change
165
168
  send({ ...state });
@@ -199,7 +202,7 @@ module.exports = function(RED) {
199
202
 
200
203
  prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
201
204
  const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
202
- const statusText = `write: ${prefix}${node.writePriority}:${inputValue}${state.units} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units}`;
205
+ const statusText = `write: ${prefix}${node.writePriority}:${inputValue}${state.units || ''} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units || ''}`;
203
206
 
204
207
  RED.events.emit("bldgblocks:global:value-changed", {
205
208
  key: node.varName,
@@ -216,8 +219,8 @@ module.exports = function(RED) {
216
219
  });
217
220
 
218
221
  node.on('close', function(removed, done) {
222
+ if (initTimer) { clearTimeout(initTimer); initTimer = null; }
219
223
  if (removed && node.varName) {
220
- RED.events.removeAllListeners("bldgblocks:global:value-changed");
221
224
  // Callback style safe for close
222
225
  node.context().global.set(node.varName, undefined, node.storeName, function() {
223
226
  done();
@@ -20,9 +20,10 @@ module.exports = function(RED) {
20
20
  // Constants
21
21
  const TRENDS_DIR = getTrendsDirPath();
22
22
  // Use node ID in filename to prevent collisions between multiple history nodes
23
- const BUFFER_FILE = path.join(TRENDS_DIR, `buffer_${node.id}.json`);
24
- // Legacy file path for migration
23
+ const BUFFER_FILE = path.join(TRENDS_DIR, `buffer_${node.id}.jsonl`);
24
+ // Legacy file paths for migration
25
25
  const LEGACY_BUFFER_FILE = path.join(TRENDS_DIR, 'buffer_current.json');
26
+ const LEGACY_BUFFER_FILE_JSON = path.join(TRENDS_DIR, `buffer_${node.id}.json`);
26
27
 
27
28
  const COMMIT_INTERVAL_MS = 30 * 1000; // 30 seconds
28
29
  const PRUNE_INTERVAL_MS = 60 * 1000; // 60 seconds
@@ -33,7 +34,8 @@ module.exports = function(RED) {
33
34
  let liveBuffer = []; // Accumulate points since last commit to BUFFER_FILE
34
35
  let commitTimer = null;
35
36
  let pruneTimer = null;
36
- let messageCount = 0;
37
+ let messageCount = 0; // Total messages received
38
+ let activeBufferCount = 0; // Messages in current buffer file (resets on rotate)
37
39
  let cachedChunkCount = 0; // Cached count of historical files
38
40
  let isInitializing = true; // Flag: initialization in progress
39
41
  let queuedMessages = []; // Queue messages during initialization
@@ -121,6 +123,26 @@ module.exports = function(RED) {
121
123
  const now = new Date();
122
124
  const currentTimestamp = Math.floor(now.getTime() / 1000);
123
125
 
126
+ // Migrate old .json buffer to .jsonl if it exists
127
+ try {
128
+ await fs.promises.access(LEGACY_BUFFER_FILE_JSON);
129
+ // If the new .jsonl doesn't exist, rename; otherwise append and delete
130
+ try {
131
+ await fs.promises.access(BUFFER_FILE);
132
+ // Both exist: append old into new, then remove old
133
+ const oldData = await fs.promises.readFile(LEGACY_BUFFER_FILE_JSON);
134
+ if (oldData.length > 0) {
135
+ await fs.promises.appendFile(BUFFER_FILE, oldData);
136
+ }
137
+ await fs.promises.unlink(LEGACY_BUFFER_FILE_JSON);
138
+ } catch (e) {
139
+ // New .jsonl doesn't exist, just rename
140
+ await fs.promises.rename(LEGACY_BUFFER_FILE_JSON, BUFFER_FILE);
141
+ }
142
+ } catch (err) {
143
+ // No legacy .json buffer, nothing to migrate
144
+ }
145
+
124
146
  // Check for this node's specific buffer
125
147
  try {
126
148
  const stats = await fs.promises.stat(BUFFER_FILE);
@@ -135,7 +157,7 @@ module.exports = function(RED) {
135
157
 
136
158
  if (isStale) {
137
159
  const fileTs = Math.floor(stats.mtimeMs / 1000);
138
- const newName = path.join(TRENDS_DIR, `trend_${fileTs}_${node.id}.json`);
160
+ const newName = path.join(TRENDS_DIR, `trend_${fileTs}_${node.id}.jsonl`);
139
161
  await fs.promises.rename(BUFFER_FILE, newName);
140
162
  }
141
163
 
@@ -146,7 +168,7 @@ module.exports = function(RED) {
146
168
  // Check for legacy buffer (migration)
147
169
  try {
148
170
  await fs.promises.access(LEGACY_BUFFER_FILE);
149
- const legacyName = path.join(TRENDS_DIR, `trend_${currentTimestamp}_legacy.json`);
171
+ const legacyName = path.join(TRENDS_DIR, `trend_${currentTimestamp}_legacy.jsonl`);
150
172
  await fs.promises.rename(LEGACY_BUFFER_FILE, legacyName);
151
173
  } catch (err) {
152
174
  // Ignore
@@ -156,9 +178,9 @@ module.exports = function(RED) {
156
178
  async function getHistoricalFiles() {
157
179
  try {
158
180
  const files = await fs.promises.readdir(TRENDS_DIR);
159
- // Filter for our files: trend_TIMESTAMP_*.json
181
+ // Filter for our files: trend_TIMESTAMP_*.json or .jsonl
160
182
  return files
161
- .filter(f => f.startsWith('trend_') && f.endsWith('.json'))
183
+ .filter(f => f.startsWith('trend_') && (f.endsWith('.json') || f.endsWith('.jsonl')))
162
184
  .sort(); // String sort works for fixed-length timestamps usually, but numeric would be safer
163
185
  } catch (err) {
164
186
  return [];
@@ -249,20 +271,10 @@ module.exports = function(RED) {
249
271
  // Reset live buffer immediately so new messages go into next batch
250
272
  liveBuffer = [];
251
273
 
252
- const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n');
274
+ const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n') + '\n';
253
275
 
254
276
  try {
255
- // Only add prefix newline if file exists and has content (simplified check)
256
- // We'll let the next append handle its own newline or just ensure we join efficiently.
257
- // Actually, safest is to append WITH a preceding newline if file exists.
258
-
259
- let prefix = '';
260
- try {
261
- const stats = await fs.promises.stat(BUFFER_FILE);
262
- if (stats.size > 0) prefix = '\n';
263
- } catch(e) { /* File doesn't exist, no prefix needed */ }
264
-
265
- await fs.promises.appendFile(BUFFER_FILE, prefix + lines);
277
+ await fs.promises.appendFile(BUFFER_FILE, lines);
266
278
  } catch (err) {
267
279
  node.warn(`Buffer commit failed: ${err.message}`);
268
280
  // Put points back at the start of buffer if write failed
@@ -335,15 +347,9 @@ module.exports = function(RED) {
335
347
  if (liveBuffer.length > 0) {
336
348
  const pointsToCommit = liveBuffer;
337
349
  liveBuffer = []; // Clear memory
338
- const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n');
350
+ const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n') + '\n';
339
351
  try {
340
- let prefix = '';
341
- try {
342
- const stats = await fs.promises.stat(BUFFER_FILE);
343
- if (stats.size > 0) prefix = '\n';
344
- } catch(e) { /* File doesn't exist */ }
345
-
346
- await fs.promises.appendFile(BUFFER_FILE, prefix + lines);
352
+ await fs.promises.appendFile(BUFFER_FILE, lines);
347
353
  } catch (err) {
348
354
  // If append fails, we might lose these points during rotation,
349
355
  // put them back and abort rotation. Use concat for safety.
@@ -355,12 +361,13 @@ module.exports = function(RED) {
355
361
 
356
362
  // Perform Rotation (Rename)
357
363
  const timestamp = Math.floor(Date.now() / 1000);
358
- const newName = path.join(TRENDS_DIR, `trend_${timestamp}_${node.id}.json`);
364
+ const newName = path.join(TRENDS_DIR, `trend_${timestamp}_${node.id}.jsonl`);
359
365
 
360
366
  try {
361
367
  await fs.promises.access(BUFFER_FILE);
362
368
  await fs.promises.rename(BUFFER_FILE, newName);
363
369
  cachedChunkCount++;
370
+ activeBufferCount = 0; // Reset active buffer count after rotation
364
371
  } catch (err) {
365
372
  // BUFFER_FILE doesn't exist - no data to rotate
366
373
  }
@@ -421,9 +428,10 @@ module.exports = function(RED) {
421
428
  });
422
429
 
423
430
  messageCount++;
431
+ activeBufferCount++; // Increment count of messages in current buffer file
424
432
 
425
433
  // Status update (throttled internally to 1s)
426
- updateStatus(`${messageCount} msgs, ${cachedChunkCount} chunks, buf: ${liveBuffer.length}`, messageCount === 1);
434
+ updateStatus(`${messageCount} processed, ${activeBufferCount} active, ${cachedChunkCount} chunks`, messageCount === 1);
427
435
 
428
436
  if (done) done();
429
437
  });
@@ -106,6 +106,15 @@ module.exports = function(RED) {
106
106
  // Set initial status
107
107
  utils.setStatusOK(node, "configuration received");
108
108
 
109
+ // Emit event for history-service relay (InfluxDB batch format)
110
+ const eventName = `bldgblocks:history:${node.historyConfig.id}`;
111
+ RED.events.emit(eventName, {
112
+ measurement: escapedMeasurementName,
113
+ fields: { value: formattedValue },
114
+ tags: tagsObj,
115
+ timestamp: timestamp
116
+ });
117
+
109
118
  // Handle storage type
110
119
  if (node.storageType === 'memory') {
111
120
  const contextKey = `history_data_${node.historyConfig.name}`;
@@ -125,7 +134,7 @@ module.exports = function(RED) {
125
134
  msg.measurement = escapedMeasurementName;
126
135
  msg.payload = line;
127
136
  node.send(msg);
128
- utils.setStatusChanged(node, `sent: ${valueString}`);
137
+ utils.setStatusChanged(node, `stored: ${valueString}`);
129
138
  } else if (node.storageType === 'object') {
130
139
  msg.measurement = escapedMeasurementName;
131
140
  msg.payload = {
@@ -135,7 +144,7 @@ module.exports = function(RED) {
135
144
  timestamp: timestamp
136
145
  };
137
146
  node.send(msg);
138
- utils.setStatusChanged(node, `sent: ${valueString}`);
147
+ utils.setStatusChanged(node, `stored: ${valueString}`);
139
148
  } else if (node.storageType === 'objectArray') {
140
149
  msg.measurement = escapedMeasurementName;
141
150
  msg.timestamp = timestamp;
@@ -146,7 +155,7 @@ module.exports = function(RED) {
146
155
  tagsObj
147
156
  ]
148
157
  node.send(msg);
149
- utils.setStatusChanged(node, `sent: ${valueString}`);
158
+ utils.setStatusChanged(node, `stored: ${valueString}`);
150
159
  } else if (node.storageType === 'batchObject') {
151
160
  msg.payload = {
152
161
  measurement: escapedMeasurementName,
@@ -157,7 +166,7 @@ module.exports = function(RED) {
157
166
  tags: tagsObj
158
167
  }
159
168
  node.send(msg);
160
- utils.setStatusChanged(node, `sent: ${valueString}`);
169
+ utils.setStatusChanged(node, `stored: ${valueString}`);
161
170
  }
162
171
 
163
172
  if (done) done();
@@ -15,6 +15,18 @@ module.exports = function(RED) {
15
15
  // Generate matching event name based on history-config ID
16
16
  const eventName = `bldgblocks:history:${node.historyConfig.id}`;
17
17
 
18
+ // Status throttling - prevent rapid status updates from fast-streaming histories
19
+ let lastRelayedName = null;
20
+ let statusDirty = false;
21
+ let statusInterval = null;
22
+
23
+ statusInterval = setInterval(() => {
24
+ if (statusDirty && lastRelayedName) {
25
+ utils.setStatusChanged(node, `relayed: ${lastRelayedName}`);
26
+ statusDirty = false;
27
+ }
28
+ }, 2000);
29
+
18
30
  // Listen for events from history-collector nodes with this config
19
31
  const eventListener = (eventData) => {
20
32
  // Guard against invalid event data
@@ -24,17 +36,14 @@ module.exports = function(RED) {
24
36
  return;
25
37
  }
26
38
 
27
- // Create output message with the event data as payload
28
- // Preserve topic if it exists in the event data
39
+ // Send event data directly as payload (already in InfluxDB batch format)
29
40
  const msg = {
30
- payload: eventData,
31
- topic: eventData.topic
41
+ payload: eventData
32
42
  };
33
43
 
34
44
  node.send(msg);
35
-
36
- // Update status
37
- utils.setStatusChanged(node, `relayed: ${eventData.seriesName || 'data'}`);
45
+ lastRelayedName = eventData.measurement || 'data';
46
+ statusDirty = true;
38
47
  };
39
48
 
40
49
  // Subscribe to events
@@ -42,6 +51,11 @@ module.exports = function(RED) {
42
51
  utils.setStatusOK(node, `listening on ${node.historyConfig.name}`);
43
52
 
44
53
  node.on("close", function(done) {
54
+ // Clear status interval
55
+ if (statusInterval) {
56
+ clearInterval(statusInterval);
57
+ statusInterval = null;
58
+ }
45
59
  // Unsubscribe from events on close
46
60
  RED.events.off(eventName, eventListener);
47
61
  done();
@@ -147,6 +147,11 @@ module.exports = function(RED) {
147
147
 
148
148
  // Check for error response
149
149
  if (data.error) {
150
+ // During startup phase, suppress error display (network still coming online)
151
+ if (data.isStartupPhase) {
152
+ // Keep stale value, don't show error - just stay in waiting state
153
+ return;
154
+ }
150
155
  const errorText = `Read failed for point #${node.pointId}: ${data.errorMessage || "Unknown error"}`;
151
156
  utils.setStatusError(node, `Error: ${data.errorMessage || "Unknown error"}`);
152
157
  node.error(errorText); // Show in debug panel