node-red-contrib-uos-nats 1.3.55 → 1.3.68
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 +3 -3
- package/nodes/datahub-input.js +74 -118
- package/nodes/datahub-output.js +1 -1
- package/nodes/datahub-write.html +2 -2
- package/nodes/uos-config.js +17 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ Reads values from existing providers (like `u_os_adm`).
|
|
|
77
77
|
### DataHub - Write
|
|
78
78
|
Changes values in other providers.
|
|
79
79
|
- **Single Mode:** Select a variable from the list. Send `msg.payload` = value.
|
|
80
|
-
- **Batch Mode:** Select NO variable (clear selection). Send `msg.payload` as a JSON object: `{"var_key": value, "
|
|
80
|
+
- **Batch Mode:** Select NO variable (clear selection). Send `msg.payload` as a FLAT JSON object: `{"var_key": value, "machine.status": value}` (uses Configured Provider). **Nested objects are NOT supported** (keys must use dot-notation).
|
|
81
81
|
- **Dynamic Mode:** Send a full target object to write anywhere:
|
|
82
82
|
```json
|
|
83
83
|
{
|
|
@@ -87,8 +87,8 @@ Changes values in other providers.
|
|
|
87
87
|
}
|
|
88
88
|
```json
|
|
89
89
|
{
|
|
90
|
-
"provider": "
|
|
91
|
-
"key": "
|
|
90
|
+
"provider": "u_os_sbm",
|
|
91
|
+
"key": "ur20_8do_p_1.process_data.channel_7.do",
|
|
92
92
|
"value": 123
|
|
93
93
|
}
|
|
94
94
|
```
|
package/nodes/datahub-input.js
CHANGED
|
@@ -168,134 +168,100 @@ module.exports = function (RED) {
|
|
|
168
168
|
return; // Stop here, do not start Interval or Subscription
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
performSnapshot = async (
|
|
171
|
+
performSnapshot = async () => {
|
|
172
|
+
// Debugging connection state
|
|
172
173
|
if (!nc || nc.isClosed()) {
|
|
173
174
|
this.warn('Snapshot skipped: Connection is closed or not ready.');
|
|
174
175
|
return;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
186
|
-
|
|
187
|
-
if (!key) continue;
|
|
178
|
+
try {
|
|
179
|
+
// Resolve requested variable names to IDs
|
|
180
|
+
let targetIds = [];
|
|
181
|
+
let isWildcard = (this.variables.length === 0);
|
|
188
182
|
|
|
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);
|
|
204
183
|
if (!isWildcard) {
|
|
184
|
+
// Reverse lookup: Find Def by Key
|
|
205
185
|
const requestedKeys = new Set(this.variables);
|
|
186
|
+
|
|
206
187
|
for (const def of defMap.values()) {
|
|
207
|
-
if (requestedKeys.has(def.key))
|
|
188
|
+
if (requestedKeys.has(def.key)) {
|
|
189
|
+
targetIds.push(Number(def.id));
|
|
190
|
+
}
|
|
208
191
|
}
|
|
209
|
-
|
|
210
|
-
|
|
192
|
+
|
|
193
|
+
// CRITICAL FIX: If user requested specific variables but we found NONE,
|
|
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.`);
|
|
211
198
|
return;
|
|
212
199
|
}
|
|
213
200
|
}
|
|
214
|
-
requests.set(this.providerId, targetIds); // Empty Set = Wildcard
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (requests.size === 0) {
|
|
218
|
-
this.warn("Snapshot Aborted: No valid variables resolved.");
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
201
|
|
|
222
|
-
|
|
223
|
-
|
|
202
|
+
// If Wildcard (empty targetIds and isWildcard=true) -> Request ALL.
|
|
203
|
+
// If Specific (targetIds has items) -> Request specific.
|
|
204
|
+
// Use serialRequest via Config Node to prevent concurrency issues on the connection
|
|
205
|
+
// Simple Snapshot Logic using Config Node Semaphore
|
|
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
|
+
}
|
|
224
219
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
221
|
+
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
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
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
} catch (err) {
|
|
235
|
+
lastError = err;
|
|
236
|
+
// If semaphore is full or timeout, wait a bit
|
|
237
|
+
await new Promise(r => setTimeout(r, 1000 * attempt));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (lastError) {
|
|
241
|
+
const msg = lastError.message || '';
|
|
242
|
+
if (msg.includes('503') || msg.includes('no responders')) {
|
|
243
|
+
this.debug(`Snapshot skipped (Provider offline/503): ${msg}`);
|
|
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' });
|
|
235
251
|
} else {
|
|
236
|
-
|
|
252
|
+
this.warn(`Snapshot failed: ${msg}`);
|
|
253
|
+
this.status({ fill: 'red', shape: 'ring', text: 'snapshot error' });
|
|
237
254
|
}
|
|
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}`);
|
|
279
255
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const errStr = errors.join('; ');
|
|
287
|
-
if (errStr.includes('503') || errStr.includes('no responders')) {
|
|
288
|
-
this.status({ fill: 'green', shape: 'ring', text: 'waiting for provider' });
|
|
289
|
-
this.debug(`Snapshot waiting: ${errStr}`);
|
|
290
|
-
} else if (errStr.includes('Authorization') || errStr.includes('Permission')) {
|
|
291
|
-
this.status({ fill: 'yellow', shape: 'ring', text: 'auth failed' });
|
|
292
|
-
this.warn(`Snapshot auth failed: ${errStr}`);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
const msg = e.message || '';
|
|
258
|
+
if (msg.includes('503') || msg.includes('no responders')) {
|
|
259
|
+
this.debug(`Snapshot skipped (Provider offline/503): ${msg}`);
|
|
260
|
+
} else if (msg.includes('Cooldown')) {
|
|
261
|
+
// Ignore cooldown errors in outer catch
|
|
293
262
|
} else {
|
|
294
|
-
this.
|
|
295
|
-
this.warn(`Snapshot errors: ${errStr}`);
|
|
263
|
+
this.warn(`Snapshot failed: ${msg}`);
|
|
296
264
|
}
|
|
297
|
-
} else {
|
|
298
|
-
this.status({ fill: 'green', shape: 'ring', text: 'empty' });
|
|
299
265
|
}
|
|
300
266
|
};
|
|
301
267
|
|
|
@@ -368,17 +334,7 @@ module.exports = function (RED) {
|
|
|
368
334
|
};
|
|
369
335
|
|
|
370
336
|
this.on('input', (msg, send, done) => {
|
|
371
|
-
|
|
372
|
-
if (msg.payload) {
|
|
373
|
-
if (Array.isArray(msg.payload) && msg.payload.length > 0) {
|
|
374
|
-
overrideItems = msg.payload;
|
|
375
|
-
} else if (typeof msg.payload === 'string' || (typeof msg.payload === 'object' && !Buffer.isBuffer(msg.payload))) {
|
|
376
|
-
// Allow single item dynamic read (convenience)
|
|
377
|
-
overrideItems = [msg.payload];
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
performSnapshot(overrideItems)
|
|
337
|
+
performSnapshot()
|
|
382
338
|
.then(() => done())
|
|
383
339
|
.catch((err) => done(err));
|
|
384
340
|
});
|
package/nodes/datahub-output.js
CHANGED
|
@@ -154,7 +154,7 @@ module.exports = function (RED) {
|
|
|
154
154
|
const { payload, fingerprint: fp } = modPayloads.buildProviderDefinitionEvent(definitions);
|
|
155
155
|
const subject = modSubjects.providerDefinitionChanged(this.providerId);
|
|
156
156
|
|
|
157
|
-
console.log(`[DataHub Output] Publishing definition for Provider '${this.providerId}' to '${subject}'`);
|
|
157
|
+
// console.log(`[DataHub Output] Publishing definition for Provider '${this.providerId}' to '${subject}'`);
|
|
158
158
|
|
|
159
159
|
fingerprint = fp;
|
|
160
160
|
await nc.publish(subject, payload);
|
package/nodes/datahub-write.html
CHANGED
|
@@ -250,12 +250,12 @@
|
|
|
250
250
|
</ol>
|
|
251
251
|
<p>If no variable is selected, the node works in <b>Batch Mode</b> or <b>Dynamic Mode</b>.</p>
|
|
252
252
|
<ul>
|
|
253
|
-
<li><b>Batch:</b> Send a JSON object to update multiple keys for the configured Provider: <code>{"key": value, "
|
|
253
|
+
<li><b>Batch:</b> Send a FLAT JSON object to update multiple keys for the configured Provider: <code>{"key": value, "machine.status": value}</code>. <br><b>Note:</b> Nested objects are NOT flattened automatically. Use dot-notation for keys!</li>
|
|
254
254
|
<li><b>Dynamic:</b> Send a specific object to target ANY Provider/Key:
|
|
255
255
|
<code>
|
|
256
256
|
{
|
|
257
257
|
"provider": "u_os_sbm",
|
|
258
|
-
"key": "
|
|
258
|
+
"key": "ur20_8do_p_1.process_data.channel_7.do",
|
|
259
259
|
"value": true
|
|
260
260
|
}
|
|
261
261
|
</code>
|
package/nodes/uos-config.js
CHANGED
|
@@ -187,6 +187,9 @@ module.exports = function (RED) {
|
|
|
187
187
|
// Ensure we have a valid token initially
|
|
188
188
|
await this.getToken();
|
|
189
189
|
|
|
190
|
+
// Sanitize clientName for Inbox usage (Strict NATS subjects)
|
|
191
|
+
const safeClientName = this.clientName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
192
|
+
|
|
190
193
|
this.nc = await connect({
|
|
191
194
|
servers: `nats://${this.host}:${this.port}`,
|
|
192
195
|
// Authenticator must be SYNCHRONOUS. We rely on background refresh to keep this.tokenInfo current.
|
|
@@ -197,7 +200,10 @@ module.exports = function (RED) {
|
|
|
197
200
|
// REVERT: Use Configured Client Name for Connection.
|
|
198
201
|
// Using UUID caused "Authorization Violation" for some users.
|
|
199
202
|
name: this.clientName,
|
|
200
|
-
|
|
203
|
+
|
|
204
|
+
// inboxPrefix: `_INBOX.${safeClientName}`, // RESTORED: Spec requires strictly this format (ACLs enforce it).
|
|
205
|
+
// Using safeClientName avoids invalid subject errors.
|
|
206
|
+
inboxPrefix: `_INBOX.${safeClientName}`,
|
|
201
207
|
maxReconnectAttempts: -1, // Infinite reconnects
|
|
202
208
|
reconnectTimeWait: 2000,
|
|
203
209
|
});
|
|
@@ -457,10 +463,18 @@ module.exports = function (RED) {
|
|
|
457
463
|
if (!def || !def.variables) return null;
|
|
458
464
|
|
|
459
465
|
const v = def.variables.find(v => v.key === variableKey);
|
|
460
|
-
if (v)
|
|
466
|
+
if (v) {
|
|
467
|
+
return {
|
|
468
|
+
id: v.id,
|
|
469
|
+
fingerprint: def.fingerprint || BigInt(0),
|
|
470
|
+
dataType: v.dataType
|
|
471
|
+
};
|
|
472
|
+
}
|
|
461
473
|
|
|
462
474
|
// Try explicit numeric string fallback
|
|
463
|
-
if (!isNaN(variableKey))
|
|
475
|
+
if (!isNaN(variableKey)) {
|
|
476
|
+
return { id: parseInt(variableKey), fingerprint: BigInt(0) }; // Fallback, no fingerprint known
|
|
477
|
+
}
|
|
464
478
|
|
|
465
479
|
return null;
|
|
466
480
|
} catch (e) {
|
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.68",
|
|
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",
|