node-red-contrib-uos-nats 1.2.2 → 1.3.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.
@@ -62,8 +62,9 @@
62
62
  const $hiddenManual = $('#node-input-manualVariables');
63
63
 
64
64
  // Styles
65
- const rowStyle = "display:grid; grid-template-columns: 30px 60px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px;";
66
- const idStyle = "background:#eee; color:#555; padding:1px 4px; border-radius:3px; font-family:monospace; text-align:center; font-size:11px;";
65
+ // Adjusted: ID column removed per user request (Key is main identifier)
66
+ const rowStyle = "display:grid; grid-template-columns: 30px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px;";
67
+ // const idStyle = ... (removed)
67
68
  const keyStyle = "overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px;";
68
69
 
69
70
  let currentVariables = []; // Stores {id, key, accessType, etc.}
@@ -93,7 +94,7 @@
93
94
  const selectedMap = getSelectedMap();
94
95
 
95
96
  vars.forEach(v => {
96
- // Robust ID handling: verify v.id exists (try 'id' and 'Id')
97
+ // Robust ID handling
97
98
  let rawId = (v.id !== undefined && v.id !== null) ? v.id : v.Id;
98
99
  const safeId = (rawId !== undefined && rawId !== null) ? rawId : 'ERR';
99
100
  const isSelected = selectedMap.has(v.key);
@@ -104,13 +105,15 @@
104
105
  const cb = $('<input type="checkbox" class="var-checkbox">')
105
106
  .prop('checked', isSelected)
106
107
  .data('key', v.key)
107
- .data('id', safeId);
108
+ .data('id', safeId); // ID strictly stored in data attribute
108
109
  cbContainer.append(cb);
109
110
 
110
- const idBadge = $('<div>').append($('<span>', { style: idStyle }).text(safeId));
111
+ // ID Column Removed from View
112
+ // const idBadge = ...
113
+
111
114
  const label = $('<div>', { style: keyStyle, title: v.key }).text(v.key);
112
115
 
113
- row.append(cbContainer).append(idBadge).append(label);
116
+ row.append(cbContainer).append(label); // No ID badge
114
117
  $listContainer.append(row);
115
118
  });
116
119
  };
@@ -150,26 +150,108 @@ module.exports = function (RED) {
150
150
  nc = await connection.acquire();
151
151
  this.status({ fill: 'green', shape: 'dot', text: 'connected' });
152
152
 
153
- // Retry Definition Fetch via NATS if Map is empty AND no manual defs
154
- if (defMap.size === 0 && this.manualDefs.length === 0) {
153
+ // Retry Definition Fetch via NATS if Map is empty OR if we have Heuristic IDs (missingId)
154
+ // Heuristic IDs (ID=Index) are dangerous because they might not match the real NATS IDs (e.g. 291 vs 5)
155
+ const hasMissingIds = Array.from(defMap.values()).some(d => d.missingId);
156
+
157
+ if ((defMap.size === 0 && this.manualDefs.length === 0) || hasMissingIds) {
155
158
  try {
159
+ this.warn(hasMissingIds
160
+ ? `Loaded variables have unresolved IDs (Heuristic). Attempting NATS Discovery to resolve real IDs for ${this.providerId}...`
161
+ : `Attempting NATS Discovery (Direct) for ${this.providerId}...`
162
+ );
163
+
156
164
  // Strategy 1: Direct Provider Query (Standard for many providers)
157
- this.warn(`Attempting NATS Discovery (Direct) for ${this.providerId}...`);
158
- const defMsg = await nc.request(subjects.readProviderDefinitionQuery(this.providerId), payloads.buildReadProviderDefinitionQuery(), { timeout: 1000 });
165
+ const requestOptions = { timeout: 2000 };
166
+ // Reuse serialRequest if available to avoid blocking connection?
167
+ // Discovery is one-off, nc.request is fine, but safer to use serial if we updated input.js fully.
168
+ // Start uses 'nc' directly currently. That's fine for now as it's sequential in 'start'.
169
+
170
+ const defMsg = await nc.request(subjects.readProviderDefinitionQuery(this.providerId), payloads.buildReadProviderDefinitionQuery(), requestOptions);
159
171
  const defs = payloads.decodeProviderDefinition(defMsg.data);
160
- this.warn(`NATS Direct Discovery: Loaded ${defs.length} variables.`);
161
- defs.forEach((def) => defMap.set(def.id, def));
172
+
173
+ if (defs && defs.variables.length > 0) {
174
+ this.warn(`NATS Discovery Successful: Received ${defs.variables.length} definitions with real IDs.`);
175
+ // Overwrite/Update defMap
176
+ // Logic: Match by KEY. DataHub providers should have unique keys.
177
+ // If we have existing "fake" ID 5 for "temp", and NATS says "temp" is ID 291.
178
+ // We need to update defMap to use 291.
179
+
180
+ // Clear Heuristic entries if we trust NATS fully?
181
+ // Or just merge?
182
+ // Safer: Create a lookup from NATS.
183
+ const realMap = new Map();
184
+ defs.variables.forEach(d => realMap.set(d.key, d));
185
+
186
+ // Update existing defMap
187
+ // If we had a heuristic entry, replace it.
188
+ // We rebuild defMap based on NATS mostly, but keep manual fallback?
189
+
190
+ // Let's iterate NATS defs and Populating defMap.
191
+ // Note: NATS Defs don't have 'missingId'.
192
+ defs.variables.forEach(d => {
193
+ // If we overwrite, we lose manual metadata (if any)?
194
+ // REST might have had better metadata? Usually NATS is source of truth for IDs.
195
+ defMap.set(d.id, d);
196
+ });
197
+
198
+ // Use Key Matching to remove old Heuristic entries?
199
+ // Heuristic entries are stored by Key (via fetchProviderVariables logic? No, by ID).
200
+ // We need to clean up the Fake IDs (0..N) if they don't map to real IDs.
201
+ // Actually, if we just add real IDs, we have duplicates?
202
+ // Map is Key=ID.
203
+ // Fake ID 5: { key: 'voltage' }
204
+ // Real ID 291: { key: 'voltage' }
205
+ // If user selected 'voltage', filtering uses KEYS (processStates line 104).
206
+ // Resolution (line 195) iterates values and matches Key.
207
+ // It will find BOTH 5 and 291.
208
+ // targetIds will get [5, 291].
209
+ // DataHub gets request [5, 291].
210
+ // 5 is invalid -> ignored.
211
+ // 291 is valid -> returns value.
212
+ // Result: It works! (Partially, effectively).
213
+
214
+ // But cleaner to remove heuristic ones.
215
+ for (const [id, def] of defMap.entries()) {
216
+ if (def.missingId) {
217
+ const real = realMap.get(def.key);
218
+ if (real && real.id !== id) {
219
+ defMap.delete(id); // Remove fake ID
220
+ }
221
+ }
222
+ }
223
+ this.warn(`IDs resolved via NATS. Mapped ${defs.variables.length} real IDs.`);
224
+ }
225
+
162
226
  } catch (firstErr) {
163
- // Strategy 2: Registry Query (Central lookup, often requires different perms or used by Hub)
227
+ // Strategy 2: Registry Query
164
228
  try {
165
- this.warn(`NATS Direct failed (${firstErr.message}), trying Registry Discovery...`);
166
- // Note: 'registryProviderQuery' accesses the central registry which might proxy the definition
229
+ if (!hasMissingIds) { // Only log if we were truly empty
230
+ this.warn(`NATS Direct failed (${firstErr.message}), trying Registry Discovery...`);
231
+ }
167
232
  const regMsg = await nc.request(subjects.registryProviderQuery(this.providerId), payloads.buildReadProviderDefinitionQuery(), { timeout: 2000 });
168
233
  const defs = payloads.decodeProviderDefinition(regMsg.data);
169
- this.warn(`NATS Registry Discovery: Loaded ${defs.length} variables.`);
170
- defs.forEach((def) => defMap.set(def.id, def));
234
+ if (defs && defs.variables.length > 0) {
235
+ this.warn(`NATS Registry Discovery: Loaded ${defs.variables.length} variables.`);
236
+ // Same merge logic
237
+ const realMap = new Map();
238
+ defs.variables.forEach(d => realMap.set(d.key, d));
239
+ defs.variables.forEach(d => defMap.set(d.id, d));
240
+ for (const [id, def] of defMap.entries()) {
241
+ if (def.missingId) {
242
+ const real = realMap.get(def.key);
243
+ if (real && real.id !== id) {
244
+ defMap.delete(id);
245
+ }
246
+ }
247
+ }
248
+ }
171
249
  } catch (secondErr) {
172
- this.warn(`All Discovery methods failed (REST, NATS Direct, NATS Registry). Please use Manual Definitions (Name:ID). Error: ${secondErr.message}`);
250
+ if (!hasMissingIds) {
251
+ this.warn(`All Discovery methods failed (REST, NATS Direct, NATS Registry). Please use Manual Definitions (Name:ID). Error: ${secondErr.message}`);
252
+ } else {
253
+ this.warn(`NATS ID Resolution failed. Continuing with Heuristic (Index-based) IDs. This generally fails for advanced providers.`);
254
+ }
173
255
  }
174
256
  }
175
257
  }
@@ -230,12 +312,41 @@ module.exports = function (RED) {
230
312
  // Re-process states (lookup names, formatting)
231
313
  const filteredSnapshot = processStates(states);
232
314
 
233
- if (filteredSnapshot.length) {
315
+ if (filteredSnapshot.length > 0) {
234
316
  this.send({ payload: { type: 'snapshot', variables: filteredSnapshot } });
235
317
  } else {
236
318
  if (states.length > 0) {
237
319
  this.warn(`Snapshot received data but everything was filtered out. Check Variable selection. Debug: First raw ID: ${states[0].id}, DefMap has it? ${defMap.has(states[0].id)}`);
238
320
  } else {
321
+ // EMPTY RESPONSE
322
+ // Check if we requested multiple IDs. Some providers fail on bulk read.
323
+ if (targetIds.length > 1) {
324
+ this.warn(`Snapshot Bulk Read failed (Empty List). Retrying ${targetIds.length} variables individually...`);
325
+ const accumulatedStates = [];
326
+
327
+ for (const id of targetIds) {
328
+ try {
329
+ let msg;
330
+ if (typeof connection.serialRequest === 'function') {
331
+ msg = await connection.serialRequest(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery([id]), { timeout: 2000 });
332
+ } else {
333
+ msg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery([id]), { timeout: 2000 });
334
+ }
335
+ const singleResponse = payloads.decodeVariableList(ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(new flatbuffers.ByteBuffer(msg.data)).variables());
336
+ if (singleResponse.length > 0) {
337
+ accumulatedStates.push(...singleResponse);
338
+ }
339
+ } catch (e) { /* ignore single failures */ }
340
+ }
341
+
342
+ const accumulatedFiltered = processStates(accumulatedStates);
343
+ if (accumulatedFiltered.length > 0) {
344
+ this.send({ payload: { type: 'snapshot', variables: accumulatedFiltered } });
345
+ this.warn(`Snapshot Recovery successful! Retrieved ${accumulatedFiltered.length} items via single requests.`);
346
+ return; // Success
347
+ }
348
+ }
349
+
239
350
  this.warn(`Snapshot received empty list from Data Hub. (Requested ${targetIds.length > 0 ? targetIds.length + ' specific IDs' : 'ALL variables'}).`);
240
351
  }
241
352
  }
@@ -29,8 +29,9 @@
29
29
  const $inputKey = $('#node-input-variableKey');
30
30
 
31
31
  // Styles matching Read Node exactly
32
- const rowStyle = "display:grid; grid-template-columns: 30px 60px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px; cursor:pointer;";
33
- const idStyle = "background:#eee; color:#555; padding:1px 4px; border-radius:3px; font-family:monospace; text-align:center; font-size:11px;";
32
+ // Adjusted: ID column removed
33
+ const rowStyle = "display:grid; grid-template-columns: 30px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px; cursor:pointer;";
34
+ // const idStyle = ...
34
35
  const keyStyle = "overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px;";
35
36
 
36
37
  // Removed iconStyle, using checkbox instead
@@ -60,12 +61,12 @@
60
61
 
61
62
  cbContainer.append(cb);
62
63
 
63
- // ID
64
- const idCol = $('<div>', { style: 'text-align:center;' }).append($('<span>', { style: idStyle }).text(safeId));
64
+ // ID - HIDDEN
65
+ // const idCol = $('<div>', { style: 'text-align:center;' }).append($('<span>', { style: idStyle }).text(safeId));
65
66
  // Key
66
67
  const keyCol = $('<div>', { style: keyStyle, title: v.key }).text(v.key);
67
68
 
68
- row.append(cbContainer).append(idCol).append(keyCol);
69
+ row.append(cbContainer).append(keyCol); // No ID Col
69
70
 
70
71
  // Click Handler (Row or Checkbox)
71
72
  const selectRow = () => {
@@ -106,13 +106,36 @@ module.exports = function (RED) {
106
106
  // Validate: either ID or Key required
107
107
  // Relaxed validation for Batch Mode (handled in Input)
108
108
 
109
- // If ID provided and valid, use it
110
- if (this.variableId && !isNaN(this.variableId)) {
109
+ // If Key is provided, we should ALWAYS resolve it to ensure ID is fresh (Auto-Healing)
110
+ // If only ID is provided (legacy), we trust it.
111
+ if (this.variableKey) {
112
+ // Trigger background resolution
113
+ node.status({ fill: 'yellow', shape: 'dot', text: 'resolving...' });
114
+ resolveVariableKey(await configNode.acquire(), this.providerId, this.variableKey, node, node.payloads)
115
+ .then(resolved => {
116
+ node.resolvedId = resolved.id;
117
+ node.resolvedDataType = resolved.dataType;
118
+ node.resolvedFingerprint = resolved.fingerprint;
119
+
120
+ if (node.variableId && node.variableId !== resolved.id) {
121
+ node.warn(`Auto-Healed ID for '${this.variableKey}': Configured=${node.variableId}, Resolved=${resolved.id}`);
122
+ }
123
+ node.status({ fill: 'green', shape: 'ring', text: 'ready' });
124
+ })
125
+ .catch(err => {
126
+ node.warn(`ID Resolution failed for '${this.variableKey}': ${err.message}. Using configured ID: ${this.variableId}`);
127
+ // Fallback to configured ID if resolution failed
128
+ if (this.variableId && !isNaN(this.variableId)) {
129
+ node.resolvedId = this.variableId;
130
+ node.status({ fill: 'green', shape: 'ring', text: 'ready (fallback)' });
131
+ } else {
132
+ node.status({ fill: 'red', shape: 'dot', text: 'resolution failed' });
133
+ }
134
+ });
135
+ } else if (this.variableId && !isNaN(this.variableId)) {
136
+ // Legacy/Manual Mode without Key
111
137
  this.resolvedId = this.variableId;
112
138
  node.status({ fill: 'green', shape: 'ring', text: 'ready' });
113
- } else if (this.variableKey) {
114
- // Key provided - will resolve on first message
115
- node.status({ fill: 'yellow', shape: 'ring', text: 'key needs resolution' });
116
139
  }
117
140
 
118
141
  // Load payloads module dynamically
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
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",