node-red-contrib-uos-nats 1.3.39 → 1.3.52
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/README.md +23 -6
- package/nodes/datahub-input.html +4 -0
- package/nodes/datahub-input.js +108 -76
- package/nodes/datahub-output.html +3 -1
- package/nodes/datahub-output.js +17 -5
- package/nodes/datahub-write.html +21 -2
- package/nodes/datahub-write.js +26 -15
- package/nodes/uos-config.js +61 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,12 +68,28 @@ Reads values from existing providers (like `u_os_adm`).
|
|
|
68
68
|
- **Provider ID:** Name of the source provider.
|
|
69
69
|
- **Variables:** Use **Load Variables** to browse and select variables.
|
|
70
70
|
- **Trigger:** "Event" (instant update) or "Poll" (interval).
|
|
71
|
+
- **Dynamic Read:** Send `msg.payload` as an Array of keys (e.g. `["machine.status", "temp"]`) to trigger a specific snapshot, ignoring the node configuration.
|
|
71
72
|
|
|
72
73
|
### DataHub - Write
|
|
73
74
|
Changes values in other providers.
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
75
|
+
- **Single Mode:** Select a variable from the list. Send `msg.payload` = value.
|
|
76
|
+
- **Batch Mode:** Select NO variable (clear selection). Send `msg.payload` as a JSON object: `{"var_key": value, "var2": value}` (uses Configured Provider).
|
|
77
|
+
- **Dynamic Mode:** Send a full target object to write anywhere:
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"provider": "target_provider_id",
|
|
81
|
+
"key": "variable_key",
|
|
82
|
+
"value": 123
|
|
83
|
+
}
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"provider": "target_provider_id",
|
|
87
|
+
"key": "variable_key",
|
|
88
|
+
"value": 123
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
Or send an **Array** of these objects to write to multiple providers in one go.
|
|
92
|
+
- **Strict Mode:** Automatically handles Fingerprints for strict providers (e.g. `u_os_sbm`).
|
|
77
93
|
|
|
78
94
|
### DataHub - Provider
|
|
79
95
|
Publishes your own data to the Data Hub.
|
|
@@ -92,10 +108,11 @@ Publishes your own data to the Data Hub.
|
|
|
92
108
|
## Troubleshooting
|
|
93
109
|
|
|
94
110
|
- **Provider not visible?** Ensure **Provider ID** matches your **Client ID**. Easiest way: Leave Provider ID empty in the node.
|
|
111
|
+
- **Node Status is Green (Ring)?**
|
|
112
|
+
- `waiting for provider`: The node is connected to NATS (OK), but the target Provider (e.g. `u_os_sbm`) is currently offline. It will resume automatically.
|
|
95
113
|
- **Node Status is Yellow?**
|
|
96
|
-
- `cooldown (10s)`: The node is
|
|
97
|
-
- `
|
|
98
|
-
- `auth failed`: Check your OAuth Client Secret and Scopes.
|
|
114
|
+
- `cooldown (10s)`: The node is pausing after an error to protect the network.
|
|
115
|
+
- `auth failed`: OAuth credentials generated an error. Check Client Secret.
|
|
99
116
|
- **Node Status is Red?**
|
|
100
117
|
- `illegal ID`: You used a reserved name like `u_os_sbm`. Rename your Client/Provider.
|
|
101
118
|
- `write error`: A command failed. Check Scopes (`hub.variables.readwrite`) or Fingerprint.
|
package/nodes/datahub-input.html
CHANGED
|
@@ -348,4 +348,8 @@
|
|
|
348
348
|
<li><b>Provider ID:</b> ID of the data source (e.g., <code>u_os_adm</code>). Use the search button to find available providers.</li>
|
|
349
349
|
<li><b>Variables:</b> Click "Load Variables" to select specific data points. Leave empty to read everything.</li>
|
|
350
350
|
</ol>
|
|
351
|
+
<p>
|
|
352
|
+
<b>Dynamic Read:</b> Send <code>msg.payload</code> as an Array of variable keys (e.g. <code>["var1", "var2"]</code>)
|
|
353
|
+
to read specific variables on demand. This overrides the node's configuration for that single request.
|
|
354
|
+
</p>
|
|
351
355
|
</script>
|
package/nodes/datahub-input.js
CHANGED
|
@@ -168,101 +168,126 @@ module.exports = function (RED) {
|
|
|
168
168
|
return; // Stop here, do not start Interval or Subscription
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
performSnapshot = async () => {
|
|
172
|
-
// Debugging connection state
|
|
171
|
+
performSnapshot = async (overrideItems) => {
|
|
173
172
|
if (!nc || nc.isClosed()) {
|
|
174
173
|
this.warn('Snapshot skipped: Connection is closed or not ready.');
|
|
175
174
|
return;
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
// Group items by Provider
|
|
178
|
+
// Default Provider: this.providerId
|
|
179
|
+
const requests = new Map(); // ProviderId -> Set<VariableId>
|
|
180
|
+
|
|
181
|
+
if (overrideItems) {
|
|
182
|
+
// Dynamic Mode: Resolve each item
|
|
183
|
+
for (const item of overrideItems) {
|
|
184
|
+
let pId = (typeof item === 'object' && item.provider) ? item.provider : this.providerId;
|
|
185
|
+
let key = (typeof item === 'object') ? item.key : item; // String or Object.key
|
|
182
186
|
|
|
187
|
+
if (!key) continue;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// Use Config Node Resolution (Cache)
|
|
191
|
+
// This handles fetching definitions for ANY provider on demand
|
|
192
|
+
const resolved = await connection.resolveVariableId(pId, key);
|
|
193
|
+
|
|
194
|
+
if (!requests.has(pId)) requests.set(pId, new Set());
|
|
195
|
+
requests.get(pId).add(resolved.id);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
this.warn(`Dynamic Read: Skipping '${key}' on '${pId}': ${e.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
// Static Mode: Use local defMap for configured provider
|
|
202
|
+
const targetIds = new Set();
|
|
203
|
+
const isWildcard = (this.variables.length === 0);
|
|
183
204
|
if (!isWildcard) {
|
|
184
|
-
// Reverse lookup: Find Def by Key
|
|
185
205
|
const requestedKeys = new Set(this.variables);
|
|
186
|
-
|
|
187
206
|
for (const def of defMap.values()) {
|
|
188
|
-
if (requestedKeys.has(def.key))
|
|
189
|
-
targetIds.push(Number(def.id));
|
|
190
|
-
}
|
|
207
|
+
if (requestedKeys.has(def.key)) targetIds.add(def.id);
|
|
191
208
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// we MUST NOT send an empty list, because that would interpret as "Read All".
|
|
195
|
-
if (targetIds.length === 0) {
|
|
196
|
-
// Already handled by Gate above mostly, but good for safety
|
|
197
|
-
this.warn(`Snapshot Aborted: None of the ${this.variables.length} requested variables could be resolved to IDs.`);
|
|
209
|
+
if (targetIds.size === 0) {
|
|
210
|
+
this.warn(`Snapshot Aborted: None of the configured variables could be resolved.`);
|
|
198
211
|
return;
|
|
199
212
|
}
|
|
200
213
|
}
|
|
214
|
+
requests.set(this.providerId, targetIds); // Empty Set = Wildcard
|
|
215
|
+
}
|
|
201
216
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// The Config Node now handles concurrency (max 3 parallel), preventing 503s naturally.
|
|
207
|
-
// We use a simple retry wrapper just in case.
|
|
208
|
-
let lastError;
|
|
209
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
210
|
-
try {
|
|
211
|
-
let snapshotMsg;
|
|
212
|
-
// If config node has serialRequest (Semaphore), use it.
|
|
213
|
-
if (typeof connection.serialRequest === 'function') {
|
|
214
|
-
snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 10000 });
|
|
215
|
-
} else {
|
|
216
|
-
// Fallback to direct request (unsafe)
|
|
217
|
-
snapshotMsg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 10000 });
|
|
218
|
-
}
|
|
217
|
+
if (requests.size === 0) {
|
|
218
|
+
this.warn("Snapshot Aborted: No valid variables resolved.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
219
221
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const states = payloads.decodeVariableList(snapshotObj.variables());
|
|
223
|
-
|
|
224
|
-
const filteredSnapshot = processStates(states);
|
|
225
|
-
if (filteredSnapshot.length > 0) {
|
|
226
|
-
this.send({ payload: { type: 'snapshot', variables: filteredSnapshot } });
|
|
227
|
-
this.status({ fill: 'green', shape: 'dot', text: 'active' });
|
|
228
|
-
} else {
|
|
229
|
-
// Handle empty/partial...
|
|
230
|
-
this.status({ fill: 'green', shape: 'ring', text: 'active (empty)' });
|
|
231
|
-
}
|
|
232
|
-
return true; // Success
|
|
222
|
+
const allResults = [];
|
|
223
|
+
const errors = [];
|
|
233
224
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
this.status({ fill: 'yellow', shape: 'dot', text: 'provider offline' });
|
|
245
|
-
} else if (msg.includes('Cooldown')) {
|
|
246
|
-
this.debug(`Snapshot skipped (Cooldown): ${msg}`);
|
|
247
|
-
this.status({ fill: 'yellow', shape: 'ring', text: 'cooldown (10s)' });
|
|
248
|
-
} else if (msg.includes('Authorization') || msg.includes('Permission')) {
|
|
249
|
-
this.debug(`Snapshot skipped (Auth): ${msg}`);
|
|
250
|
-
this.status({ fill: 'yellow', shape: 'ring', text: 'auth failed' });
|
|
225
|
+
// Execute Requests (Serial or Parallel?)
|
|
226
|
+
// Using Serial to be safe with connection bandwidth
|
|
227
|
+
for (const [pId, varIds] of requests) {
|
|
228
|
+
try {
|
|
229
|
+
let targetIdsArr = Array.from(varIds);
|
|
230
|
+
|
|
231
|
+
// Request
|
|
232
|
+
let snapshotMsg;
|
|
233
|
+
if (typeof connection.serialRequest === 'function') {
|
|
234
|
+
snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(pId), payloads.buildReadVariablesQuery(targetIdsArr), { timeout: 10000 });
|
|
251
235
|
} else {
|
|
252
|
-
|
|
253
|
-
this.status({ fill: 'red', shape: 'ring', text: 'snapshot error' });
|
|
236
|
+
snapshotMsg = await nc.request(subjects.readVariablesQuery(pId), payloads.buildReadVariablesQuery(targetIdsArr), { timeout: 10000 });
|
|
254
237
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
238
|
+
|
|
239
|
+
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
240
|
+
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
241
|
+
const states = payloads.decodeVariableList(snapshotObj.variables());
|
|
242
|
+
|
|
243
|
+
// Add Provider Info to Result?
|
|
244
|
+
// ProcessStates adds keys, but we might want to know which provider it came from if mixed?
|
|
245
|
+
// The current `processStates` maps IDs back to Keys using `defMap`.
|
|
246
|
+
// ISSUE: `defMap` only has local provider defs.
|
|
247
|
+
// If we read from foreign provider, `processStates` will return IDs or "Unknown".
|
|
248
|
+
// We need `resolveVariableId` to also START caching the reverse lookup or we assume result has IDs?
|
|
249
|
+
// Wait, `decodeVariableList` returns {id, value}.
|
|
250
|
+
// `processStates` tries to find Key.
|
|
251
|
+
|
|
252
|
+
// QUICK FIX for Dynamic Multi-Provider:
|
|
253
|
+
// If pId !== this.providerId, we don't have local defMap.
|
|
254
|
+
// But `resolveVariableId` (in config) caches definitions.
|
|
255
|
+
// We can ask config to resolve ID back to Key? Or just return ID?
|
|
256
|
+
// Better: We know the Keys we asked for.
|
|
257
|
+
|
|
258
|
+
// Let's modify processStates to accept a custom Map?
|
|
259
|
+
// Or simple: Just return the raw objects with IDs if we can't map them?
|
|
260
|
+
// Actually, for Dynamic Read, users might accept {id, value} or we try to enrich.
|
|
261
|
+
|
|
262
|
+
const enriched = states.map(s => {
|
|
263
|
+
// Try local defMap
|
|
264
|
+
if (pId === this.providerId && defMap.has(s.id)) {
|
|
265
|
+
return { ...s, key: defMap.get(s.id).key, provider: pId };
|
|
266
|
+
}
|
|
267
|
+
// Try Config Node Cache (it has all fetched defs)
|
|
268
|
+
const cachedDef = connection.getProviderVariable(pId, s.id);
|
|
269
|
+
if (cachedDef) {
|
|
270
|
+
return { ...s, key: cachedDef.key, provider: pId };
|
|
271
|
+
}
|
|
272
|
+
return { ...s, key: `id:${s.id}`, provider: pId };
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
allResults.push(...enriched);
|
|
276
|
+
|
|
277
|
+
} catch (err) {
|
|
278
|
+
errors.push(`${pId}: ${err.message}`);
|
|
264
279
|
}
|
|
265
280
|
}
|
|
281
|
+
|
|
282
|
+
if (allResults.length > 0) {
|
|
283
|
+
this.send({ payload: { type: 'snapshot', variables: allResults } });
|
|
284
|
+
this.status({ fill: 'green', shape: 'dot', text: 'active' });
|
|
285
|
+
} else if (errors.length > 0) {
|
|
286
|
+
this.warn(`Snapshot errors: ${errors.join('; ')}`);
|
|
287
|
+
this.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
288
|
+
} else {
|
|
289
|
+
this.status({ fill: 'green', shape: 'ring', text: 'empty' });
|
|
290
|
+
}
|
|
266
291
|
};
|
|
267
292
|
|
|
268
293
|
|
|
@@ -334,7 +359,14 @@ module.exports = function (RED) {
|
|
|
334
359
|
};
|
|
335
360
|
|
|
336
361
|
this.on('input', (msg, send, done) => {
|
|
337
|
-
|
|
362
|
+
let overrideItems = null;
|
|
363
|
+
if (Array.isArray(msg.payload) && msg.payload.length > 0) {
|
|
364
|
+
// Pass the raw array items (String or Object {provider, key})
|
|
365
|
+
// performSnapshot will handle filtering
|
|
366
|
+
overrideItems = msg.payload;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
performSnapshot(overrideItems)
|
|
338
370
|
.then(() => done())
|
|
339
371
|
.catch((err) => done(err));
|
|
340
372
|
});
|
|
@@ -135,7 +135,9 @@
|
|
|
135
135
|
<dd>Connection node with host/port and OAuth credentials.</dd>
|
|
136
136
|
<dt>Provider ID</dt>
|
|
137
137
|
<dd>The unique ID for this provider. <b>Note:</b> You cannot use <code>u_os_sbm</code> (Reserved system name).</dd>
|
|
138
|
-
<
|
|
138
|
+
<dt>Provider ID</dt>
|
|
139
|
+
<dd>The unique ID for this provider. <b>Note:</b> You cannot use <code>u_os_sbm</code> (Reserved system name).</dd>
|
|
140
|
+
<dd><b>Auto Mode:</b> If empty, uses your **Client Name** (e.g. `nodered`) to match your Connection.</dd>
|
|
139
141
|
<dt>Keep-Alive (s)</dt>
|
|
140
142
|
<dd>Interval in seconds to refresh the provider definition. Default: <b>300s (5 min)</b>.</dd>
|
|
141
143
|
<dt>Allow external writes</dt>
|
package/nodes/datahub-output.js
CHANGED
|
@@ -77,11 +77,10 @@ module.exports = function (RED) {
|
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
// Retrieve configuration node
|
|
80
|
-
//
|
|
81
|
-
//
|
|
80
|
+
// Auto Mode: Prefer Client Name ("nodered") to match Connection Name.
|
|
81
|
+
// This ensures consistency with DataHub UI expectations.
|
|
82
82
|
let defaultId = connection.clientName;
|
|
83
83
|
if (!defaultId) {
|
|
84
|
-
// Fallback to ID if Name is missing (should not happen as Name is mandatory)
|
|
85
84
|
defaultId = connection.clientId || 'nodered';
|
|
86
85
|
}
|
|
87
86
|
|
|
@@ -134,6 +133,7 @@ module.exports = function (RED) {
|
|
|
134
133
|
};
|
|
135
134
|
|
|
136
135
|
// Check Singleton Status
|
|
136
|
+
this.fatalError = false;
|
|
137
137
|
let isPrimary = false;
|
|
138
138
|
const existingNodeId = providerRegistry.get(this.providerId);
|
|
139
139
|
if (existingNodeId && existingNodeId !== this.id) {
|
|
@@ -147,7 +147,8 @@ module.exports = function (RED) {
|
|
|
147
147
|
// --- SEND DEFINITION HELPER ---
|
|
148
148
|
const sendDefinitionUpdate = async (modPayloads, modSubjects) => {
|
|
149
149
|
if (!nc) return;
|
|
150
|
-
if (!isPrimary) return;
|
|
150
|
+
if (!isPrimary) return;
|
|
151
|
+
if (this.fatalError) return; // permanent lockout
|
|
151
152
|
|
|
152
153
|
try {
|
|
153
154
|
const { payload, fingerprint: fp } = modPayloads.buildProviderDefinitionEvent(definitions);
|
|
@@ -160,7 +161,17 @@ module.exports = function (RED) {
|
|
|
160
161
|
await nc.flush();
|
|
161
162
|
console.log(`[DataHub Output] Definition published. FP: ${fp}`);
|
|
162
163
|
} catch (err) {
|
|
163
|
-
|
|
164
|
+
let msg = err.message || '';
|
|
165
|
+
// Check for fatal Auth/Permission errors
|
|
166
|
+
if (msg.includes('Authorization') || msg.includes('permissions') || msg.includes('10003') || msg.includes('Access Denied')) {
|
|
167
|
+
this.fatalError = true;
|
|
168
|
+
this.error(`FATAL AUTH ERROR: ${msg}. Stopping provider to protect connection.`);
|
|
169
|
+
this.status({ fill: 'red', shape: 'dot', text: 'auth blocked (permanent)' });
|
|
170
|
+
// Clear heartbeats
|
|
171
|
+
if (outputHeartbeat) clearInterval(outputHeartbeat);
|
|
172
|
+
} else {
|
|
173
|
+
this.warn(`Definition update error: ${err.message}`);
|
|
174
|
+
}
|
|
164
175
|
}
|
|
165
176
|
};
|
|
166
177
|
|
|
@@ -186,6 +197,7 @@ module.exports = function (RED) {
|
|
|
186
197
|
// console.log('[DataHub Output] Heartbeat skipped: NATS closed.');
|
|
187
198
|
return;
|
|
188
199
|
}
|
|
200
|
+
if (this.fatalError) return; // permanent lockout
|
|
189
201
|
if (!loadedPayloads || !loadedSubjects) return;
|
|
190
202
|
|
|
191
203
|
// If we have no definitions yet, nothing to send
|
package/nodes/datahub-write.html
CHANGED
|
@@ -248,5 +248,24 @@
|
|
|
248
248
|
<li><b>Provider ID:</b> Target provider (e.g. <code>u_os_adm</code>). Use the search button to find it.</li>
|
|
249
249
|
<li><b>Variable:</b> Select a target variable. <b>Only writable variables are shown.</b></li>
|
|
250
250
|
</ol>
|
|
251
|
-
<p>If no variable is selected, the node works in <b>Batch Mode</b>
|
|
252
|
-
|
|
251
|
+
<p>If no variable is selected, the node works in <b>Batch Mode</b> or <b>Dynamic Mode</b>.</p>
|
|
252
|
+
<ul>
|
|
253
|
+
<li><b>Batch:</b> Send a JSON object to update multiple keys for the configured Provider: <code>{"key": value, "key2": value}</code></li>
|
|
254
|
+
<li><b>Dynamic:</b> Send a specific object to target ANY Provider/Key:
|
|
255
|
+
<code>
|
|
256
|
+
{
|
|
257
|
+
"provider": "u_os_sbm",
|
|
258
|
+
"key": "machine.outputs.DO01",
|
|
259
|
+
"value": true
|
|
260
|
+
}
|
|
261
|
+
</code>
|
|
262
|
+
</li>
|
|
263
|
+
<li><b>Mixed Array:</b> Send an array of dynamic objects to write to multiple providers at once:
|
|
264
|
+
<code>
|
|
265
|
+
[
|
|
266
|
+
{ "provider": "u_os_sbm", "key": "doo1", "value": true },
|
|
267
|
+
{ "provider": "other_provider", "key": "var2", "value": 123 }
|
|
268
|
+
]
|
|
269
|
+
</code>
|
|
270
|
+
</li>
|
|
271
|
+
</ul>
|
package/nodes/datahub-write.js
CHANGED
|
@@ -170,7 +170,8 @@ module.exports = function (RED) {
|
|
|
170
170
|
node.resolvedId = this.variableId;
|
|
171
171
|
// Log as debug implies we handle it gracefully => No User Warn
|
|
172
172
|
node.debug(`ID Resolution failed for '${this.variableKey}' (${err.message}). Using configured ID: ${this.variableId}`);
|
|
173
|
-
|
|
173
|
+
// User Request v1.3.48: Show Green 'ready' even if ID resolution failed (Dynamic Mode)
|
|
174
|
+
node.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
174
175
|
} else {
|
|
175
176
|
node.warn(`ID Resolution failed for '${this.variableKey}': ${err.message}`);
|
|
176
177
|
node.status({ fill: 'red', shape: 'dot', text: 'resolution failed' });
|
|
@@ -228,30 +229,40 @@ module.exports = function (RED) {
|
|
|
228
229
|
// Array: [{id:1, value:val}, {key:'k', value:val}]
|
|
229
230
|
items = rawPayload;
|
|
230
231
|
} else {
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
232
|
+
// Check for Single Dynamic Write Object
|
|
233
|
+
// Format: { provider: "p", key: "k", value: "v" }
|
|
234
|
+
if (rawPayload.provider || rawPayload.key || rawPayload.variableKey || rawPayload.variableId) {
|
|
235
|
+
items = [rawPayload];
|
|
236
|
+
} else {
|
|
237
|
+
// Object: { "key": val, "id:1": val }
|
|
238
|
+
items = Object.entries(rawPayload).map(([k, v]) => {
|
|
239
|
+
// Detect if key is actually an ID (e.g. "5" or "id:5")?
|
|
240
|
+
// Easier: treat all keys as Keys, unless user passes explicit Array.
|
|
241
|
+
const asInt = parseInt(k, 10);
|
|
242
|
+
if (!isNaN(asInt) && String(asInt) === k) {
|
|
243
|
+
return { id: asInt, value: v };
|
|
244
|
+
}
|
|
245
|
+
return { key: k, value: v };
|
|
246
|
+
});
|
|
247
|
+
}
|
|
241
248
|
}
|
|
242
249
|
|
|
243
250
|
// Process Items
|
|
244
251
|
for (const item of items) {
|
|
245
|
-
let targetId = item.id;
|
|
252
|
+
let targetId = item.id || item.variableId;
|
|
246
253
|
let targetType = item.dataType; // Optional explicit type
|
|
247
254
|
|
|
248
255
|
if (targetId === undefined) {
|
|
249
|
-
|
|
256
|
+
const effectiveKey = item.key || item.variableKey;
|
|
257
|
+
if (effectiveKey) {
|
|
250
258
|
// Resolve Key
|
|
251
259
|
try {
|
|
260
|
+
// Use Dynamic Provider if present, otherwise configured Provider
|
|
261
|
+
const effectiveProvider = item.provider || item.providerId || node.providerId;
|
|
262
|
+
|
|
252
263
|
// Use Centralized Cache in Config Node
|
|
253
264
|
if (typeof configNode.resolveVariableId === 'function') {
|
|
254
|
-
const resolved = await configNode.resolveVariableId(
|
|
265
|
+
const resolved = await configNode.resolveVariableId(effectiveProvider, effectiveKey);
|
|
255
266
|
targetId = resolved.id;
|
|
256
267
|
if (!targetType) targetType = resolved.dataType;
|
|
257
268
|
if (resolved.fingerprint) currentFingerprint = resolved.fingerprint;
|
|
@@ -259,7 +270,7 @@ module.exports = function (RED) {
|
|
|
259
270
|
throw new Error("Config Node too old");
|
|
260
271
|
}
|
|
261
272
|
} catch (e) {
|
|
262
|
-
node.warn(`Skipping key '${
|
|
273
|
+
node.warn(`Skipping key '${effectiveKey}': ${e.message}`);
|
|
263
274
|
continue;
|
|
264
275
|
}
|
|
265
276
|
} else {
|
package/nodes/uos-config.js
CHANGED
|
@@ -176,37 +176,51 @@ module.exports = function (RED) {
|
|
|
176
176
|
if (this.nc) {
|
|
177
177
|
return this.nc;
|
|
178
178
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
179
|
+
// Deduplication: Return existing promise if we are already connecting
|
|
180
|
+
if (this.connectionPromise) {
|
|
181
|
+
// this.log('Joining pending connection request...');
|
|
182
|
+
return this.connectionPromise;
|
|
183
|
+
}
|
|
181
184
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
185
|
+
this.connectionPromise = (async () => {
|
|
186
|
+
try {
|
|
187
|
+
// Ensure we have a valid token initially
|
|
188
|
+
await this.getToken();
|
|
189
|
+
|
|
190
|
+
this.nc = await connect({
|
|
191
|
+
servers: `nats://${this.host}:${this.port}`,
|
|
192
|
+
// Authenticator must be SYNCHRONOUS. We rely on background refresh to keep this.tokenInfo current.
|
|
193
|
+
// Token function must be SYNCHRONOUS if we rely on background refresh.
|
|
194
|
+
token: () => {
|
|
195
|
+
return this.tokenInfo ? this.tokenInfo.token : '';
|
|
196
|
+
},
|
|
197
|
+
// REVERT: Use Configured Client Name for Connection.
|
|
198
|
+
// Using UUID caused "Authorization Violation" for some users.
|
|
199
|
+
name: this.clientName,
|
|
200
|
+
inboxPrefix: `_INBOX.${this.clientName}`,
|
|
201
|
+
maxReconnectAttempts: -1, // Infinite reconnects
|
|
202
|
+
reconnectTimeWait: 2000,
|
|
203
|
+
});
|
|
197
204
|
|
|
198
|
-
|
|
199
|
-
this.authFailureTimestamp = 0;
|
|
205
|
+
this.log(`NATS connecting as Name: '${this.clientName}' (Dedup Active)`);
|
|
200
206
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
// Reset Failure timestamp on success
|
|
208
|
+
this.authFailureTimestamp = 0;
|
|
209
|
+
return this.nc;
|
|
210
|
+
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (e.message && (e.message.includes('Authorization') || e.message.includes('Permissions') || e.message.includes('Authentication'))) {
|
|
213
|
+
this.warn(`NATS Authorization failed. Invalidating token cache. Circuit Breaker active for 10s.`);
|
|
214
|
+
this.tokenInfo = null; // Force fresh token next time
|
|
215
|
+
this.authFailureTimestamp = Date.now(); // Start Cooldown
|
|
216
|
+
}
|
|
217
|
+
this.error(`NATS connect failed: ${e.message}`);
|
|
218
|
+
throw e;
|
|
219
|
+
} finally {
|
|
220
|
+
this.connectionPromise = null;
|
|
206
221
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
222
|
+
})();
|
|
223
|
+
return this.connectionPromise;
|
|
210
224
|
this.nc.closed().then(() => {
|
|
211
225
|
this.nc = null;
|
|
212
226
|
this.emit('disconnected');
|
|
@@ -545,8 +559,26 @@ module.exports = function (RED) {
|
|
|
545
559
|
}
|
|
546
560
|
};
|
|
547
561
|
|
|
548
|
-
this.on('close', (done) => {
|
|
549
|
-
|
|
562
|
+
this.on('close', async (done) => {
|
|
563
|
+
// Force Close Connection on Full Deploy (ignore reference count)
|
|
564
|
+
if (this.refreshTimer) {
|
|
565
|
+
clearTimeout(this.refreshTimer);
|
|
566
|
+
this.refreshTimer = null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (this.nc) {
|
|
570
|
+
const nc = this.nc;
|
|
571
|
+
this.nc = null;
|
|
572
|
+
this.users = 0;
|
|
573
|
+
try {
|
|
574
|
+
// Drain ensures all pending messages are sent before closing
|
|
575
|
+
await nc.drain();
|
|
576
|
+
// this.log('NATS Connection flushed and closed.');
|
|
577
|
+
} catch (err) {
|
|
578
|
+
this.warn(`Error closing NATS connection: ${err.message}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
done();
|
|
550
582
|
});
|
|
551
583
|
}
|
|
552
584
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.52",
|
|
4
4
|
"description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "IoTUeli",
|