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 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, "var2": value}` (uses Configured Provider).
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": "target_provider_id",
91
- "key": "variable_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
  ```
@@ -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 (overrideItems) => {
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
- // 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
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)) targetIds.add(def.id);
188
+ if (requestedKeys.has(def.key)) {
189
+ targetIds.push(Number(def.id));
190
+ }
208
191
  }
209
- if (targetIds.size === 0) {
210
- this.warn(`Snapshot Aborted: None of the configured variables could be resolved.`);
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
- const allResults = [];
223
- const errors = [];
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
- // 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);
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
- // Request
232
- let snapshotMsg;
233
- if (typeof connection.serialRequest === 'function') {
234
- snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(pId), payloads.buildReadVariablesQuery(targetIdsArr), { timeout: 10000 });
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
- snapshotMsg = await nc.request(subjects.readVariablesQuery(pId), payloads.buildReadVariablesQuery(targetIdsArr), { timeout: 10000 });
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
- 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
- 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.status({ fill: 'red', shape: 'ring', text: 'error' });
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
- let overrideItems = null;
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
  });
@@ -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);
@@ -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, "key2": value}</code></li>
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": "machine.outputs.DO01",
258
+ "key": "ur20_8do_p_1.process_data.channel_7.do",
259
259
  "value": true
260
260
  }
261
261
  </code>
@@ -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
- inboxPrefix: `_INBOX.${this.clientName}`,
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) return v.id;
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)) return parseInt(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.55",
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",