node-red-contrib-uos-nats 1.0.8 → 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 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: 1,
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>. Lower values increase traffic but might be needed for very strict networks.</dd>
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": {
@@ -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);
@@ -244,6 +244,81 @@ module.exports = function (RED) {
244
244
  });
245
245
  console.log('[DataHub Output] Subscribed to Read Query.');
246
246
 
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}`);
251
+
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
+ }
321
+
247
322
  this.status({ fill: 'green', shape: 'dot', text: 'ready' });
248
323
 
249
324
  // Heartbeat Restored: Provider disappears if definition is not refreshed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "1.0.8",
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",