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 +8 -8
- package/lib/package.json +3 -0
- package/lib/payloads.js +9 -0
- package/lib/subjects.js +3 -0
- package/nodes/datahub-input.html +201 -50
- package/nodes/datahub-input.js +23 -13
- package/nodes/datahub-output.js +49 -8
- package/nodes/datahub-write.html +154 -103
- package/nodes/datahub-write.js +0 -64
- package/nodes/uos-config.js +222 -144
- package/package.json +1 -1
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 |
|
|
14
|
-
|
|
15
|
-
| **u-OS Config** |
|
|
16
|
-
| **DataHub - Read** |
|
|
17
|
-
| **DataHub - Write** |
|
|
18
|
-
| **DataHub - Provider** |
|
|
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:**
|
|
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.
|
package/lib/package.json
ADDED
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
|
+
}
|
package/nodes/datahub-input.html
CHANGED
|
@@ -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
|
|
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
|
-
// ---
|
|
40
|
-
const $
|
|
41
|
-
const $
|
|
42
|
-
|
|
43
|
-
$
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
187
|
+
// 1. Save Variables
|
|
76
188
|
const items = [];
|
|
77
|
-
$
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
107
|
-
|
|
108
|
-
<
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
129
|
-
<input type="number" id="node-input-pollingInterval" placeholder="
|
|
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
|
|
package/nodes/datahub-input.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
this.warn(
|
|
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
|
|
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);
|
package/nodes/datahub-output.js
CHANGED
|
@@ -109,12 +109,22 @@ module.exports = function (RED) {
|
|
|
109
109
|
return { def, created: true };
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|