@bldgblocks/node-red-contrib-control 0.1.37 → 0.2.0
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/LICENSE.md +38 -5
- package/README.md +10 -0
- package/nodes/accumulate-block.html +3 -3
- package/nodes/alarm-collector.html +11 -0
- package/nodes/alarm-collector.js +31 -5
- package/nodes/alarm-config.html +11 -6
- package/nodes/alarm-config.js +34 -35
- package/nodes/alarm-service.js +2 -2
- package/nodes/and-block.js +1 -1
- package/nodes/boolean-switch-block.html +27 -14
- package/nodes/boolean-switch-block.js +22 -12
- package/nodes/call-status-block.html +83 -56
- package/nodes/call-status-block.js +335 -248
- package/nodes/changeover-block.html +30 -31
- package/nodes/changeover-block.js +287 -389
- package/nodes/contextual-label-block.js +3 -3
- package/nodes/delay-block.js +74 -13
- package/nodes/global-getter.html +173 -12
- package/nodes/global-getter.js +29 -14
- package/nodes/global-setter.html +37 -0
- package/nodes/global-setter.js +96 -14
- package/nodes/history-buffer.js +32 -27
- package/nodes/history-collector.html +3 -1
- package/nodes/history-collector.js +4 -4
- package/nodes/history-config.html +8 -2
- package/nodes/network-point-read.js +6 -1
- package/nodes/network-point-register.html +1 -1
- package/nodes/network-service-bridge.js +43 -11
- package/nodes/network-service-registry.html +236 -27
- package/nodes/network-service-registry.js +1 -1
- package/nodes/or-block.js +1 -1
- package/nodes/priority-block.js +1 -1
- package/nodes/tstat-block.html +34 -79
- package/nodes/tstat-block.js +223 -345
- package/nodes/utils.js +1 -1
- package/package.json +90 -75
package/nodes/global-setter.js
CHANGED
|
@@ -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,
|
|
@@ -91,16 +92,46 @@ module.exports = function(RED) {
|
|
|
91
92
|
}
|
|
92
93
|
node.isBusy = true;
|
|
93
94
|
|
|
94
|
-
//
|
|
95
|
+
// Resolve write priority — three sources, in order of precedence:
|
|
96
|
+
// 1. msg.priority (number 1-16 or "default") — explicit per-message override
|
|
97
|
+
// 2. msg.context ("priority1"–"priority16" or "default") — tagged-input pattern (matches priority-block)
|
|
98
|
+
// 3. Configured writePriority (dropdown / msg / flow typed-input)
|
|
95
99
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
if (msg.hasOwnProperty("priority")) {
|
|
101
|
+
// Source 1: msg.priority (direct number or "default")
|
|
102
|
+
const mp = msg.priority;
|
|
103
|
+
if (mp === "default") {
|
|
104
|
+
node.writePriority = "default";
|
|
105
|
+
} else {
|
|
106
|
+
const p = parseInt(mp, 10);
|
|
107
|
+
if (isNaN(p) || p < 1 || p > 16) {
|
|
108
|
+
node.isBusy = false;
|
|
109
|
+
return utils.sendError(node, msg, done, `Invalid msg.priority: ${mp}`);
|
|
110
|
+
}
|
|
111
|
+
node.writePriority = String(p);
|
|
112
|
+
}
|
|
113
|
+
} else if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
|
|
114
|
+
// Source 2: msg.context tagged-input ("priority8", "default", etc.)
|
|
115
|
+
// "reload" is handled separately below — skip it here
|
|
116
|
+
const ctx = msg.context;
|
|
117
|
+
const priorityMatch = /^priority([1-9]|1[0-6])$/.exec(ctx);
|
|
118
|
+
if (priorityMatch) {
|
|
119
|
+
node.writePriority = priorityMatch[1];
|
|
120
|
+
} else if (ctx === "default") {
|
|
121
|
+
node.writePriority = "default";
|
|
122
|
+
}
|
|
123
|
+
// Other contexts (e.g. "reload") fall through — config stays as-is
|
|
124
|
+
} else {
|
|
125
|
+
// Source 3: Configured typed-input (dropdown, msg path, flow variable)
|
|
126
|
+
const evaluations = [];
|
|
127
|
+
evaluations.push(
|
|
128
|
+
utils.requiresEvaluation(config.writePriorityType)
|
|
129
|
+
? utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg)
|
|
130
|
+
: Promise.resolve(node.writePriority)
|
|
131
|
+
);
|
|
132
|
+
const results = await Promise.all(evaluations);
|
|
133
|
+
node.writePriority = results[0];
|
|
134
|
+
}
|
|
104
135
|
} catch (err) {
|
|
105
136
|
throw new Error(`Property Eval Error: ${err.message}`);
|
|
106
137
|
} finally {
|
|
@@ -120,7 +151,7 @@ module.exports = function(RED) {
|
|
|
120
151
|
await utils.setGlobalState(node, node.varName, node.storeName, state);
|
|
121
152
|
|
|
122
153
|
prefix = state.activePriority === 'default' ? '' : 'P';
|
|
123
|
-
const statusText = `reload: ${prefix}${state.activePriority}:${state.value}${state.units}`;
|
|
154
|
+
const statusText = `reload: ${prefix}${state.activePriority}:${state.value}${state.units || ''}`;
|
|
124
155
|
|
|
125
156
|
return utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
|
|
126
157
|
}
|
|
@@ -158,8 +189,16 @@ module.exports = function(RED) {
|
|
|
158
189
|
|
|
159
190
|
// Check for change
|
|
160
191
|
if (value === state.value && priority === state.activePriority) {
|
|
192
|
+
// Ensure payload stays in sync with value
|
|
193
|
+
state.payload = state.value;
|
|
194
|
+
// Persist even when output unchanged — the priority array itself changed
|
|
195
|
+
await utils.setGlobalState(node, node.varName, node.storeName, state);
|
|
196
|
+
if (node.storeName !== 'default') {
|
|
197
|
+
await utils.setGlobalState(node, node.varName, 'default', state);
|
|
198
|
+
}
|
|
161
199
|
prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
|
|
162
|
-
const
|
|
200
|
+
const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
|
|
201
|
+
const noChangeText = `no change: ${prefix}${node.writePriority}:${inputValue} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units || ''}`;
|
|
163
202
|
utils.setStatusUnchanged(node, noChangeText);
|
|
164
203
|
// Pass message through even if no context change
|
|
165
204
|
send({ ...state });
|
|
@@ -199,7 +238,7 @@ module.exports = function(RED) {
|
|
|
199
238
|
|
|
200
239
|
prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
|
|
201
240
|
const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
|
|
202
|
-
const statusText = `write: ${prefix}${node.writePriority}:${inputValue}${state.units} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units}`;
|
|
241
|
+
const statusText = `write: ${prefix}${node.writePriority}:${inputValue}${state.units || ''} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units || ''}`;
|
|
203
242
|
|
|
204
243
|
RED.events.emit("bldgblocks:global:value-changed", {
|
|
205
244
|
key: node.varName,
|
|
@@ -216,8 +255,8 @@ module.exports = function(RED) {
|
|
|
216
255
|
});
|
|
217
256
|
|
|
218
257
|
node.on('close', function(removed, done) {
|
|
258
|
+
if (initTimer) { clearTimeout(initTimer); initTimer = null; }
|
|
219
259
|
if (removed && node.varName) {
|
|
220
|
-
RED.events.removeAllListeners("bldgblocks:global:value-changed");
|
|
221
260
|
// Callback style safe for close
|
|
222
261
|
node.context().global.set(node.varName, undefined, node.storeName, function() {
|
|
223
262
|
done();
|
|
@@ -228,4 +267,47 @@ module.exports = function(RED) {
|
|
|
228
267
|
});
|
|
229
268
|
}
|
|
230
269
|
RED.nodes.registerType("global-setter", GlobalSetterNode);
|
|
270
|
+
|
|
271
|
+
// --- Admin endpoint: Clear all priority slots for a given setter node ---
|
|
272
|
+
RED.httpAdmin.post('/global-setter/:id/clear-priorities', RED.auth.needsPermission('global-setter.write'), async function(req, res) {
|
|
273
|
+
const targetNode = RED.nodes.getNode(req.params.id);
|
|
274
|
+
if (!targetNode) {
|
|
275
|
+
return res.status(404).json({ error: "Node not found" });
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
let state = await utils.getGlobalState(targetNode, targetNode.varName, targetNode.storeName);
|
|
279
|
+
if (!state || typeof state !== 'object' || !state.priority) {
|
|
280
|
+
return res.status(200).json({ message: "No state to clear" });
|
|
281
|
+
}
|
|
282
|
+
// Clear all 16 priority slots
|
|
283
|
+
for (let i = 1; i <= 16; i++) {
|
|
284
|
+
state.priority[i] = null;
|
|
285
|
+
}
|
|
286
|
+
// Recalculate winner (will fall back to default)
|
|
287
|
+
const { value, priority } = utils.getHighestPriority(state);
|
|
288
|
+
state.payload = value;
|
|
289
|
+
state.value = value;
|
|
290
|
+
state.activePriority = priority;
|
|
291
|
+
state.metadata.lastSet = new Date().toISOString();
|
|
292
|
+
state.metadata.sourceId = targetNode.id;
|
|
293
|
+
|
|
294
|
+
await utils.setGlobalState(targetNode, targetNode.varName, targetNode.storeName, state);
|
|
295
|
+
if (targetNode.storeName !== 'default') {
|
|
296
|
+
await utils.setGlobalState(targetNode, targetNode.varName, 'default', state);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
RED.events.emit("bldgblocks:global:value-changed", {
|
|
300
|
+
key: targetNode.varName,
|
|
301
|
+
store: targetNode.storeName,
|
|
302
|
+
data: state
|
|
303
|
+
});
|
|
304
|
+
utils.setStatusOK(targetNode, `cleared: default:${state.value}`);
|
|
305
|
+
targetNode.send({ ...state });
|
|
306
|
+
|
|
307
|
+
res.status(200).json({ message: "Priorities cleared", value: state.value, activePriority: state.activePriority });
|
|
308
|
+
} catch (err) {
|
|
309
|
+
targetNode.error(`Clear priorities error: ${err.message}`);
|
|
310
|
+
res.status(500).json({ error: err.message });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
231
313
|
}
|
package/nodes/history-buffer.js
CHANGED
|
@@ -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}.
|
|
24
|
-
// Legacy file
|
|
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
|
|
@@ -122,6 +123,26 @@ module.exports = function(RED) {
|
|
|
122
123
|
const now = new Date();
|
|
123
124
|
const currentTimestamp = Math.floor(now.getTime() / 1000);
|
|
124
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
|
+
|
|
125
146
|
// Check for this node's specific buffer
|
|
126
147
|
try {
|
|
127
148
|
const stats = await fs.promises.stat(BUFFER_FILE);
|
|
@@ -136,7 +157,7 @@ module.exports = function(RED) {
|
|
|
136
157
|
|
|
137
158
|
if (isStale) {
|
|
138
159
|
const fileTs = Math.floor(stats.mtimeMs / 1000);
|
|
139
|
-
const newName = path.join(TRENDS_DIR, `trend_${fileTs}_${node.id}.
|
|
160
|
+
const newName = path.join(TRENDS_DIR, `trend_${fileTs}_${node.id}.jsonl`);
|
|
140
161
|
await fs.promises.rename(BUFFER_FILE, newName);
|
|
141
162
|
}
|
|
142
163
|
|
|
@@ -147,7 +168,7 @@ module.exports = function(RED) {
|
|
|
147
168
|
// Check for legacy buffer (migration)
|
|
148
169
|
try {
|
|
149
170
|
await fs.promises.access(LEGACY_BUFFER_FILE);
|
|
150
|
-
const legacyName = path.join(TRENDS_DIR, `trend_${currentTimestamp}_legacy.
|
|
171
|
+
const legacyName = path.join(TRENDS_DIR, `trend_${currentTimestamp}_legacy.jsonl`);
|
|
151
172
|
await fs.promises.rename(LEGACY_BUFFER_FILE, legacyName);
|
|
152
173
|
} catch (err) {
|
|
153
174
|
// Ignore
|
|
@@ -157,9 +178,9 @@ module.exports = function(RED) {
|
|
|
157
178
|
async function getHistoricalFiles() {
|
|
158
179
|
try {
|
|
159
180
|
const files = await fs.promises.readdir(TRENDS_DIR);
|
|
160
|
-
// Filter for our files: trend_TIMESTAMP_*.json
|
|
181
|
+
// Filter for our files: trend_TIMESTAMP_*.json or .jsonl
|
|
161
182
|
return files
|
|
162
|
-
.filter(f => f.startsWith('trend_') && f.endsWith('.json'))
|
|
183
|
+
.filter(f => f.startsWith('trend_') && (f.endsWith('.json') || f.endsWith('.jsonl')))
|
|
163
184
|
.sort(); // String sort works for fixed-length timestamps usually, but numeric would be safer
|
|
164
185
|
} catch (err) {
|
|
165
186
|
return [];
|
|
@@ -250,20 +271,10 @@ module.exports = function(RED) {
|
|
|
250
271
|
// Reset live buffer immediately so new messages go into next batch
|
|
251
272
|
liveBuffer = [];
|
|
252
273
|
|
|
253
|
-
const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n');
|
|
274
|
+
const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n') + '\n';
|
|
254
275
|
|
|
255
276
|
try {
|
|
256
|
-
|
|
257
|
-
// We'll let the next append handle its own newline or just ensure we join efficiently.
|
|
258
|
-
// Actually, safest is to append WITH a preceding newline if file exists.
|
|
259
|
-
|
|
260
|
-
let prefix = '';
|
|
261
|
-
try {
|
|
262
|
-
const stats = await fs.promises.stat(BUFFER_FILE);
|
|
263
|
-
if (stats.size > 0) prefix = '\n';
|
|
264
|
-
} catch(e) { /* File doesn't exist, no prefix needed */ }
|
|
265
|
-
|
|
266
|
-
await fs.promises.appendFile(BUFFER_FILE, prefix + lines);
|
|
277
|
+
await fs.promises.appendFile(BUFFER_FILE, lines);
|
|
267
278
|
} catch (err) {
|
|
268
279
|
node.warn(`Buffer commit failed: ${err.message}`);
|
|
269
280
|
// Put points back at the start of buffer if write failed
|
|
@@ -336,15 +347,9 @@ module.exports = function(RED) {
|
|
|
336
347
|
if (liveBuffer.length > 0) {
|
|
337
348
|
const pointsToCommit = liveBuffer;
|
|
338
349
|
liveBuffer = []; // Clear memory
|
|
339
|
-
const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n');
|
|
350
|
+
const lines = pointsToCommit.map(p => JSON.stringify(p)).join('\n') + '\n';
|
|
340
351
|
try {
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
const stats = await fs.promises.stat(BUFFER_FILE);
|
|
344
|
-
if (stats.size > 0) prefix = '\n';
|
|
345
|
-
} catch(e) { /* File doesn't exist */ }
|
|
346
|
-
|
|
347
|
-
await fs.promises.appendFile(BUFFER_FILE, prefix + lines);
|
|
352
|
+
await fs.promises.appendFile(BUFFER_FILE, lines);
|
|
348
353
|
} catch (err) {
|
|
349
354
|
// If append fails, we might lose these points during rotation,
|
|
350
355
|
// put them back and abort rotation. Use concat for safety.
|
|
@@ -356,7 +361,7 @@ module.exports = function(RED) {
|
|
|
356
361
|
|
|
357
362
|
// Perform Rotation (Rename)
|
|
358
363
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
359
|
-
const newName = path.join(TRENDS_DIR, `trend_${timestamp}_${node.id}.
|
|
364
|
+
const newName = path.join(TRENDS_DIR, `trend_${timestamp}_${node.id}.jsonl`);
|
|
360
365
|
|
|
361
366
|
try {
|
|
362
367
|
await fs.promises.access(BUFFER_FILE);
|
|
@@ -95,7 +95,9 @@
|
|
|
95
95
|
|
|
96
96
|
// 1. Update Series Dropdown
|
|
97
97
|
const seriesOptions = configNode && configNode.series ?
|
|
98
|
-
configNode.series
|
|
98
|
+
configNode.series
|
|
99
|
+
.map(s => ({ value: s.seriesName, label: s.seriesName }))
|
|
100
|
+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' })) :
|
|
99
101
|
[];
|
|
100
102
|
|
|
101
103
|
seriesInput.typedInput('types', [{
|
|
@@ -134,7 +134,7 @@ module.exports = function(RED) {
|
|
|
134
134
|
msg.measurement = escapedMeasurementName;
|
|
135
135
|
msg.payload = line;
|
|
136
136
|
node.send(msg);
|
|
137
|
-
utils.setStatusChanged(node, `
|
|
137
|
+
utils.setStatusChanged(node, `stored: ${valueString}`);
|
|
138
138
|
} else if (node.storageType === 'object') {
|
|
139
139
|
msg.measurement = escapedMeasurementName;
|
|
140
140
|
msg.payload = {
|
|
@@ -144,7 +144,7 @@ module.exports = function(RED) {
|
|
|
144
144
|
timestamp: timestamp
|
|
145
145
|
};
|
|
146
146
|
node.send(msg);
|
|
147
|
-
utils.setStatusChanged(node, `
|
|
147
|
+
utils.setStatusChanged(node, `stored: ${valueString}`);
|
|
148
148
|
} else if (node.storageType === 'objectArray') {
|
|
149
149
|
msg.measurement = escapedMeasurementName;
|
|
150
150
|
msg.timestamp = timestamp;
|
|
@@ -155,7 +155,7 @@ module.exports = function(RED) {
|
|
|
155
155
|
tagsObj
|
|
156
156
|
]
|
|
157
157
|
node.send(msg);
|
|
158
|
-
utils.setStatusChanged(node, `
|
|
158
|
+
utils.setStatusChanged(node, `stored: ${valueString}`);
|
|
159
159
|
} else if (node.storageType === 'batchObject') {
|
|
160
160
|
msg.payload = {
|
|
161
161
|
measurement: escapedMeasurementName,
|
|
@@ -166,7 +166,7 @@ module.exports = function(RED) {
|
|
|
166
166
|
tags: tagsObj
|
|
167
167
|
}
|
|
168
168
|
node.send(msg);
|
|
169
|
-
utils.setStatusChanged(node, `
|
|
169
|
+
utils.setStatusChanged(node, `stored: ${valueString}`);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (done) done();
|
|
@@ -149,8 +149,11 @@
|
|
|
149
149
|
);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
(node.series || []).
|
|
153
|
-
|
|
152
|
+
const sortedSeries = (node.series || []).slice().sort((a, b) =>
|
|
153
|
+
(a.seriesName || '').localeCompare(b.seriesName || '', undefined, { sensitivity: 'base' })
|
|
154
|
+
);
|
|
155
|
+
sortedSeries.forEach(addRow);
|
|
156
|
+
if (sortedSeries.length === 0) {
|
|
154
157
|
addRow({seriesName: "OutsideTemp", seriesUnits: "°F"});
|
|
155
158
|
}
|
|
156
159
|
|
|
@@ -223,6 +226,9 @@
|
|
|
223
226
|
RED.notify("At least one valid series is required", "error");
|
|
224
227
|
throw new Error("No valid series");
|
|
225
228
|
}
|
|
229
|
+
series.sort((a, b) =>
|
|
230
|
+
(a.seriesName || '').localeCompare(b.seriesName || '', undefined, { sensitivity: 'base' })
|
|
231
|
+
);
|
|
226
232
|
this.series = series;
|
|
227
233
|
this.name = cleanSaveStr($("#node-config-input-name").val()) || 'default';
|
|
228
234
|
}
|
|
@@ -147,9 +147,14 @@ 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
|
-
node.error(errorText); // Show in debug panel
|
|
157
|
+
//node.error(errorText); // Show in debug panel
|
|
153
158
|
// Don't update cache on error, keep stale value
|
|
154
159
|
return;
|
|
155
160
|
}
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
// Also check deployed registry for any points not visible in editor
|
|
129
|
-
$.getJSON(
|
|
129
|
+
$.getJSON(`network-point-registry/list/${registry}`, function (data) {
|
|
130
130
|
const maxDeployedId = data.reduce((max, pt) => Math.max(max, pt.id), 0);
|
|
131
131
|
const next = Math.max(maxEditorId, maxDeployedId) + 1;
|
|
132
132
|
idInput.val(next);
|
|
@@ -112,10 +112,18 @@ module.exports = function(RED) {
|
|
|
112
112
|
const pending = node.pendingRequests[data.requestId];
|
|
113
113
|
delete node.pendingRequests[data.requestId];
|
|
114
114
|
|
|
115
|
-
// Suppress error notification during startup phase
|
|
116
|
-
// (allows network to come online without nuisance errors)
|
|
117
115
|
if (pending.isStartupPhase) {
|
|
118
|
-
//
|
|
116
|
+
// During startup: still notify point-read to reset isPollPending,
|
|
117
|
+
// but suppress error logging (network may still be coming online)
|
|
118
|
+
RED.events.emit('pointReference:response', {
|
|
119
|
+
sourceNodeId: data.sourceNodeId,
|
|
120
|
+
pointId: data.pointId,
|
|
121
|
+
value: null,
|
|
122
|
+
error: true,
|
|
123
|
+
errorMessage: "Startup timeout",
|
|
124
|
+
requestId: data.requestId,
|
|
125
|
+
isStartupPhase: true
|
|
126
|
+
});
|
|
119
127
|
return;
|
|
120
128
|
}
|
|
121
129
|
|
|
@@ -195,10 +203,16 @@ module.exports = function(RED) {
|
|
|
195
203
|
const pending = node.pendingRequests[data.requestId];
|
|
196
204
|
delete node.pendingRequests[data.requestId];
|
|
197
205
|
|
|
198
|
-
// Suppress error notification during startup phase
|
|
199
|
-
// (allows network to come online without nuisance errors)
|
|
200
206
|
if (pending.isStartupPhase) {
|
|
201
|
-
//
|
|
207
|
+
// During startup: still notify point-write to reset pending state,
|
|
208
|
+
// but suppress error logging
|
|
209
|
+
RED.events.emit('pointWrite:response', {
|
|
210
|
+
sourceNodeId: pending.sourceNodeId,
|
|
211
|
+
pointId: data.pointId,
|
|
212
|
+
error: "Startup timeout",
|
|
213
|
+
requestId: data.requestId,
|
|
214
|
+
isStartupPhase: true
|
|
215
|
+
});
|
|
202
216
|
return;
|
|
203
217
|
}
|
|
204
218
|
|
|
@@ -240,14 +254,16 @@ module.exports = function(RED) {
|
|
|
240
254
|
// ================================================================
|
|
241
255
|
|
|
242
256
|
// Check if this looks like a point response (has network.pointId or status.pointId)
|
|
243
|
-
const
|
|
257
|
+
const rawPointId = msg.network?.pointId ?? msg.status?.pointId ?? msg.pointId;
|
|
258
|
+
// Normalize to number - point-read stores pointId as int, but WebSocket responses may return strings
|
|
259
|
+
const responsePointId = rawPointId !== undefined && rawPointId !== null ? parseInt(rawPointId, 10) : undefined;
|
|
244
260
|
const responseValue = msg.value ?? msg.payload;
|
|
245
261
|
const statusCode = msg.status?.code;
|
|
246
262
|
const statusMessage = msg.status?.message || "";
|
|
247
263
|
const isError = statusCode === "error";
|
|
248
264
|
|
|
249
|
-
// Valid response if we have a
|
|
250
|
-
const isValidResponse = responsePointId !== undefined;
|
|
265
|
+
// Valid response if we have a valid numeric pointId
|
|
266
|
+
const isValidResponse = responsePointId !== undefined && !isNaN(responsePointId);
|
|
251
267
|
|
|
252
268
|
if (isValidResponse) {
|
|
253
269
|
// Find ALL matching pending requests by pointId
|
|
@@ -313,8 +329,10 @@ module.exports = function(RED) {
|
|
|
313
329
|
updateStatus();
|
|
314
330
|
}
|
|
315
331
|
} else {
|
|
316
|
-
// Response without matching request -
|
|
317
|
-
|
|
332
|
+
// Response without matching request - duplicate, stale, or already timed-out
|
|
333
|
+
// This is normal when remote has multiple WebSocket nodes or response arrives after timeout cleanup
|
|
334
|
+
// Don't change node status - just log at trace level
|
|
335
|
+
node.trace(`Ignoring duplicate/stale response for point #${responsePointId}`);
|
|
318
336
|
}
|
|
319
337
|
|
|
320
338
|
if (done) done();
|
|
@@ -358,6 +376,11 @@ module.exports = function(RED) {
|
|
|
358
376
|
// Node lifecycle
|
|
359
377
|
// ====================================================================
|
|
360
378
|
node.on("close", function(done) {
|
|
379
|
+
// Clear startup timer
|
|
380
|
+
if (node.startupTimer) {
|
|
381
|
+
clearTimeout(node.startupTimer);
|
|
382
|
+
node.startupTimer = null;
|
|
383
|
+
}
|
|
361
384
|
// Clear pending requests on close
|
|
362
385
|
node.pendingRequests = {};
|
|
363
386
|
// Remove event listeners
|
|
@@ -369,6 +392,15 @@ module.exports = function(RED) {
|
|
|
369
392
|
// ====================================================================
|
|
370
393
|
// Initialize
|
|
371
394
|
// ====================================================================
|
|
395
|
+
// One-shot timer to guarantee startup delay completes even if no messages arrive
|
|
396
|
+
node.startupTimer = setTimeout(() => {
|
|
397
|
+
if (!node.startupComplete) {
|
|
398
|
+
node.startupComplete = true;
|
|
399
|
+
updateStatus();
|
|
400
|
+
}
|
|
401
|
+
node.startupTimer = null;
|
|
402
|
+
}, node.startupDelay * 1000);
|
|
403
|
+
|
|
372
404
|
updateStatus();
|
|
373
405
|
}
|
|
374
406
|
|