node-red-contrib-uos-nats 0.2.77 → 0.2.94

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 CHANGED
@@ -10,12 +10,12 @@ Maintained by [IoTUeli](https://iotueli.ch). Source: [GitHub](https://github.com
10
10
 
11
11
  ## Nodes Overview
12
12
 
13
- | Node | Icon | Purpose |
14
- |------|------|---------|
15
- | **u-OS Config** | ⚙️ | Central configuration for NATS connection and OAuth credentials. |
16
- | **DataHub - Read** | 📥 | Subscribe to variable changes from system providers (e.g. `u_os_adm`). |
17
- | **DataHub - Write** | 📤 | Send commands to change variables in other providers. |
18
- | **DataHub - Provider** | 📡 | Create your own provider to publish variables to the Data Hub. |
13
+ | Node | Purpose |
14
+ |------|---------|
15
+ | **u-OS Config** | Central configuration for NATS connection and OAuth credentials. |
16
+ | **DataHub - Read** | Subscribe to variable changes from system providers (e.g. `u_os_adm`). |
17
+ | **DataHub - Write** | Send commands to change variables in other providers. |
18
+ | **DataHub - Provider** | Create your own provider to publish variables to the Data Hub. |
19
19
 
20
20
  ---
21
21
 
@@ -66,13 +66,13 @@ Import this flow to test reading and writing immediately:
66
66
  ### DataHub - Read
67
67
  Reads values from existing providers (like `u_os_adm`).
68
68
  - **Provider ID:** Name of the source provider.
69
- - **Variables:** Enter `Key:ID` manually.
69
+ - **Variables:** Enter `Key:ID` manually or use the **Variables Table** to search and add variables.
70
70
  - **Trigger:** "Event" (instant update) or "Poll" (interval).
71
71
 
72
72
  ### DataHub - Write
73
73
  Changes values in other providers.
74
74
  - **Input:** Send `msg.payload` with the new value.
75
- - **Config:** Target `Provider ID` and `Variable ID` (or Key).
75
+ - **Config:** Select the **Provider ID**, click **Load Variables**, and choose a variable from the list.
76
76
 
77
77
  ### DataHub - Provider
78
78
  Publishes your own data to the Data Hub.
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
package/lib/payloads.js CHANGED
@@ -20,6 +20,8 @@ import { ReadVariablesQueryRequestT } from './fbs/weidmueller/ucontrol/hub/read-
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
22
  import { WriteVariablesCommandT } from './fbs/weidmueller/ucontrol/hub/write-variables-command.js';
23
+ import { StateChangedEvent } from './fbs/weidmueller/ucontrol/hub/state-changed-event.js';
24
+ import { State } from './fbs/weidmueller/ucontrol/hub/state.js';
23
25
 
24
26
  const DEFAULT_QUALITY = 'GOOD';
25
27
  export function buildProviderDefinitionEvent(defs) {
@@ -100,6 +102,13 @@ export function buildReadProviderDefinitionQuery() {
100
102
  return builder.asUint8Array();
101
103
  }
102
104
 
105
+ export function parseRegistryStateEvent(buffer) {
106
+ const bb = new flatbuffers.ByteBuffer(buffer);
107
+ const event = StateChangedEvent.getRootAsStateChangedEvent(bb);
108
+ // State is an enum: 1 = RUNNING
109
+ return event.state();
110
+ }
111
+
103
112
  // Encode write variables command
104
113
  export function encodeWriteVariablesCommand(variables) {
105
114
  // variables: [{id: number, value: any}, ...]
package/lib/subjects.js CHANGED
@@ -15,3 +15,6 @@ export function registryProviderQuery(providerId) {
15
15
  export function readProviderDefinitionQuery(providerId) {
16
16
  return `${VERSION_PREFIX}.${LOCATION_PREFIX}.${providerId}.def.qry.read`;
17
17
  }
18
+ export function registryStateEvent() {
19
+ return `${VERSION_PREFIX}.${LOCATION_PREFIX}.registry.state.evt.changed`;
20
+ }
@@ -8,7 +8,8 @@
8
8
  providerId: { value: "", required: true },
9
9
  manualVariables: { value: "" },
10
10
  triggerMode: { value: "event" },
11
- pollingInterval: { value: 0, validate: RED.validators.number() }
11
+ pollingInterval: { value: 0, validate: RED.validators.number() },
12
+ pollingUnit: { value: "ms" }
12
13
  },
13
14
  inputs: 1,
14
15
  outputs: 1,
@@ -21,9 +22,24 @@
21
22
  oneditprepare: function () {
22
23
  const node = this;
23
24
 
24
- // --- Trigger Mode (Event/Poll) Visibility ---
25
+ // --- Trigger Mode & Polling Logic ---
25
26
  const $triggerMode = $('#node-input-triggerMode');
26
27
  const $pollRow = $('#poll-interval-row');
28
+ const $pollIntInput = $('#node-input-pollingInterval');
29
+ const $pollUnitInput = $('#node-input-pollingUnit');
30
+
31
+ // Convert stored ms to unit for display
32
+ let currentMs = node.pollingInterval || 0;
33
+ let uiVal = currentMs;
34
+ let uiUnit = node.pollingUnit || 'ms';
35
+
36
+ // Heuristic helper if unit was missing (legacy support)
37
+ if (!node.pollingUnit && currentMs > 0) {
38
+ if (currentMs >= 60000 && currentMs % 60000 === 0) { uiVal = currentMs / 60000; uiUnit = 'min'; }
39
+ else if (currentMs >= 1000 && currentMs % 1000 === 0) { uiVal = currentMs / 1000; uiUnit = 's'; }
40
+ }
41
+ $pollUnitInput.val(uiUnit);
42
+ $pollIntInput.val(uiVal);
27
43
 
28
44
  const updateTriggerVisibility = () => {
29
45
  if ($triggerMode.val() === 'poll') {
@@ -36,52 +52,166 @@
36
52
  $triggerMode.on('change', updateTriggerVisibility);
37
53
  updateTriggerVisibility();
38
54
 
39
- // --- Manual Table Logic ---
40
- const $manualList = $('#node-input-manual-def-list');
41
- const $manualHidden = $('#node-input-manualVariables');
42
-
43
- $manualList.editableList({
44
- addItem: function (row, index, data) {
45
- row.css({ display: 'flex', alignItems: 'center', gap: '5px' });
46
-
47
- $('<input/>', { class: 'node-input-manual-name', type: 'text', placeholder: 'Variable Name', style: 'flex:1;' })
48
- .val(data.name || '')
49
- .appendTo(row);
50
-
51
- $('<input/>', { class: 'node-input-manual-id', type: 'number', placeholder: 'ID', style: 'width:80px;' })
52
- .val(data.id || '')
53
- .appendTo(row);
54
- },
55
- removable: true,
56
- header: $('<div></div>').append(
57
- $.parseHTML('<div style="display:flex; gap:5px; padding-left:5px;"><div style="flex:1;">Variable Name (Key)</div><div style="width:80px;">ID (Number)</div></div>')
58
- ),
59
- addButton: 'Add Variable'
55
+ // --- Variable Selector Logic ---
56
+ const $fetchBtn = $('#btn-fetch-vars');
57
+ const $listContainer = $('#variable-list-container');
58
+ const $statusMsg = $('#fetch-status');
59
+ const $searchInput = $('#node-input-var-search');
60
+ const $selectAllBtn = $('#btn-select-all');
61
+ const $clearAllBtn = $('#btn-clear-all');
62
+ const $hiddenManual = $('#node-input-manualVariables');
63
+
64
+ // Styles
65
+ const rowStyle = "display:grid; grid-template-columns: 30px 60px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px;";
66
+ const idStyle = "background:#eee; color:#555; padding:1px 4px; border-radius:3px; font-family:monospace; text-align:center; font-size:11px;";
67
+ const keyStyle = "overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px;";
68
+
69
+ let currentVariables = []; // Stores {id, key, accessType, etc.}
70
+
71
+ // Parse existing selection
72
+ const getSelectedMap = () => {
73
+ const selected = new Map(); // Key -> ID
74
+ const raw = $hiddenManual.val();
75
+ if (raw) {
76
+ raw.split(',').forEach(entry => {
77
+ const parts = entry.split(':');
78
+ if (parts.length === 2) {
79
+ selected.set(parts[0].trim(), parts[1].trim());
80
+ }
81
+ });
82
+ }
83
+ return selected;
84
+ };
85
+
86
+ const renderList = (vars) => {
87
+ $listContainer.empty();
88
+ if (!vars || vars.length === 0) {
89
+ $listContainer.append('<div style="padding:15px; color:#777; text-align:center;">No variables found.</div>');
90
+ return;
91
+ }
92
+
93
+ const selectedMap = getSelectedMap();
94
+
95
+ vars.forEach(v => {
96
+ // Robust ID handling: verify v.id exists (try 'id' and 'Id')
97
+ let rawId = (v.id !== undefined && v.id !== null) ? v.id : v.Id;
98
+ const safeId = (rawId !== undefined && rawId !== null) ? rawId : 'ERR';
99
+ const isSelected = selectedMap.has(v.key);
100
+
101
+ const row = $('<div>', { class: 'var-row', style: rowStyle });
102
+
103
+ const cbContainer = $('<div>', { style: 'text-align:center;' });
104
+ const cb = $('<input type="checkbox" class="var-checkbox">')
105
+ .prop('checked', isSelected)
106
+ .data('key', v.key)
107
+ .data('id', safeId);
108
+ cbContainer.append(cb);
109
+
110
+ const idBadge = $('<div>').append($('<span>', { style: idStyle }).text(safeId));
111
+ const label = $('<div>', { style: keyStyle, title: v.key }).text(v.key);
112
+
113
+ row.append(cbContainer).append(idBadge).append(label);
114
+ $listContainer.append(row);
115
+ });
116
+ };
117
+
118
+ const filterList = (term) => {
119
+ const rows = $listContainer.find('.var-row');
120
+ term = term.toLowerCase();
121
+ rows.each(function () {
122
+ const text = $(this).find('div:nth-child(3)').text().toLowerCase(); // 3rd child is name
123
+ $(this).toggle(text.indexOf(term) > -1);
124
+ });
125
+ };
126
+
127
+ $searchInput.on('keyup', function () {
128
+ filterList($(this).val());
60
129
  });
61
130
 
62
- const currentManual = node.manualVariables || '';
63
- if (currentManual) {
64
- currentManual.split(',').forEach(entry => {
65
- const parts = entry.split(':');
66
- if (parts.length === 2) {
67
- const n = parts[0].trim();
68
- const i = parts[1].trim();
69
- if (n && i) $manualList.editableList('addItem', { name: n, id: i });
70
- }
131
+ $selectAllBtn.on('click', function () {
132
+ $listContainer.find('.var-checkbox:visible').prop('checked', true);
133
+ });
134
+
135
+ $clearAllBtn.on('click', function () {
136
+ $listContainer.find('.var-checkbox:visible').prop('checked', false);
137
+ });
138
+
139
+ $fetchBtn.on('click', function () {
140
+ const configNodeId = $('#node-input-connection').val();
141
+ const providerId = $('#node-input-providerId').val();
142
+
143
+ if (!configNodeId || configNodeId === '_ADD_') {
144
+ $statusMsg.text('Select Config Node!').css('color', 'red');
145
+ return;
146
+ }
147
+ if (!providerId) {
148
+ $statusMsg.text('Enter Provider ID!').css('color', 'red');
149
+ return;
150
+ }
151
+
152
+ $fetchBtn.prop('disabled', true);
153
+ $statusMsg.text('Fetching...').css('color', 'blue');
154
+ $listContainer.html('<div style="padding:20px; text-align:center;"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
155
+
156
+ $.getJSON('uos/providers/' + configNodeId + '/' + providerId + '/variables', function (data) {
157
+ $fetchBtn.prop('disabled', false);
158
+ $statusMsg.text('');
159
+ // Sort by ID (handle 'id' or 'Id')
160
+ currentVariables = data.sort((a, b) => {
161
+ const idA = (a.id !== undefined) ? a.id : a.Id;
162
+ const idB = (b.id !== undefined) ? b.id : b.Id;
163
+ return parseInt(idA) - parseInt(idB);
164
+ });
165
+ renderList(currentVariables);
166
+ $statusMsg.text(`Loaded ${data.length} variables.`).css('color', 'green');
167
+ }).fail(function (jqxhr) {
168
+ $fetchBtn.prop('disabled', false);
169
+ $statusMsg.text('Error: ' + (jqxhr.responseJSON?.error || 'Unknown')).css('color', 'red');
170
+ $listContainer.empty();
71
171
  });
172
+ });
173
+
174
+ // Initial Render
175
+ const initialMap = getSelectedMap();
176
+ if (initialMap.size > 0 && currentVariables.length === 0) {
177
+ const dummyVars = [];
178
+ initialMap.forEach((id, key) => dummyVars.push({ id, key }));
179
+ // Just in case stored ID is undefined, show it so user can see it's broken
180
+ renderList(dummyVars);
181
+ $statusMsg.text('Cached variables shown. Load again to refresh.').css('color', '#888');
182
+ } else {
183
+ $listContainer.html('<div style="padding:20px; text-align:center; color:#999;">Start by clicking <b>Load Variables</b>.<br><br>Empty Selection = <b>Read ALL</b></div>');
72
184
  }
73
185
  },
74
186
  oneditsave: function () {
75
- const $manualList = $('#node-input-manual-def-list');
187
+ // 1. Save Variables
76
188
  const items = [];
77
- $manualList.editableList('items').each(function () {
78
- const name = $(this).find('.node-input-manual-name').val().trim();
79
- const id = $(this).find('.node-input-manual-id').val().trim();
80
- if (name && id) {
81
- items.push(`${name}:${id}`);
189
+ $('#variable-list-container').find('.var-checkbox').each(function () {
190
+ if ($(this).prop('checked')) {
191
+ const k = $(this).data('key');
192
+ const i = $(this).data('id');
193
+ // Important: Don't save if ID is 'ERR' or undefined
194
+ if (k && i !== undefined && i !== 'ERR') {
195
+ items.push(`${k}:${i}`);
196
+ }
82
197
  }
83
198
  });
84
199
  $('#node-input-manualVariables').val(items.join(','));
200
+
201
+ // 2. Save Polling Interval (calculate ms)
202
+ const val = parseInt($('#node-input-pollingInterval').val(), 10) || 0;
203
+ const unit = $('#node-input-pollingUnit').val();
204
+ let multiplier = 1;
205
+ if (unit === 's') multiplier = 1000;
206
+ if (unit === 'min') multiplier = 60000;
207
+
208
+ // We overwrite the raw value with calculated MS.
209
+ // WAIT! The 'defaults' define pollingInterval. If we overwrite it here, the UI input needs to read it back correctly in oneditprepare.
210
+ // Actually, standard pattern is to store the MS value in 'pollingInterval' and recover the Unit in UI.
211
+ // But my oneditprepare logic for unit recovery was heuristic. Let's make it robust by trusting the calculation.
212
+ const totalMs = val * multiplier;
213
+ $('#node-input-pollingInterval').val(totalMs);
214
+ $('#node-input-pollingUnit').val(unit); // Also save the unit
85
215
  }
86
216
  });
87
217
  </script>
@@ -99,34 +229,55 @@
99
229
 
100
230
  <div class="form-row">
101
231
  <label for="node-input-providerId"><i class="fa fa-server"></i> Provider ID</label>
102
- <input type="text" id="node-input-providerId" placeholder="e.g. u_os_sbm">
232
+ <div style="display:flex; gap:5px;">
233
+ <input type="text" id="node-input-providerId" placeholder="e.g. u_os_adm" style="flex:1;">
234
+ <button id="btn-fetch-vars" class="red-ui-button"><i class="fa fa-refresh"></i> Load Variables</button>
235
+ </div>
236
+ <div id="fetch-status" style="margin-top:5px; font-size:0.9em; min-height:1.2em;"></div>
103
237
  </div>
104
238
 
105
- <div class="form-row">
106
- <label><i class="fa fa-table"></i> Variables</label>
107
- <div style="width:100%; margin-top:8px;">
108
- <ol id="node-input-manual-def-list"></ol>
239
+ <div class="form-row" style="border:1px solid #ccc; padding:0; border-radius:4px; background:#fff;">
240
+ <div style="background:#f7f7f7; padding:8px 10px; border-bottom:1px solid #ddd; display:flex; justify-content:space-between; align-items:center;">
241
+ <span style="font-weight:bold; font-size:12px;"><i class="fa fa-list-ul"></i> Selection</span>
242
+ <input type="text" id="node-input-var-search" placeholder="Filter..." style="width:120px; font-size:11px; padding:2px;">
243
+ </div>
244
+
245
+ <div style="display:grid; grid-template-columns: 30px 60px 1fr; background:#eee; font-size:11px; font-weight:bold; padding:4px 0; border-bottom:1px solid #ccc;">
246
+ <div style="text-align:center;"><i class="fa fa-check-square-o"></i></div>
247
+ <div style="text-align:center;">ID</div>
248
+ <div style="padding-left:10px;">Name</div>
249
+ </div>
250
+
251
+ <div id="variable-list-container" style="height:250px; overflow-y:auto; background:white;">
252
+ <!-- Variables -->
109
253
  </div>
110
- <input type="hidden" id="node-input-manualVariables">
111
- </div>
112
254
 
113
- <div class="form-tips">
114
- <i class="fa fa-info-circle"></i> Enter Variable Names and their IDs. Only these variables will be read.
255
+ <div style="background:#f7f7f7; padding:8px 10px; border-top:1px solid #ddd; display:flex; gap:10px;">
256
+ <button id="btn-select-all" class="red-ui-button red-ui-button-small">Select All</button>
257
+ <button id="btn-clear-all" class="red-ui-button red-ui-button-small">Clear All</button>
258
+ <div style="flex:1; text-align:right; color:#888; font-size:11px; padding-top:4px;">Empty = Read ALL</div>
259
+ </div>
260
+ <input type="hidden" id="node-input-manualVariables">
115
261
  </div>
116
262
 
117
263
  <hr style="margin:15px 0;">
118
264
 
119
265
  <div class="form-row">
120
266
  <label for="node-input-triggerMode"><i class="fa fa-bolt"></i> Trigger</label>
121
- <select id="node-input-triggerMode">
267
+ <select id="node-input-triggerMode" style="width:70%;">
122
268
  <option value="event">Event (on change)</option>
123
269
  <option value="poll">Poll (interval)</option>
124
270
  </select>
125
271
  </div>
126
272
 
127
273
  <div id="poll-interval-row" class="form-row" style="display:none;">
128
- <label for="node-input-pollingInterval"><i class="fa fa-clock-o"></i> Interval (ms)</label>
129
- <input type="number" id="node-input-pollingInterval" placeholder="e.g. 1000">
274
+ <label for="node-input-pollingInterval"><i class="fa fa-clock-o"></i> Interval</label>
275
+ <input type="number" id="node-input-pollingInterval" style="width:80px;" placeholder="1000">
276
+ <select id="node-input-pollingUnit" style="width:80px;">
277
+ <option value="ms">ms</option>
278
+ <option value="s">sec</option>
279
+ <option value="min">min</option>
280
+ </select>
130
281
  </div>
131
282
  </script>
132
283
 
@@ -51,6 +51,11 @@ module.exports = function (RED) {
51
51
  });
52
52
  }
53
53
 
54
+ // Warn if manual config existed but resulted in no valid definitions (e.g. corruption or NaN IDs)
55
+ if (manualText.length > 0 && this.manualDefs.length === 0) {
56
+ this.warn("Configuration Warning: 'Selected Variables' contained data but no valid IDs could be parsed. Falling back to 'Read All'. Please re-select variables in the editor.");
57
+ }
58
+
54
59
  let nc;
55
60
  let sub;
56
61
  let closed = false;
@@ -177,33 +182,38 @@ module.exports = function (RED) {
177
182
  }
178
183
 
179
184
  try {
185
+ // Resolve requested variable names to IDs
180
186
  // Resolve requested variable names to IDs
181
187
  let targetIds = [];
182
- if (this.variables.length > 0) {
188
+ let isWildcard = (this.variables.length === 0);
189
+
190
+ if (!isWildcard) {
183
191
  // Reverse lookup: Find Def by Key
184
192
  const requestedKeys = new Set(this.variables);
185
193
 
186
- // Debug Map Content before filtering
187
- // if (defMap.size > 0 && providerRequestCount < 2) {
188
- // this.warn(`DefMap Dump (Size ${defMap.size}): ${Array.from(defMap.values()).map(d=>d.key).slice(0,5).join(', ')} ...`);
189
- // }
190
-
191
194
  for (const def of defMap.values()) {
192
195
  if (requestedKeys.has(def.key)) {
193
196
  targetIds.push(Number(def.id));
194
197
  }
195
198
  }
196
- if (targetIds.length === 0 && defMap.size > 0) {
197
- // Warning logic ...
198
- const sampleKeys = Array.from(defMap.values()).filter(d => d.type !== 'MANUAL').map(d => `'${d.key}'`).slice(0, 5).join(', ');
199
- this.warn(`Snapshot Warning: None of the ${this.variables.length} configured variables were found in the Provider Definition.`);
200
- if (!sampleKeys && this.manualDefs.length > 0) {
201
- this.warn(' -> Manual Definitions are configured but did not match the requested variable names? Check capitalization.');
199
+
200
+ // CRITICAL FIX: If user requested specific variables but we found NONE,
201
+ // we MUST NOT send an empty list, because that would interpret as "Read All".
202
+ if (targetIds.length === 0) {
203
+ if (defMap.size > 0) {
204
+ this.warn(`Snapshot Aborted: None of the ${this.variables.length} requested variables could be resolved to IDs. (Provider has ${defMap.size} vars).`);
205
+ } else {
206
+ // If defMap is empty (Discovery failed), we might want to try reading ALL to see if we get lucky?
207
+ // Or just rely on Manual Defs fallback which would have populated defMap.
208
+ // Safe default: Abort to avoid flooding if discovery failed.
209
+ this.warn('Snapshot Aborted: Provider Definition not ready (no IDs resolved).');
202
210
  }
211
+ return;
203
212
  }
204
213
  }
205
214
 
206
- // If we have specific IDs, request only those. Otherwise request all (empty array).
215
+ // If Wildcard (empty targetIds and isWildcard=true) -> Request ALL.
216
+ // If Specific (targetIds has items) -> Request specific.
207
217
  const snapshotMsg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(targetIds), { timeout: 2000 });
208
218
 
209
219
  const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
@@ -109,12 +109,22 @@ module.exports = function (RED) {
109
109
  return { def, created: true };
110
110
  };
111
111
 
112
- const sendDefinitionUpdate = async (payloads, subjects) => {
113
- console.log(`[DataHub Output] Publishing definition with ${definitions.length} vars...`);
114
- const { payload, fingerprint: fp } = payloads.buildProviderDefinitionEvent(definitions);
115
- fingerprint = fp;
116
- await nc.publish(subjects.providerDefinitionChanged(this.providerId), payload);
117
- console.log(`[DataHub Output] Definition published. FP: ${fp}`);
112
+ // --- SEND DEFINITION HELPER ---
113
+ const sendDefinitionUpdate = async (modPayloads, modSubjects) => {
114
+ if (!nc) return;
115
+ try {
116
+ const { payload, fingerprint: fp } = modPayloads.buildProviderDefinitionEvent(definitions);
117
+ const subject = modSubjects.providerDefinitionChanged(this.providerId);
118
+
119
+ console.log(`[DataHub Output] Publishing definition for Provider '${this.providerId}' to '${subject}'`);
120
+
121
+ fingerprint = fp;
122
+ await nc.publish(subject, payload);
123
+ await nc.flush();
124
+ console.log(`[DataHub Output] Definition published. FP: ${fp}`);
125
+ } catch (err) {
126
+ this.warn(`Definition update error: ${err.message}`);
127
+ }
118
128
  };
119
129
 
120
130
  const handleRead = async (payloads, msg) => {
@@ -137,7 +147,19 @@ module.exports = function (RED) {
137
147
  const sendValuesUpdate = async () => {
138
148
  if (!nc || nc.isClosed()) {
139
149
  console.log('[DataHub Output] Heartbeat skipped: NATS closed or missing.');
140
- return;
150
+ // Add event listeners for connection status changes
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
141
163
  }
142
164
  if (!loadedPayloads || !loadedSubjects) return;
143
165
 
@@ -187,9 +209,28 @@ module.exports = function (RED) {
187
209
  nc = await connection.acquire();
188
210
  console.log('[DataHub Output] NATS acquired.');
189
211
 
190
- // Only publish definition if we have one. Empty definitions might be rejected?
191
212
  if (definitions.length > 0) {
213
+ // Initial publish
192
214
  await sendDefinitionUpdate(payloads, subjects);
215
+
216
+ // 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
+ nc.subscribe(subjects.registryStateEvent(), {
220
+ callback: (err, msg) => {
221
+ if (err) return;
222
+ try {
223
+ const state = payloads.parseRegistryStateEvent(msg.data);
224
+ if (state === 1) { // 1 = RUNNING
225
+ console.log('[DataHub Output] Registry is RUNNING. Re-publishing definition...');
226
+ sendDefinitionUpdate(payloads, subjects);
227
+ }
228
+ } catch (e) {
229
+ console.warn('[DataHub Output] Registry state parse error:', e);
230
+ }
231
+ }
232
+ });
233
+ console.log('[DataHub Output] Subscribed to Registry State.');
193
234
  }
194
235
 
195
236
  // Listen for Variable READ requests