node-red-contrib-uos-nats 1.3.46 → 1.3.55
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 +28 -7
- package/lib/payloads.js +8 -2
- package/nodes/datahub-input.html +4 -0
- package/nodes/datahub-input.js +118 -74
- package/nodes/datahub-write.html +21 -2
- package/nodes/datahub-write.js +26 -15
- package/nodes/uos-config.html +7 -0
- package/nodes/uos-config.js +20 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
**Unofficial Node-RED Package for Weidmüller u-OS Data Hub**
|
|
3
2
|
|
|
4
3
|
Read, write, and provide variables via NATS protocol using **OAuth2 authentication**.
|
|
5
4
|
Optimized for high performance and real-time updates.
|
|
6
5
|
|
|
6
|
+
> **IMPORTANT:**
|
|
7
|
+
> These nodes **MUST** run directly on the **u-OS device** (e.g. as a simplified App or Snap).
|
|
8
|
+
> The system's NATS server is **NOT accessible from the outside** (blocked by firewall/binding).
|
|
9
|
+
> You cannot use this package from a remote Node-RED instance (e.g. on your Laptop) to connect to the device.
|
|
10
|
+
|
|
7
11
|
Maintained by [IoTUeli](https://iotueli.ch). Source: [GitHub](https://github.com/uiff/nats-NodeRed-Node-uc20)
|
|
8
12
|
|
|
9
13
|
---
|
|
@@ -68,12 +72,28 @@ Reads values from existing providers (like `u_os_adm`).
|
|
|
68
72
|
- **Provider ID:** Name of the source provider.
|
|
69
73
|
- **Variables:** Use **Load Variables** to browse and select variables.
|
|
70
74
|
- **Trigger:** "Event" (instant update) or "Poll" (interval).
|
|
75
|
+
- **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
76
|
|
|
72
77
|
### DataHub - Write
|
|
73
78
|
Changes values in other providers.
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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).
|
|
81
|
+
- **Dynamic Mode:** Send a full target object to write anywhere:
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"provider": "target_provider_id",
|
|
85
|
+
"key": "variable_key",
|
|
86
|
+
"value": 123
|
|
87
|
+
}
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"provider": "target_provider_id",
|
|
91
|
+
"key": "variable_key",
|
|
92
|
+
"value": 123
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
Or send an **Array** of these objects to write to multiple providers in one go.
|
|
96
|
+
- **Strict Mode:** Automatically handles Fingerprints for strict providers (e.g. `u_os_sbm`).
|
|
77
97
|
|
|
78
98
|
### DataHub - Provider
|
|
79
99
|
Publishes your own data to the Data Hub.
|
|
@@ -92,10 +112,11 @@ Publishes your own data to the Data Hub.
|
|
|
92
112
|
## Troubleshooting
|
|
93
113
|
|
|
94
114
|
- **Provider not visible?** Ensure **Provider ID** matches your **Client ID**. Easiest way: Leave Provider ID empty in the node.
|
|
115
|
+
- **Node Status is Green (Ring)?**
|
|
116
|
+
- `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
117
|
- **Node Status is Yellow?**
|
|
96
|
-
- `cooldown (10s)`: The node is
|
|
97
|
-
- `
|
|
98
|
-
- `auth failed`: Check your OAuth Client Secret and Scopes.
|
|
118
|
+
- `cooldown (10s)`: The node is pausing after an error to protect the network.
|
|
119
|
+
- `auth failed`: OAuth credentials generated an error. Check Client Secret.
|
|
99
120
|
- **Node Status is Red?**
|
|
100
121
|
- `illegal ID`: You used a reserved name like `u_os_sbm`. Rename your Client/Provider.
|
|
101
122
|
- `write error`: A command failed. Check Scopes (`hub.variables.readwrite`) or Fingerprint.
|
package/lib/payloads.js
CHANGED
|
@@ -79,8 +79,14 @@ export function buildReadVariablesQuery(ids) {
|
|
|
79
79
|
const builder = new flatbuffers.Builder(128);
|
|
80
80
|
let idsOffset = 0;
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// Safety: Filter invalid IDs to prevent crashes in C++ provider
|
|
83
|
+
// FlatBuffers requires strict types.
|
|
84
|
+
const validIds = (ids || [])
|
|
85
|
+
.map(id => Number(id))
|
|
86
|
+
.filter(id => Number.isInteger(id) && id >= 0);
|
|
87
|
+
|
|
88
|
+
if (validIds.length > 0) {
|
|
89
|
+
idsOffset = ReadVariablesQueryRequest.createIdsVector(builder, validIds);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
ReadVariablesQueryRequest.startReadVariablesQueryRequest(builder);
|
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,100 +168,134 @@ 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
|
}
|
|
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}`);
|
|
255
279
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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}`);
|
|
262
293
|
} else {
|
|
263
|
-
this.
|
|
294
|
+
this.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
295
|
+
this.warn(`Snapshot errors: ${errStr}`);
|
|
264
296
|
}
|
|
297
|
+
} else {
|
|
298
|
+
this.status({ fill: 'green', shape: 'ring', text: 'empty' });
|
|
265
299
|
}
|
|
266
300
|
};
|
|
267
301
|
|
|
@@ -334,7 +368,17 @@ module.exports = function (RED) {
|
|
|
334
368
|
};
|
|
335
369
|
|
|
336
370
|
this.on('input', (msg, send, done) => {
|
|
337
|
-
|
|
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)
|
|
338
382
|
.then(() => done())
|
|
339
383
|
.catch((err) => done(err));
|
|
340
384
|
});
|
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.html
CHANGED
|
@@ -230,6 +230,13 @@
|
|
|
230
230
|
|
|
231
231
|
<h3>Troubleshooting</h3>
|
|
232
232
|
|
|
233
|
+
<p style="color:var(--red-ui-text-color-error); font-weight:bold; border:1px solid var(--red-ui-text-color-error); padding:5px; border-radius:3px;">
|
|
234
|
+
⚠️ IMPORTANT:<br>
|
|
235
|
+
These nodes MUST run directly on the u-OS device (App/Snap).
|
|
236
|
+
The NATS server is NOT accessible from the outside.
|
|
237
|
+
You cannot use this node from a remote instance (e.g. Laptop).
|
|
238
|
+
</p>
|
|
239
|
+
|
|
233
240
|
<h4>Test Connection fails</h4>
|
|
234
241
|
<ul>
|
|
235
242
|
<li>Check Host/Port are correct and device is reachable</li>
|
package/nodes/uos-config.js
CHANGED
|
@@ -559,8 +559,26 @@ module.exports = function (RED) {
|
|
|
559
559
|
}
|
|
560
560
|
};
|
|
561
561
|
|
|
562
|
-
this.on('close', (done) => {
|
|
563
|
-
|
|
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();
|
|
564
582
|
});
|
|
565
583
|
}
|
|
566
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.55",
|
|
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",
|