node-red-contrib-uos-nats 1.1.0 → 1.2.2
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 +6 -1
- package/nodes/datahub-input.js +11 -2
- package/nodes/datahub-output.html +13 -0
- package/nodes/datahub-output.js +36 -3
- package/nodes/uos-config.js +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,7 +80,12 @@ Publishes your own data to the Data Hub.
|
|
|
80
80
|
- **Provider ID:** Leave empty to use your Client ID (Recommended).
|
|
81
81
|
- **Input:** Send a JSON object: `{ "machine": { "status": "active" } }`.
|
|
82
82
|
- **Auto-Discovery:** Automatically creates variable definitions based on your JSON structure.
|
|
83
|
-
- **
|
|
83
|
+
- **Bi-Directional:** Enable "Allow external writes" to let other apps write to your variables. Changes are emitted continuously on the **2nd Output**.
|
|
84
|
+
- **Keep-Alive:** Configurable interval (Default: 300s / 5min).
|
|
85
|
+
- **Quality & Timestamp:** Override metadata by sending an object:
|
|
86
|
+
```json
|
|
87
|
+
{ "temp": { "value": 23.5, "quality": "BAD", "timestamp": 1712000000 } }
|
|
88
|
+
```
|
|
84
89
|
|
|
85
90
|
---
|
|
86
91
|
|
package/nodes/datahub-input.js
CHANGED
|
@@ -214,7 +214,14 @@ module.exports = function (RED) {
|
|
|
214
214
|
|
|
215
215
|
// If Wildcard (empty targetIds and isWildcard=true) -> Request ALL.
|
|
216
216
|
// If Specific (targetIds has items) -> Request specific.
|
|
217
|
-
|
|
217
|
+
// Use serialRequest via Config Node to prevent concurrency issues on the connection
|
|
218
|
+
let snapshotMsg;
|
|
219
|
+
if (typeof connection.serialRequest === 'function') {
|
|
220
|
+
snapshotMsg = await connection.serialRequest(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 5000 });
|
|
221
|
+
} else {
|
|
222
|
+
// Fallback for older config nodes (should not happen if package updated correctly)
|
|
223
|
+
snapshotMsg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 5000 });
|
|
224
|
+
}
|
|
218
225
|
|
|
219
226
|
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
220
227
|
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
@@ -239,7 +246,9 @@ module.exports = function (RED) {
|
|
|
239
246
|
|
|
240
247
|
|
|
241
248
|
|
|
242
|
-
// Initial snapshot
|
|
249
|
+
// Initial snapshot with random jitter into prevent concurrency overload
|
|
250
|
+
// (If multiple nodes start simultaneously)
|
|
251
|
+
await new Promise(r => setTimeout(r, Math.floor(Math.random() * 500) + 100));
|
|
243
252
|
await performSnapshot();
|
|
244
253
|
|
|
245
254
|
// Setup polling if configured
|
|
@@ -156,4 +156,17 @@
|
|
|
156
156
|
}
|
|
157
157
|
}</pre>
|
|
158
158
|
<p>becomes variables <code>machine.status</code> and <code>machine.details.temp</code> in the Data Hub.</p>
|
|
159
|
+
|
|
160
|
+
<h3>Advanced: Manual Quality & Timestamp</h3>
|
|
161
|
+
<p>You can override the quality and timestamp by sending a value object instead of a raw value:</p>
|
|
162
|
+
<pre>{
|
|
163
|
+
"machine": {
|
|
164
|
+
"temp": {
|
|
165
|
+
"value": 45.2,
|
|
166
|
+
"quality": "BAD",
|
|
167
|
+
"timestamp": 1712000000000
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}</pre>
|
|
171
|
+
<p>Supported Qualities: <code>GOOD</code>, <code>BAD</code>, <code>UNCERTAIN</code>.</p>
|
|
159
172
|
</script>
|
package/nodes/datahub-output.js
CHANGED
|
@@ -33,7 +33,20 @@ const defaultValue = (type) => {
|
|
|
33
33
|
const flattenPayload = (value, prefix = '') => {
|
|
34
34
|
const entries = [];
|
|
35
35
|
const path = (key) => (prefix ? `${prefix}.${key}` : key);
|
|
36
|
+
|
|
37
|
+
// Extended Value Object Detection:
|
|
38
|
+
// If object has 'value' AND ('quality' OR 'timestamp'), treat as single leaf with metadata.
|
|
36
39
|
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
40
|
+
if ('value' in value && ('quality' in value || 'timestamp' in value)) {
|
|
41
|
+
entries.push({
|
|
42
|
+
key: prefix || 'value', // If root is the object
|
|
43
|
+
value: value.value,
|
|
44
|
+
quality: value.quality,
|
|
45
|
+
timestamp: value.timestamp
|
|
46
|
+
});
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
Object.entries(value).forEach(([key, val]) => {
|
|
38
51
|
if (val !== undefined) {
|
|
39
52
|
entries.push(...flattenPayload(val, path(key)));
|
|
@@ -363,7 +376,8 @@ module.exports = function (RED) {
|
|
|
363
376
|
|
|
364
377
|
let definitionsChanged = false;
|
|
365
378
|
|
|
366
|
-
entries.forEach((
|
|
379
|
+
entries.forEach((entry) => {
|
|
380
|
+
const { key, value } = entry;
|
|
367
381
|
// Ensure we don't accidentally send undefined/null as value if logic slipped through
|
|
368
382
|
if (value === undefined || value === null) return;
|
|
369
383
|
|
|
@@ -371,11 +385,30 @@ module.exports = function (RED) {
|
|
|
371
385
|
if (created) {
|
|
372
386
|
definitionsChanged = true;
|
|
373
387
|
}
|
|
388
|
+
|
|
389
|
+
// Handle custom timestamp
|
|
390
|
+
let ts = BigInt(Date.now()) * 1_000_000n;
|
|
391
|
+
if (entry.timestamp) {
|
|
392
|
+
try {
|
|
393
|
+
// Support Date object or number (ms)
|
|
394
|
+
const ms = (entry.timestamp instanceof Date) ? entry.timestamp.getTime() : Number(entry.timestamp);
|
|
395
|
+
if (!isNaN(ms)) {
|
|
396
|
+
ts = BigInt(Math.floor(ms)) * 1_000_000n;
|
|
397
|
+
}
|
|
398
|
+
} catch (e) { /* ignore */ }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Handle custom quality
|
|
402
|
+
let qual = 'GOOD';
|
|
403
|
+
if (entry.quality) {
|
|
404
|
+
qual = String(entry.quality).toUpperCase();
|
|
405
|
+
}
|
|
406
|
+
|
|
374
407
|
const state = {
|
|
375
408
|
id: def.id,
|
|
376
409
|
value,
|
|
377
|
-
timestamp:
|
|
378
|
-
quality:
|
|
410
|
+
timestamp: ts,
|
|
411
|
+
quality: qual,
|
|
379
412
|
};
|
|
380
413
|
// states.push(state); // No longer pushing to a temporary 'states' array
|
|
381
414
|
stateMap.set(def.id, state); // Update the global stateMap
|
package/nodes/uos-config.js
CHANGED
|
@@ -175,6 +175,31 @@ module.exports = function (RED) {
|
|
|
175
175
|
return this.nc;
|
|
176
176
|
};
|
|
177
177
|
|
|
178
|
+
this.requestQueue = Promise.resolve();
|
|
179
|
+
|
|
180
|
+
this.serialRequest = async (subject, payload, options = {}) => {
|
|
181
|
+
// Enqueue request to run sequentially
|
|
182
|
+
const result = this.requestQueue.then(async () => {
|
|
183
|
+
const nc = await this.ensureConnection();
|
|
184
|
+
return nc.request(subject, payload, options);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Ensure queue does not block on failures (catch individually inside the chain logic)
|
|
188
|
+
// Actually, chaining the result directly makes the next one wait for result resolution.
|
|
189
|
+
// We want to update queue pointer to catch errors so next request still runs.
|
|
190
|
+
this.requestQueue = result.catch(() => { });
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @deprecated Use serialRequest for concurrency safety
|
|
197
|
+
*/
|
|
198
|
+
this.acquire = async () => {
|
|
199
|
+
this.users += 1;
|
|
200
|
+
return this.ensureConnection();
|
|
201
|
+
};
|
|
202
|
+
|
|
178
203
|
this.getGrantedScopes = async () => {
|
|
179
204
|
await this.getToken();
|
|
180
205
|
return this.tokenInfo?.grantedScope || '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
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",
|