node-red-contrib-uos-nats 0.2.99 → 0.2.101
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/lib/payloads.js +3 -4
- package/nodes/datahub-write.js +62 -28
- package/package.json +1 -1
package/lib/payloads.js
CHANGED
|
@@ -110,12 +110,11 @@ export function parseRegistryStateEvent(buffer) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// Encode write variables command
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// Encode write variables command
|
|
114
|
+
export function encodeWriteVariablesCommand(variables, fingerprint = BigInt(0)) {
|
|
115
115
|
// variables: [{id: number, value: any}, ...]
|
|
116
116
|
// VariableListT constructor: fingerprint, baseTimestamp, items
|
|
117
|
-
|
|
118
|
-
const varList = new VariableListT(BigInt(0), nowNs(), []);
|
|
117
|
+
const varList = new VariableListT(fingerprint, nowNs(), []);
|
|
119
118
|
varList.items = variables.map(v => {
|
|
120
119
|
const varT = new VariableT();
|
|
121
120
|
varT.id = v.id;
|
package/nodes/datahub-write.js
CHANGED
|
@@ -8,18 +8,14 @@ const providerCache = new Map();
|
|
|
8
8
|
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
9
9
|
|
|
10
10
|
module.exports = function (RED) {
|
|
11
|
-
|
|
11
|
+
// Helper to get or fetch provider definition
|
|
12
|
+
async function getProviderDefinition(nc, providerId, node, payloads) {
|
|
12
13
|
const cacheKey = `${providerId}`;
|
|
13
14
|
const cached = providerCache.get(cacheKey);
|
|
14
15
|
|
|
15
16
|
// Check cache
|
|
16
17
|
if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
|
|
17
|
-
|
|
18
|
-
if (variable) {
|
|
19
|
-
node.debug && node.debug(`Key '${key}' resolved to ID ${variable.id} (cached)`);
|
|
20
|
-
// Return full object including type
|
|
21
|
-
return { id: variable.id, dataType: variable.dataType };
|
|
22
|
-
}
|
|
18
|
+
return cached.definition;
|
|
23
19
|
}
|
|
24
20
|
|
|
25
21
|
// Query provider definition
|
|
@@ -28,7 +24,6 @@ module.exports = function (RED) {
|
|
|
28
24
|
const query = payloads.buildReadProviderDefinitionQuery();
|
|
29
25
|
|
|
30
26
|
// Strategy 1: Direct Query (v1.loc.<provider>.def.qry.read)
|
|
31
|
-
// This is required for providers like Node-RED itself or simple Python scripts
|
|
32
27
|
const directSubject = `v1.loc.${providerId}.def.qry.read`;
|
|
33
28
|
try {
|
|
34
29
|
const response = await nc.request(directSubject, query, { timeout: 1000 });
|
|
@@ -38,7 +33,6 @@ module.exports = function (RED) {
|
|
|
38
33
|
}
|
|
39
34
|
|
|
40
35
|
// Strategy 2: Registry Query (v1.loc.registry.providers.<provider>.def.qry.read)
|
|
41
|
-
// Fallback for managed providers
|
|
42
36
|
if (!definition) {
|
|
43
37
|
const registrySubject = `v1.loc.registry.providers.${providerId}.def.qry.read`;
|
|
44
38
|
const response = await nc.request(registrySubject, query, { timeout: 2000 });
|
|
@@ -55,6 +49,17 @@ module.exports = function (RED) {
|
|
|
55
49
|
timestamp: Date.now()
|
|
56
50
|
});
|
|
57
51
|
|
|
52
|
+
return definition;
|
|
53
|
+
|
|
54
|
+
} catch (err) {
|
|
55
|
+
throw new Error(`Failed to fetch definition for '${providerId}': ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function resolveVariableKey(nc, providerId, key, node, payloads) {
|
|
60
|
+
try {
|
|
61
|
+
const definition = await getProviderDefinition(nc, providerId, node, payloads);
|
|
62
|
+
|
|
58
63
|
// Find variable by key
|
|
59
64
|
const variable = definition.variables.find(v => v.key === key);
|
|
60
65
|
if (!variable) {
|
|
@@ -62,13 +67,14 @@ module.exports = function (RED) {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
node.debug && node.debug(`Key '${key}' resolved to ID ${variable.id} (Type: ${variable.dataType})`);
|
|
65
|
-
return { id: variable.id, dataType: variable.dataType };
|
|
70
|
+
return { id: variable.id, dataType: variable.dataType, fingerprint: definition.fingerprint };
|
|
66
71
|
|
|
67
72
|
} catch (err) {
|
|
68
73
|
throw new Error(`Failed to resolve key '${key}': ${err.message}`);
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
|
|
72
78
|
function DataHubWriteNode(config) {
|
|
73
79
|
RED.nodes.createNode(this, config);
|
|
74
80
|
const node = this;
|
|
@@ -88,6 +94,7 @@ module.exports = function (RED) {
|
|
|
88
94
|
this.variableKey = config.variableKey?.trim();
|
|
89
95
|
this.resolvedId = null; // Cached resolved ID
|
|
90
96
|
this.resolvedDataType = null; // Cached Data Type for strict writing
|
|
97
|
+
this.resolvedFingerprint = BigInt(0); // Cached Fingerprint
|
|
91
98
|
this.payloads = null; // Will be loaded dynamically
|
|
92
99
|
|
|
93
100
|
if (!this.providerId) {
|
|
@@ -97,11 +104,7 @@ module.exports = function (RED) {
|
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
// Validate: either ID or Key required
|
|
100
|
-
|
|
101
|
-
node.error('Either Variable ID or Variable Key is required');
|
|
102
|
-
node.status({ fill: 'red', shape: 'dot', text: 'missing variable' });
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
107
|
+
// Relaxed validation for Batch Mode (handled in Input)
|
|
105
108
|
|
|
106
109
|
// If ID provided and valid, use it
|
|
107
110
|
if (this.variableId && !isNaN(this.variableId)) {
|
|
@@ -110,10 +113,6 @@ module.exports = function (RED) {
|
|
|
110
113
|
} else if (this.variableKey) {
|
|
111
114
|
// Key provided - will resolve on first message
|
|
112
115
|
node.status({ fill: 'yellow', shape: 'ring', text: 'key needs resolution' });
|
|
113
|
-
} else {
|
|
114
|
-
node.error('Invalid Variable ID');
|
|
115
|
-
node.status({ fill: 'red', shape: 'dot', text: 'invalid ID' });
|
|
116
|
-
return;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
// Load payloads module dynamically
|
|
@@ -148,6 +147,7 @@ module.exports = function (RED) {
|
|
|
148
147
|
|
|
149
148
|
// Prepare list of variables to write
|
|
150
149
|
let varsToWrite = [];
|
|
150
|
+
let currentFingerprint = node.resolvedFingerprint || BigInt(0);
|
|
151
151
|
|
|
152
152
|
// MODE 1: Batch Write (Object or Array) AND no configured single variable
|
|
153
153
|
// If the user configured a Provider but NOT a Variable ID/Key, we assume Dynamic Mode.
|
|
@@ -165,8 +165,6 @@ module.exports = function (RED) {
|
|
|
165
165
|
items = Object.entries(rawPayload).map(([k, v]) => {
|
|
166
166
|
// Detect if key is actually an ID (e.g. "5" or "id:5")?
|
|
167
167
|
// Easier: treat all keys as Keys, unless user passes explicit Array.
|
|
168
|
-
// But user map might be { "1": val, "machine.temp": val }.
|
|
169
|
-
// We can check if k is Int.
|
|
170
168
|
const asInt = parseInt(k, 10);
|
|
171
169
|
if (!isNaN(asInt) && String(asInt) === k) {
|
|
172
170
|
return { id: asInt, value: v };
|
|
@@ -184,10 +182,14 @@ module.exports = function (RED) {
|
|
|
184
182
|
if (item.key) {
|
|
185
183
|
// Resolve Key
|
|
186
184
|
try {
|
|
185
|
+
// Resolving individual keys for batch... this might be slow if uncached.
|
|
186
|
+
// Optimization: resolveVariableKey uses cache.
|
|
187
187
|
const resolved = await resolveVariableKey(nc, node.providerId, item.key, node, node.payloads);
|
|
188
188
|
targetId = resolved.id;
|
|
189
189
|
// Use resolved type if not explicit
|
|
190
190
|
if (!targetType) targetType = resolved.dataType;
|
|
191
|
+
// Use the fingerprint from the LAST resolution (assuming they come from same provider version)
|
|
192
|
+
if (resolved.fingerprint) currentFingerprint = resolved.fingerprint;
|
|
191
193
|
} catch (e) {
|
|
192
194
|
node.warn(`Skipping key '${item.key}': ${e.message}`);
|
|
193
195
|
continue;
|
|
@@ -211,11 +213,7 @@ module.exports = function (RED) {
|
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
} else {
|
|
214
|
-
// MODE 2: Single Mode
|
|
215
|
-
// If Single Mode is active, we IGNORE the structure of obj/array and write the WHOLE msg.payload
|
|
216
|
-
// to the configured target variable. (e.g. payload IS the value).
|
|
217
|
-
// UNLESS strict check? No, standard behavior is payload=value.
|
|
218
|
-
|
|
216
|
+
// MODE 2: Single Mode
|
|
219
217
|
if (!isSingleMode) {
|
|
220
218
|
// No variable configured and primitive payload? Error.
|
|
221
219
|
node.error("Configuration Error: No Variable configured and payload is not a batch object.");
|
|
@@ -226,13 +224,49 @@ module.exports = function (RED) {
|
|
|
226
224
|
let varId = node.resolvedId;
|
|
227
225
|
let varType = node.resolvedDataType;
|
|
228
226
|
|
|
227
|
+
// Logic to ensure we have a fingerprint (critical for strict providers)
|
|
228
|
+
// If we have an ID but no fingerprint, we must fetch the definition.
|
|
229
|
+
if (varId && (node.resolvedFingerprint === BigInt(0) || !node.resolvedFingerprint)) {
|
|
230
|
+
try {
|
|
231
|
+
node.status({ fill: 'yellow', shape: 'dot', text: 'fetching definition...' });
|
|
232
|
+
// Reuse resolveVariableKey logic but just to get definition?
|
|
233
|
+
// We can't use resolveVariableKey easily if we don't have a key.
|
|
234
|
+
// But we can peek into the cache directly or force a lookup.
|
|
235
|
+
// Let's call a simplified lookup.
|
|
236
|
+
|
|
237
|
+
// We don't have a dedicated function for "getDefinition", so we construct it or assume key is available?
|
|
238
|
+
// If user selected from list, we might NOT have the key in config (if legacy)?
|
|
239
|
+
// But usually we do.
|
|
240
|
+
// If we don't have key, we can matches by ID.
|
|
241
|
+
|
|
242
|
+
const definitions = await getProviderDefinition(nc, node.providerId, node, node.payloads);
|
|
243
|
+
node.resolvedFingerprint = definitions.fingerprint;
|
|
244
|
+
currentFingerprint = definitions.fingerprint;
|
|
245
|
+
|
|
246
|
+
// Optional: Verify ID matches and get Type
|
|
247
|
+
const foundVar = definitions.variables.find(v => v.id === varId);
|
|
248
|
+
if (foundVar) {
|
|
249
|
+
varType = foundVar.dataType;
|
|
250
|
+
node.resolvedDataType = varType; // update cache
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
} catch (e) {
|
|
254
|
+
node.warn(`Could not fetch fingerprint for provider ${node.providerId}: ${e.message}`);
|
|
255
|
+
// Non-fatal? If strict, write will fail. If not, 0 might work.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
229
259
|
if (!varId && node.variableKey) {
|
|
230
260
|
node.status({ fill: 'yellow', shape: 'dot', text: 'resolving key...' });
|
|
231
261
|
const resolved = await resolveVariableKey(nc, node.providerId, node.variableKey, node, node.payloads);
|
|
232
262
|
varId = resolved.id;
|
|
233
263
|
varType = resolved.dataType;
|
|
264
|
+
|
|
234
265
|
node.resolvedId = varId;
|
|
235
266
|
node.resolvedDataType = varType;
|
|
267
|
+
node.resolvedFingerprint = resolved.fingerprint;
|
|
268
|
+
currentFingerprint = resolved.fingerprint;
|
|
269
|
+
|
|
236
270
|
node.status({ fill: 'green', shape: 'ring', text: 'ready' });
|
|
237
271
|
}
|
|
238
272
|
|
|
@@ -244,8 +278,8 @@ module.exports = function (RED) {
|
|
|
244
278
|
}
|
|
245
279
|
|
|
246
280
|
// Build write command (Flatbuffer)
|
|
247
|
-
//
|
|
248
|
-
const writeCommand = node.payloads.encodeWriteVariablesCommand(varsToWrite);
|
|
281
|
+
// Pass the fingerprint (defaults to 0 if not found)
|
|
282
|
+
const writeCommand = node.payloads.encodeWriteVariablesCommand(varsToWrite, currentFingerprint);
|
|
249
283
|
|
|
250
284
|
// Publish write command
|
|
251
285
|
const subject = `v1.loc.${node.providerId}.vars.cmd.write`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.101",
|
|
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",
|