node-red-contrib-uos-nats 1.0.7 → 1.1.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.
- package/lib/payloads.js +7 -1
- package/nodes/datahub-output.html +17 -3
- package/nodes/datahub-output.js +98 -50
- package/package.json +1 -1
package/lib/payloads.js
CHANGED
|
@@ -19,7 +19,7 @@ import { ReadVariablesQueryResponseT } from './fbs/weidmueller/ucontrol/hub/read
|
|
|
19
19
|
import { ReadVariablesQueryRequestT } from './fbs/weidmueller/ucontrol/hub/read-variables-query-request.js';
|
|
20
20
|
import { ReadVariablesQueryRequest } from './fbs/weidmueller/ucontrol/hub/read-variables-query-request.js';
|
|
21
21
|
import { ReadProviderDefinitionQueryRequest } from './fbs/weidmueller/ucontrol/hub/read-provider-definition-query-request.js';
|
|
22
|
-
import { WriteVariablesCommandT } from './fbs/weidmueller/ucontrol/hub/write-variables-command.js';
|
|
22
|
+
import { WriteVariablesCommandT, WriteVariablesCommand } from './fbs/weidmueller/ucontrol/hub/write-variables-command.js';
|
|
23
23
|
import { StateChangedEvent } from './fbs/weidmueller/ucontrol/hub/state-changed-event.js';
|
|
24
24
|
import { State } from './fbs/weidmueller/ucontrol/hub/state.js';
|
|
25
25
|
|
|
@@ -206,6 +206,12 @@ export function encodeWriteVariablesCommand(variables, fingerprint = BigInt(0))
|
|
|
206
206
|
return builder.asUint8Array();
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
export function decodeWriteVariablesCommand(buffer) {
|
|
210
|
+
const bb = new flatbuffers.ByteBuffer(buffer);
|
|
211
|
+
const cmd = WriteVariablesCommand.getRootAsWriteVariablesCommand(bb);
|
|
212
|
+
return decodeVariableList(cmd.variables());
|
|
213
|
+
}
|
|
214
|
+
|
|
209
215
|
export function decodeVariableList(list) {
|
|
210
216
|
if (!list)
|
|
211
217
|
return [];
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
name: { value: '' },
|
|
7
7
|
connection: { type: 'uos-config', required: true },
|
|
8
8
|
providerId: { value: '', required: false },
|
|
9
|
-
heartbeatInterval: { value: 300, required: true, validate: RED.validators.number() } // Default 5 mins
|
|
9
|
+
heartbeatInterval: { value: 300, required: true, validate: RED.validators.number() }, // Default 5 mins
|
|
10
|
+
enableWrites: { value: false }
|
|
10
11
|
},
|
|
11
12
|
inputs: 1,
|
|
12
|
-
outputs:
|
|
13
|
+
outputs: 2,
|
|
14
|
+
outputLabels: ["Input Passthrough", "External Write Command"],
|
|
13
15
|
icon: "datahub-provider.svg",
|
|
14
16
|
oneditprepare: function () {
|
|
15
17
|
const node = this;
|
|
@@ -119,6 +121,11 @@
|
|
|
119
121
|
<input type="number" id="node-input-heartbeatInterval" placeholder="300" style="width:100px;">
|
|
120
122
|
<span style="color:#777; font-size:0.9em; margin-left:10px;">(Default: 300s / 5min)</span>
|
|
121
123
|
</div>
|
|
124
|
+
<div class="form-row">
|
|
125
|
+
<label for="node-input-enableWrites"><i class="fa fa-pencil"></i> Access</label>
|
|
126
|
+
<input type="checkbox" id="node-input-enableWrites" style="width:auto; vertical-align:top;">
|
|
127
|
+
<span style="margin-left:5px;">Allow external writes (Bidirectional)</span>
|
|
128
|
+
</div>
|
|
122
129
|
</script>
|
|
123
130
|
|
|
124
131
|
<script type="text/html" data-help-name="datahub-output">
|
|
@@ -130,8 +137,15 @@
|
|
|
130
137
|
<dd>The unique ID for this provider.</dd>
|
|
131
138
|
<dd><b>Auto Mode:</b> Shows the <em>Client Name</em> from your Config. Uses this name automatically.</dd>
|
|
132
139
|
<dt>Keep-Alive (s)</dt>
|
|
133
|
-
<dd>Interval in seconds to refresh the provider definition. Default: <b>300s (5 min)</b
|
|
140
|
+
<dd>Interval in seconds to refresh the provider definition. Default: <b>300s (5 min)</b>.</dd>
|
|
141
|
+
<dt>Allow external writes</dt>
|
|
142
|
+
<dd>If checked, variables are published as <code>READ_WRITE</code>. External changes are emitted on the <b>2nd Output</b>.</dd>
|
|
134
143
|
</dl>
|
|
144
|
+
<h3>Outputs</h3>
|
|
145
|
+
<ol>
|
|
146
|
+
<li><b>Input Passthrough:</b> The original message sent to the node.</li>
|
|
147
|
+
<li><b>External Write Command:</b> Emits a message when another system writes to a variable. <code>msg.var</code> contains the key, <code>msg.value</code> the new value.</li>
|
|
148
|
+
</ol>
|
|
135
149
|
<p>Send a JSON object as <code>msg.payload</code>. Nested objects are flattened using dot-notation to create <strong>subcategories</strong> automatically:</p>
|
|
136
150
|
<pre>{
|
|
137
151
|
"machine": {
|
package/nodes/datahub-output.js
CHANGED
|
@@ -96,7 +96,7 @@ module.exports = function (RED) {
|
|
|
96
96
|
id: nextId += 1,
|
|
97
97
|
key: normalized,
|
|
98
98
|
dataType,
|
|
99
|
-
access: 'READ_ONLY',
|
|
99
|
+
access: config.enableWrites ? 'READ_WRITE' : 'READ_ONLY',
|
|
100
100
|
};
|
|
101
101
|
defMap.set(normalized, def);
|
|
102
102
|
definitions.push(def);
|
|
@@ -146,38 +146,21 @@ module.exports = function (RED) {
|
|
|
146
146
|
|
|
147
147
|
const sendValuesUpdate = async () => {
|
|
148
148
|
if (!nc || nc.isClosed()) {
|
|
149
|
-
console.log('[DataHub Output] Heartbeat skipped: NATS closed
|
|
150
|
-
|
|
151
|
-
// Note: `connection` is defined in the outer scope, not `this.connection`
|
|
152
|
-
connection.on('reconnected', () => {
|
|
153
|
-
console.log('[DataHub Output] Reconnected. Re-publishing definition...');
|
|
154
|
-
this.status({ fill: 'green', shape: 'ring', text: 'reconnected' });
|
|
155
|
-
start().catch((err) => {
|
|
156
|
-
this.warn(`Re-registration failed: ${err.message}`);
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
connection.on('disconnected', () => {
|
|
160
|
-
this.status({ fill: 'red', shape: 'ring', text: 'disconnected' });
|
|
161
|
-
});
|
|
162
|
-
return; // Skip sending values if NATS is closed
|
|
149
|
+
// console.log('[DataHub Output] Heartbeat skipped: NATS closed.');
|
|
150
|
+
return;
|
|
163
151
|
}
|
|
164
152
|
if (!loadedPayloads || !loadedSubjects) return;
|
|
165
153
|
|
|
166
154
|
// If we have no definitions yet, nothing to send
|
|
167
|
-
if (definitions.length === 0)
|
|
168
|
-
// console.log('[DataHub Output] Heartbeat skipped: No definitions.');
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
155
|
+
if (definitions.length === 0) return;
|
|
171
156
|
|
|
172
157
|
// Log heartbeat occasionally to prove aliveness
|
|
173
158
|
const nowMs = Date.now();
|
|
174
159
|
if (!this.lastHeartbeatLog || nowMs - this.lastHeartbeatLog > 10000) {
|
|
175
|
-
console.log(`[DataHub Output] Sending heartbeat for ${definitions.length} vars...`);
|
|
176
160
|
this.lastHeartbeatLog = nowMs;
|
|
177
161
|
}
|
|
178
162
|
|
|
179
163
|
const stateObj = {};
|
|
180
|
-
const nowNs = Date.now() * 1_000_000;
|
|
181
164
|
for (const s of stateMap.values()) {
|
|
182
165
|
s.timestamp = BigInt(Date.now()) * 1_000_000n; // Force refresh timestamp
|
|
183
166
|
stateObj[s.id] = s;
|
|
@@ -187,7 +170,7 @@ module.exports = function (RED) {
|
|
|
187
170
|
const subject = loadedSubjects.varsChangedEvent(this.providerId);
|
|
188
171
|
|
|
189
172
|
await nc.publish(subject, payload);
|
|
190
|
-
await nc.flush();
|
|
173
|
+
await nc.flush();
|
|
191
174
|
} catch (err) {
|
|
192
175
|
this.warn(`Heartbeat error: ${err.message}`);
|
|
193
176
|
}
|
|
@@ -197,6 +180,18 @@ module.exports = function (RED) {
|
|
|
197
180
|
sendValuesUpdate();
|
|
198
181
|
}, 1000); // 1.0s interval matches Python SDK
|
|
199
182
|
|
|
183
|
+
// Reconnect Handler (Defined outside start to be referenced in close)
|
|
184
|
+
const onReconnect = () => {
|
|
185
|
+
console.log('[DataHub Output] Connection restored (event). Re-sending definition...');
|
|
186
|
+
this.status({ fill: 'green', shape: 'ring', text: 'reconnected' });
|
|
187
|
+
// Force definition update immediately upon reconnection
|
|
188
|
+
if (loadedPayloads && loadedSubjects) {
|
|
189
|
+
sendDefinitionUpdate(loadedPayloads, loadedSubjects).catch(err => {
|
|
190
|
+
this.warn(`Reconnect update error: ${err.message}`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
200
195
|
const start = async () => {
|
|
201
196
|
try {
|
|
202
197
|
console.log('[DataHub Output] Starting...');
|
|
@@ -209,13 +204,14 @@ module.exports = function (RED) {
|
|
|
209
204
|
nc = await connection.acquire();
|
|
210
205
|
console.log('[DataHub Output] NATS acquired.');
|
|
211
206
|
|
|
207
|
+
// ATTACH LISTENER HERE
|
|
208
|
+
connection.on('reconnected', onReconnect);
|
|
209
|
+
|
|
212
210
|
if (definitions.length > 0) {
|
|
213
211
|
// Initial publish
|
|
214
212
|
await sendDefinitionUpdate(payloads, subjects);
|
|
215
213
|
|
|
216
214
|
// Subscribe to Registry State changes
|
|
217
|
-
// The registry publishes its state (RUNNING=1) when it comes online.
|
|
218
|
-
// Providers MUST re-publish their definition when this happens.
|
|
219
215
|
nc.subscribe(subjects.registryStateEvent(), {
|
|
220
216
|
callback: (err, msg) => {
|
|
221
217
|
if (err) return;
|
|
@@ -237,10 +233,7 @@ module.exports = function (RED) {
|
|
|
237
233
|
sub = nc.subscribe(subjects.readVariablesQuery(this.providerId), {
|
|
238
234
|
callback: (err, msg) => {
|
|
239
235
|
if (err) {
|
|
240
|
-
// Suppress permission violation error as it's expected for some tokens
|
|
241
|
-
// and doesn't prevent pushing data.
|
|
242
236
|
if (err.message.includes('Permissions Violation')) {
|
|
243
|
-
this.trace(`Read request permission invalid (expected for push-only): ${err.message}`);
|
|
244
237
|
return;
|
|
245
238
|
}
|
|
246
239
|
this.warn(`Read request error: ${err.message}`);
|
|
@@ -251,28 +244,80 @@ module.exports = function (RED) {
|
|
|
251
244
|
});
|
|
252
245
|
console.log('[DataHub Output] Subscribed to Read Query.');
|
|
253
246
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const defSub = nc.subscribe(subjects.readProviderDefinitionQuery(this.providerId), {
|
|
259
|
-
callback: (err, msg) => {
|
|
260
|
-
if (err) {
|
|
261
|
-
this.warn(`Def request error: ${err.message}`);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (!msg.reply) return;
|
|
265
|
-
|
|
266
|
-
// Send known definition
|
|
267
|
-
const { payload } = payloads.buildProviderDefinitionEvent(definitions);
|
|
268
|
-
nc.publish(msg.reply, payload);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
*/
|
|
247
|
+
// LISTEN FOR WRITE COMMANDS (BIDIRECTIONAL)
|
|
248
|
+
if (config.enableWrites) {
|
|
249
|
+
const writeSubject = subjects.writeVariablesCommand(this.providerId);
|
|
250
|
+
console.log(`[DataHub Output] Subscribing to WRITE command: ${writeSubject}`);
|
|
272
251
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
252
|
+
nc.subscribe(writeSubject, {
|
|
253
|
+
callback: (err, msg) => {
|
|
254
|
+
if (err) {
|
|
255
|
+
this.warn(`Write command error: ${err.message}`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const updates = payloads.decodeWriteVariablesCommand(msg.data);
|
|
260
|
+
if (!updates || updates.length === 0) return;
|
|
261
|
+
|
|
262
|
+
console.log(`[DataHub Output] Received WRITE for ${updates.length} vars.`);
|
|
263
|
+
|
|
264
|
+
// Prepare output for Node-RED Flow
|
|
265
|
+
const outputMsgs = [];
|
|
266
|
+
let stateChanged = false;
|
|
267
|
+
|
|
268
|
+
updates.forEach(update => {
|
|
269
|
+
// Find defining key by ID
|
|
270
|
+
// This is expensive O(N). Optimization: Map<ID, Key>
|
|
271
|
+
// But definitions are usually few (<1000).
|
|
272
|
+
// We have stateMap keyed by ID!
|
|
273
|
+
const currentState = stateMap.get(update.id);
|
|
274
|
+
if (currentState) {
|
|
275
|
+
// Update internal state
|
|
276
|
+
currentState.value = update.value;
|
|
277
|
+
currentState.timestamp = BigInt(Date.now()) * 1_000_000n;
|
|
278
|
+
currentState.quality = 'GOOD_LOCAL_OVERRIDE'; // Mark as written? Or just GOOD?
|
|
279
|
+
// NATS Sample uses GOOD.
|
|
280
|
+
|
|
281
|
+
// Find key (optional, for convenience in msg)
|
|
282
|
+
const def = definitions.find(d => d.id === update.id);
|
|
283
|
+
const key = def ? def.key : `id_${update.id}`;
|
|
284
|
+
|
|
285
|
+
outputMsgs.push({
|
|
286
|
+
topic: key,
|
|
287
|
+
payload: update.value,
|
|
288
|
+
var: key,
|
|
289
|
+
value: update.value,
|
|
290
|
+
providerId: this.providerId,
|
|
291
|
+
_msgid: RED.util.generateId()
|
|
292
|
+
});
|
|
293
|
+
stateChanged = true;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (stateChanged) {
|
|
298
|
+
// ACK changes back to DataHub (VariablesChangedEvent)
|
|
299
|
+
// This confirms to the writer that the value was accepted.
|
|
300
|
+
sendValuesUpdate();
|
|
301
|
+
|
|
302
|
+
// Emit to 2nd Output
|
|
303
|
+
if (outputMsgs.length > 0) {
|
|
304
|
+
// Node-RED send: [output1, output2]
|
|
305
|
+
// We send null to output1 (pass-through not involved here)
|
|
306
|
+
// We send array of msgs to output2? Or one by one?
|
|
307
|
+
// Ideally one array if multiple updates?
|
|
308
|
+
// Let's send them individually to be safe with flows.
|
|
309
|
+
outputMsgs.forEach(m => {
|
|
310
|
+
this.send([null, m]);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
} catch (e) {
|
|
316
|
+
this.warn(`Failed to process write command: ${e.message}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
276
321
|
|
|
277
322
|
this.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
278
323
|
|
|
@@ -317,7 +362,6 @@ module.exports = function (RED) {
|
|
|
317
362
|
}
|
|
318
363
|
|
|
319
364
|
let definitionsChanged = false;
|
|
320
|
-
const states = [];
|
|
321
365
|
|
|
322
366
|
entries.forEach(({ key, value }) => {
|
|
323
367
|
// Ensure we don't accidentally send undefined/null as value if logic slipped through
|
|
@@ -364,11 +408,15 @@ module.exports = function (RED) {
|
|
|
364
408
|
|
|
365
409
|
this.on('close', async (done) => {
|
|
366
410
|
try {
|
|
411
|
+
// REMOVE LISTENER
|
|
412
|
+
if (connection && onReconnect) {
|
|
413
|
+
connection.removeListener('reconnected', onReconnect);
|
|
414
|
+
}
|
|
415
|
+
|
|
367
416
|
if (valueHeartbeat) clearInterval(valueHeartbeat);
|
|
368
417
|
if (sub) {
|
|
369
418
|
await sub.drain();
|
|
370
419
|
}
|
|
371
|
-
// if (outputHeartbeat) clearInterval(outputHeartbeat);
|
|
372
420
|
await connection.release();
|
|
373
421
|
}
|
|
374
422
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|