node-red-contrib-uos-nats 0.1.53 → 0.1.55

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.
@@ -1,334 +1,283 @@
1
1
  <script type="text/javascript">
2
2
  RED.nodes.registerType('datahub-input', {
3
- category: 'DataHub-NATS',
4
- color: '#ff9f43',
3
+ category: 'u-OS',
4
+ color: '#ff9900',
5
5
  defaults: {
6
- name: { value: '' },
7
- connection: { type: 'uos-config', required: true },
8
- providerId: { value: '', required: false },
9
- variablesText: { value: '', required: false },
10
- manualVariables: { value: '', required: false },
11
- pollingInterval: { value: 0, required: false, validate: RED.validators.number() },
6
+ name: { value: "" },
7
+ connection: { type: "uos-config", required: true },
8
+ providerId: { value: "", required: true },
9
+ variablesText: { value: "" },
10
+ manualVariables: { value: "" },
11
+ singleName: { value: "" },
12
+ singleId: { value: "" },
13
+ mode: { value: "auto" },
14
+ triggerMode: { value: "event" }, // event or poll
15
+ pollingInterval: { value: 0, validate: RED.validators.number() }
12
16
  },
13
17
  inputs: 1,
14
18
  outputs: 1,
15
19
  icon: "white/datahub-input.svg",
20
+ label: function () {
21
+ if (this.name) return this.name;
22
+ if (this.mode === 'manual_single' && this.singleName) return this.singleName;
23
+ return "u-OS " + (this.providerId || "Data Hub");
24
+ },
25
+ paletteLabel: "u-OS Input",
16
26
  oneditprepare: function () {
17
27
  const node = this;
18
- const $config = $('#node-input-connection');
19
- const $providerHidden = $('#node-input-providerId');
20
- const $providerSelect = $('#datahub-provider-select');
21
- const $providerManual = $('#datahub-provider-manual');
22
- const $providerRefresh = $('#datahub-provider-refresh');
23
- const $providerStatus = $('#datahub-provider-status');
24
28
 
25
- const $variablesHidden = $('#node-input-variablesText');
26
- const $variables = $('#datahub-input-variables'); // Note: This selector might be wrong in previous code, checking container
27
- const $variablesContainer = $('#datahub-variables-container');
28
- const $varsRefresh = $('#datahub-vars-refresh');
29
- const $variablesStatus = $('#datahub-variables-status');
29
+ // --- Mode Visibility Logic ---
30
+ const $modeSelect = $('#node-input-mode');
31
+ const $autoRow = $('#mode-auto-row');
32
+ const $manualSingleRow = $('#mode-manual-single-row');
33
+ const $manualMultiRow = $('#mode-manual-multi-row');
34
+
35
+ const updateModeVisibility = () => {
36
+ const mode = $modeSelect.val();
37
+ $autoRow.hide();
38
+ $manualSingleRow.hide();
39
+ $manualMultiRow.hide();
40
+
41
+ if (mode === 'auto') $autoRow.show();
42
+ else if (mode === 'manual_single') $manualSingleRow.show();
43
+ else if (mode === 'manual_multi') $manualMultiRow.show();
44
+ };
30
45
 
31
- const initialProvider = $providerHidden.val() || '';
32
- const initialVars = ($variablesHidden.val() || '')
33
- .split(',')
34
- .map((entry) => entry.trim())
35
- .filter((entry) => entry.length > 0);
46
+ $modeSelect.on('change', updateModeVisibility);
47
+ updateModeVisibility();
36
48
 
37
- const normalizeProviders = (payload) => {
38
- console.log('normalizeProviders payload:', payload);
39
- if (!payload) {
40
- return [];
41
- }
42
- if (Array.isArray(payload)) {
43
- return payload;
44
- }
45
- if (Array.isArray(payload.providers)) {
46
- return payload.providers;
47
- }
48
- return [];
49
- };
49
+ // --- Trigger Mode (Event/Poll) Visibility ---
50
+ const $triggerMode = $('#node-input-triggerMode');
51
+ const $pollRow = $('#poll-interval-row');
50
52
 
51
- const normalizeVariables = (payload) => {
52
- console.log('normalizeVariables payload:', payload);
53
- if (!payload) {
54
- return [];
55
- }
56
- if (Array.isArray(payload)) {
57
- return payload;
58
- }
59
- if (Array.isArray(payload.variables)) {
60
- return payload.variables;
53
+ const updateTriggerVisibility = () => {
54
+ if ($triggerMode.val() === 'poll') {
55
+ $pollRow.show();
56
+ } else {
57
+ $pollRow.hide();
61
58
  }
62
- return [];
63
59
  };
64
60
 
65
- // API returns [{ "id": "..." }] for providers
66
- const providerLabel = (provider) => provider.displayName || provider.name || provider.id || provider.providerId || 'Unnamed provider';
67
- const providerValue = (provider) => provider.id || provider.providerId || provider.name || '';
68
-
69
- // API returns [{ "key": "..." }] for variables
70
- const variableValue = (variable) => variable.key || variable.name || variable.path || variable.id || '';
71
- const variableLabel = (variable) => variable.tagName || variable.key || variable.name || variable.path || variable.id || 'Unnamed variable';
61
+ $triggerMode.on('change', updateTriggerVisibility);
62
+ updateTriggerVisibility();
72
63
 
73
- const syncVariablesField = () => {
74
- // For checkboxes, we update the hidden field directly on change.
75
- // This helper is kept if needed for initial loading but usually not needed.
76
- };
64
+ // --- Provider & Variables Loading (Auto Mode) ---
65
+ const $config = $('#node-input-connection');
66
+ const $providerSelect = $('#node-input-providerId');
67
+ const $providerRefresh = $('#node-input-provider-refresh');
68
+ const $varsContainer = $('#node-input-variables-container');
69
+ const $varsRefresh = $('#node-input-vars-refresh');
70
+ const $varsStatus = $('#vars-status');
71
+ const $variablesHidden = $('#node-input-variablesText');
77
72
 
78
73
  let providerRequest = 0;
79
74
  let variableRequest = 0;
80
75
 
81
- const setProviderStatus = (text) => {
82
- if (text) {
83
- $providerStatus.text(text).show();
84
- } else {
85
- $providerStatus.hide();
86
- }
76
+ const normalizeProviders = (payload) => {
77
+ if (Array.isArray(payload)) return payload;
78
+ if (payload && Array.isArray(payload.providers)) return payload.providers;
79
+ return [];
87
80
  };
88
- const setVariableStatus = (text) => $variablesStatus.text(text || '');
89
81
 
90
- const setProviderValue = (value) => {
91
- $providerHidden.val(value || '');
82
+ const normalizeVariables = (payload) => {
83
+ if (Array.isArray(payload)) return payload;
84
+ if (payload && Array.isArray(payload.variables)) return payload.variables;
85
+ return [];
92
86
  };
93
87
 
94
- const getProviderValue = () => $providerHidden.val() || '';
88
+ const providerValue = (p) => p.id || p.providerId || p.name;
89
+ const providerLabel = (p) => p.name || p.displayName || p.providerId || p.id;
90
+ const variableValue = (v) => v.key || v.name || v.path || v.id;
91
+ const variableLabel = (v) => v.tagName || v.key || v.name || v.path || v.id;
95
92
 
96
- const showManualProvider = (message) => {
97
- $providerSelect.hide().prop('disabled', true);
98
- $providerManual.show().prop('disabled', false);
99
- $providerManual.val(getProviderValue());
100
- setProviderStatus(message || 'Enter the provider ID manually.');
101
- };
93
+ const setVarsStatus = (msg) => $varsStatus.text(msg || '');
102
94
 
103
- const showSelectProvider = () => {
104
- $providerManual.hide().prop('disabled', true);
105
- $providerSelect.show().prop('disabled', false);
95
+ const syncVariablesField = () => {
96
+ const selected = [];
97
+ $varsContainer.find('input[type="checkbox"]:checked').each(function () {
98
+ selected.push($(this).val());
99
+ });
100
+ $variablesHidden.val(selected.join(','));
101
+ setVarsStatus(`${selected.length} variable(s) selected`);
106
102
  };
107
103
 
108
104
  const loadVariables = (providerId, keepSelection = false) => {
109
105
  const configId = $config.val();
110
106
  if (!configId || !providerId) {
111
- $variablesContainer.empty().append('<div style="color:#aaa; font-style:italic; padding:5px;">Select a provider to load variables...</div>');
112
- setVariableStatus('Select a provider to load variables.');
107
+ $varsContainer.empty().append('<div style="color:#999; padding:10px; text-align:center;">Select a provider first</div>');
108
+ setVarsStatus('');
113
109
  return;
114
110
  }
111
+
115
112
  const seq = ++variableRequest;
116
- setVariableStatus('Loading variables…');
113
+ setVarsStatus('Loading...');
114
+ $varsContainer.empty().append('<div style="color:#999; padding:10px; text-align:center;"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
117
115
 
118
116
  $.getJSON(`uos/providers/${configId}/${encodeURIComponent(providerId)}/variables`)
119
117
  .done((payload) => {
120
118
  if (seq !== variableRequest) return;
121
119
 
122
120
  const vars = normalizeVariables(payload);
123
- $variablesContainer.empty();
121
+ $varsContainer.empty();
122
+
124
123
  if (!vars.length) {
125
- setVariableStatus('No variables returned. Check Data Hub permissions.');
126
- $variablesContainer.append('<div style="color:#aaa; padding:5px;">No variables found.</div>');
127
- syncVariablesField();
124
+ $varsContainer.append('<div style="color:#999; padding:10px; text-align:center;">No variables found</div>');
125
+ setVarsStatus('No variables');
128
126
  return;
129
127
  }
130
128
 
131
- const preset = keepSelection ? ($variablesHidden.val() || '').split(',').map(e => e.trim()).filter(Boolean) : initialVars;
129
+ const preset = keepSelection
130
+ ? ($variablesHidden.val() || '').split(',').map(e => e.trim()).filter(Boolean)
131
+ : (node.variablesText || '').split(',').map(e => e.trim()).filter(Boolean);
132
132
  const currentSet = new Set(preset);
133
133
 
134
+ vars.sort((a, b) => (variableLabel(a) || '').localeCompare(variableLabel(b) || ''));
135
+
134
136
  vars.forEach((variable) => {
135
137
  const value = variableValue(variable);
136
138
  if (!value) return;
137
139
 
138
- const isChecked = currentSet.has(value) ? 'checked' : '';
139
- // Create checkbox row
140
- const row = $(`<div style="display:flex; align-items:center; margin-bottom:2px;">
141
- <input type="checkbox" id="uv-${seq}-${value}" value="${value}" ${isChecked} style="width:auto; margin:0 5px 0 0;">
142
- <label for="uv-${seq}-${value}" style="width:auto; margin-bottom:0; cursor:pointer;">${variableLabel(variable)}</label>
143
- </div>`);
144
-
145
- $variablesContainer.append(row);
146
- });
147
-
148
- // Re-bind change events for new checkboxes
149
- $variablesContainer.find('input[type="checkbox"]').on('change', () => {
150
- const selected = [];
151
- $variablesContainer.find('input[type="checkbox"]:checked').each(function () {
152
- selected.push($(this).val());
153
- });
154
- $variablesHidden.val(selected.join(','));
140
+ const isChecked = currentSet.has(value);
141
+ const row = $('<div style="margin-bottom:2px;"></div>');
142
+ const checkbox = $('<input type="checkbox">')
143
+ .val(value)
144
+ .prop('checked', isChecked)
145
+ .on('change', syncVariablesField);
146
+ const label = $('<label style="cursor:pointer; margin-bottom:0;"></label>')
147
+ .append(checkbox)
148
+ .append(' ' + variableLabel(variable));
149
+ row.append(label);
150
+ $varsContainer.append(row);
155
151
  });
156
152
 
157
153
  syncVariablesField();
158
- setVariableStatus('Select variables. Leave all unchecked to receive EVERYTHING.');
159
154
  })
160
155
  .fail((xhr) => {
161
- console.error('Variable load failed', xhr);
156
+ if (seq !== variableRequest) return;
162
157
  const error = xhr?.responseJSON?.error;
163
- let msg = error || xhr.statusText || 'Failed to load variables.';
158
+ let msg = error || xhr.statusText || 'Failed to load';
164
159
  if (xhr.status === 404 && error === 'config not found') {
165
- msg = 'Config not deployed yet. Please Deploy first to load Variables.';
160
+ msg = 'Config not deployed. Deploy first.';
166
161
  }
167
- setVariableStatus(msg);
168
- $variablesContainer.empty().append('<div style="color:#aaa; font-style:italic; padding:5px;">To load variables, the Config Node must be deployed first.</div>');
169
- syncVariablesField();
162
+ $varsContainer.empty().append(`<div style="color:#c55; padding:10px;">${msg}</div>`);
163
+ setVarsStatus('Error');
170
164
  });
171
165
  };
172
166
 
173
167
  const loadProviders = (force = false) => {
174
168
  const configId = $config.val();
175
169
  if (!configId) {
176
- $providerSelect.empty().append('<option value="">Select a u-OS config first</option>').prop('disabled', true);
177
- setVariableStatus('');
178
- $variablesContainer.empty().append('<div style="color:#aaa; font-style:italic; padding:5px;">Select a provider to load variables...</div>');
170
+ $providerSelect.empty().append('<option value="">Select Config first</option>').prop('disabled', true);
179
171
  return;
180
172
  }
181
173
 
182
174
  const seq = ++providerRequest;
183
-
184
- // Use cache only if not forced
185
- if (!force && window.uOSProvidersCache && window.uOSProvidersCache[configId]) {
186
- console.log('Using cached providers for', configId);
187
- setTimeout(() => {
188
- if (seq === providerRequest) populateProviders(window.uOSProvidersCache[configId]);
189
- }, 0);
190
- return;
191
- }
192
-
193
- setProviderStatus('Loading providers…');
194
175
  $providerSelect.prop('disabled', true);
195
- // Removed showManualProvider/showSelectProvider logic - always assume Select
196
176
 
197
177
  $.getJSON(`uos/providers/${configId}`)
198
178
  .done((payload) => {
199
179
  if (seq !== providerRequest) return;
200
- // Update cache on success
201
180
  const list = normalizeProviders(payload);
202
- if (window.uOSProvidersCache) window.uOSProvidersCache[configId] = list;
203
- populateProviders(payload);
204
- })
205
- .fail((xhr) => {
206
- console.error('Provider load failed', xhr);
207
- const error = xhr?.responseJSON?.error;
208
- let msg = error || xhr.statusText || 'Failed to load providers.';
209
- if (xhr.status === 404 && error === 'config not found') {
210
- msg = 'Config not deployed yet. Please use "Test Connection" in Config Node or Deploy first.';
181
+ $providerSelect.empty();
182
+
183
+ if (!list.length) {
184
+ $providerSelect.append('<option value="">No providers found</option>').prop('disabled', true);
185
+ return;
211
186
  }
212
- $providerSelect.empty().append('<option value="">Error loading providers</option>').prop('disabled', true);
213
- setProviderStatus(`Error: ${msg} - Click Refresh to retry.`);
214
- // Do NOT switch to manual input
215
- });
216
- };
217
187
 
218
- const populateProviders = (payload) => {
219
- const list = normalizeProviders(payload);
220
- $providerSelect.empty();
221
- if (!list.length) {
222
- $providerSelect.append('<option value="">No providers found</option>').prop('disabled', true);
223
- setProviderStatus('No providers found. Verify Data Hub access.');
224
- return;
225
- }
226
- let foundInitial = false;
227
- list.forEach((prov) => {
228
- const value = providerValue(prov);
229
- if (!value) return;
230
- const option = $('<option></option>').attr('value', value).text(providerLabel(prov));
231
- $providerSelect.append(option);
232
- if (value === initialProvider) foundInitial = true;
233
- });
234
- $providerSelect.prop('disabled', false);
235
-
236
- if (initialProvider && foundInitial) {
237
- $providerSelect.val(initialProvider);
238
- } else if (initialProvider) {
239
- // Valid case: provider set but not in list? Or manual override needed?
240
- // User said "no input", so we just show what we have.
241
- // Optionally add the current unknown provider as an option
242
- $providerSelect.append($('<option></option>').attr('value', initialProvider).text(initialProvider + ' (unknown)'));
243
- $providerSelect.val(initialProvider);
244
- }
188
+ let foundInitial = false;
189
+ list.forEach((prov) => {
190
+ const val = providerValue(prov);
191
+ if (!val) return;
192
+ const opt = $('<option></option>').val(val).text(providerLabel(prov));
193
+ $providerSelect.append(opt);
194
+ if (val === node.providerId) foundInitial = true;
195
+ });
196
+ $providerSelect.prop('disabled', false);
245
197
 
246
- // Select first if nothing selected
247
- if (!$providerSelect.val()) {
248
- $providerSelect.val($providerSelect.find('option:first').attr('value'));
249
- }
198
+ if (node.providerId && foundInitial) {
199
+ $providerSelect.val(node.providerId);
200
+ } else if (node.providerId) {
201
+ $providerSelect.append($('<option></option>').val(node.providerId).text(node.providerId + ' (unknown)'));
202
+ $providerSelect.val(node.providerId);
203
+ }
250
204
 
251
- const selected = $providerSelect.val();
252
- setProviderValue(selected);
253
- setProviderStatus('');
254
- loadVariables(selected, false); // Reload vars for selected
255
- };
205
+ if (!$providerSelect.val()) {
206
+ $providerSelect.val($providerSelect.find('option:first').val());
207
+ }
256
208
 
257
- $variables.on('change', () => {
258
- syncVariablesField();
259
- });
209
+ const selected = $providerSelect.val();
210
+ if (selected) loadVariables(selected, false);
211
+ })
212
+ .fail((xhr) => {
213
+ if (seq !== providerRequest) return;
214
+ $providerSelect.empty().append('<option value="">Error loading providers</option>').prop('disabled', true);
215
+ });
216
+ };
260
217
 
261
218
  $providerSelect.on('change', () => {
262
- const value = $providerSelect.val();
263
- setProviderValue(value);
264
219
  $variablesHidden.val('');
265
- syncVariablesField();
266
- loadVariables(value, false);
267
- });
268
-
269
- $providerRefresh.on('click', () => {
270
- loadProviders(true); // Force refresh
220
+ const val = $providerSelect.val();
221
+ if (val) loadVariables(val, false);
271
222
  });
272
223
 
224
+ $providerRefresh.on('click', () => loadProviders(true));
273
225
  $varsRefresh.on('click', () => {
274
- const prov = $providerSelect.val();
275
- if (prov) loadVariables(prov, true);
226
+ const val = $providerSelect.val();
227
+ if (val) loadVariables(val, true);
276
228
  });
277
229
 
278
- $('#datahub-vars-all').on('click', () => {
279
- $variablesContainer.find('input[type="checkbox"]').prop('checked', true).trigger('change');
230
+ $('#node-input-select-all').on('click', () => {
231
+ $varsContainer.find('input[type="checkbox"]').prop('checked', true).trigger('change');
280
232
  });
281
233
 
282
- $('#datahub-vars-none').on('click', () => {
283
- $variablesContainer.find('input[type="checkbox"]').prop('checked', false).trigger('change');
234
+ $('#node-input-select-none').on('click', () => {
235
+ $varsContainer.find('input[type="checkbox"]').prop('checked', false).trigger('change');
284
236
  });
285
237
 
286
- // Removed $providerManual listeners as per instruction
287
-
288
238
  $config.on('change', () => {
289
- loadProviders(false);
239
+ setTimeout(() => loadProviders(false), 50);
290
240
  });
291
241
 
292
- syncVariablesField();
293
- // Delay provider loading slightly to ensure the config typedInput is ready.
294
- setTimeout(() => loadProviders(), 100);
295
- // Manual Definitions Table
296
- const $manualList = $('#node-input-manual-defs-container');
242
+ setTimeout(() => loadProviders(false), 100);
243
+
244
+ // --- Manual Table Logic ---
245
+ const $manualList = $('#node-input-manual-def-list');
297
246
  const $manualHidden = $('#node-input-manualVariables');
298
247
 
299
248
  $manualList.editableList({
300
249
  addItem: function (row, index, data) {
301
250
  row.css({ display: 'flex', alignItems: 'center', gap: '5px' });
302
251
 
303
- $('<input/>', { class: 'node-input-manual-name', type: 'text', placeholder: 'Name (e.g. manufacturer_name)', style: 'flex:1;' })
252
+ $('<input/>', { class: 'node-input-manual-name', type: 'text', placeholder: 'Name', style: 'flex:1;' })
304
253
  .val(data.name || '')
305
254
  .appendTo(row);
306
255
 
307
- $('<input/>', { class: 'node-input-manual-id', type: 'number', placeholder: 'ID', style: 'width:100px;' })
256
+ $('<input/>', { class: 'node-input-manual-id', type: 'number', placeholder: 'ID', style: 'width:80px;' })
308
257
  .val(data.id || '')
309
258
  .appendTo(row);
310
259
  },
311
260
  removable: true,
312
261
  header: $('<div></div>').append(
313
- $.parseHTML('<div style="display:flex; gap:5px; padding-left:5px;"><div style="flex:1;">Variable Name (Key)</div><div style="width:100px;">ID (Int)</div></div>')
262
+ $.parseHTML('<div style="display:flex; gap:5px; padding-left:5px;"><div style="flex:1;">Variable Name</div><div style="width:80px;">ID</div></div>')
314
263
  ),
315
- addButton: 'Add Mapping'
264
+ addButton: 'Add Variable'
316
265
  });
317
266
 
318
- // Populate Table from Hidden String "name:id, name2:id2"
319
- const currentManual = $manualHidden.val() || '';
267
+ const currentManual = node.manualVariables || '';
320
268
  if (currentManual) {
321
269
  currentManual.split(',').forEach(entry => {
322
270
  const parts = entry.split(':');
323
271
  if (parts.length === 2) {
324
- $manualList.editableList('addItem', { name: parts[0].trim(), id: parts[1].trim() });
272
+ const n = parts[0].trim();
273
+ const i = parts[1].trim();
274
+ if (n && i) $manualList.editableList('addItem', { name: n, id: i });
325
275
  }
326
276
  });
327
277
  }
328
-
329
278
  },
330
279
  oneditsave: function () {
331
- const $manualList = $('#node-input-manual-defs-container');
280
+ const $manualList = $('#node-input-manual-def-list');
332
281
  const items = [];
333
282
  $manualList.editableList('items').each(function () {
334
283
  const name = $(this).find('.node-input-manual-name').val().trim();
@@ -338,110 +287,106 @@
338
287
  }
339
288
  });
340
289
  $('#node-input-manualVariables').val(items.join(','));
341
- },
342
- label: function () {
343
- return this.name || `Input ${this.providerId || ''}`;
344
- },
345
- labelStyle: function () {
346
- return this.name ? 'node_label_italic' : '';
347
290
  }
348
291
  });
349
292
  </script>
350
293
 
351
294
  <script type="text/html" data-template-name="datahub-input">
352
- <div class="form-row">
353
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
354
- <input type="text" id="node-input-name">
355
- </div>
356
- <!-- Polling (ms) removed in favor of Input Port triggering -->
357
- <div class="form-row">
358
- <label for="node-input-connection"><i class="fa fa-cube"></i> u-OS Config</label>
359
- <input type="text" id="node-input-connection">
295
+ <div class="form-row">
360
296
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
361
- <input type="text" id="node-input-name" placeholder="Name">
297
+ <input type="text" id="node-input-name" placeholder="Optional">
362
298
  </div>
299
+
363
300
  <div class="form-row">
364
301
  <label for="node-input-connection"><i class="fa fa-globe"></i> Config</label>
365
302
  <input type="text" id="node-input-connection">
366
303
  </div>
304
+
367
305
  <div class="form-row">
368
306
  <label for="node-input-providerId"><i class="fa fa-server"></i> Provider</label>
369
- <div style="display: inline-flex; width: 70%;">
370
- <select id="node-input-providerId" style="flex-grow: 1;"></select>
371
- <button id="node-input-check-custom-provider" class="red-ui-button" style="margin-left: 5px;">Refresh</button>
307
+ <div style="display:inline-flex; width:70%;">
308
+ <select id="node-input-providerId" style="flex:1;"></select>
309
+ <button id="node-input-provider-refresh" class="red-ui-button" type="button" style="margin-left:5px;">
310
+ <i class="fa fa-refresh"></i>
311
+ </button>
372
312
  </div>
373
313
  </div>
314
+
374
315
  <div class="form-row">
375
316
  <label for="node-input-mode"><i class="fa fa-sliders"></i> Mode</label>
376
317
  <select id="node-input-mode">
377
- <option value="auto">Auto-Discovery (Select from List)</option>
318
+ <option value="auto">Auto-Discovery</option>
378
319
  <option value="manual_single">Manual: Single Variable</option>
379
- <option value="manual_multi">Manual: Multi Variable (Table)</option>
320
+ <option value="manual_multi">Manual: Multiple Variables</option>
380
321
  </select>
381
322
  </div>
382
323
 
383
- <!-- Mode: Auto-Discovery -->
384
- <div id="mode-auto-row" class="mode-row">
385
- <div class="form-row" style="margin-bottom: 0px;">
324
+ <!-- Auto Mode -->
325
+ <div id="mode-auto-row" class="mode-row" style="display:none;">
326
+ <div class="form-row">
386
327
  <label><i class="fa fa-list"></i> Variables</label>
387
- <div style="display: inline-block; width: 70%;">
388
- <div style="margin-bottom: 5px;">
389
- <button id="node-input-refresh-vars" class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i> Refresh</button>
390
- <button id="node-input-select-all" class="red-ui-button red-ui-button-small" style="margin-left: 5px;">Select All</button>
391
- <button id="node-input-select-none" class="red-ui-button red-ui-button-small" style="margin-left: 5px;">None</button>
392
- <span id="vars-status" style="margin-left: 10px; font-style: italic; color: #888;"></span>
328
+ <div style="display:inline-block; width:70%;">
329
+ <div style="margin-bottom:5px;">
330
+ <button id="node-input-vars-refresh" class="red-ui-button red-ui-button-small" type="button">
331
+ <i class="fa fa-refresh"></i> Refresh
332
+ </button>
333
+ <button id="node-input-select-all" class="red-ui-button red-ui-button-small" type="button" style="margin-left:5px;">All</button>
334
+ <button id="node-input-select-none" class="red-ui-button red-ui-button-small" type="button" style="margin-left:5px;">None</button>
335
+ <span id="vars-status" style="margin-left:10px; font-style:italic; color:#888;"></span>
393
336
  </div>
394
337
  </div>
395
338
  </div>
396
339
  <div class="form-row">
397
340
  <input type="hidden" id="node-input-variablesText">
398
- <div id="node-input-variables-container" style="border: 1px solid #ccc; border-radius: 4px; height: 250px; overflow-y: scroll; padding: 5px; background: #fff; width: 100%; box-sizing: border-box;">
399
- <!-- Checkboxes will be injected here -->
400
- <div style="padding: 10px; text-align: center; color: #888;">
401
- Select a Provider to load variables...
402
- </div>
403
- </div>
404
- <div class="form-tips" style="margin-top: 5px;">
405
- <i class="fa fa-info-circle"></i> Requires <b>Deploy</b> or active connection.
341
+ <div id="node-input-variables-container" style="border:1px solid #ccc; border-radius:4px; height:200px; overflow-y:auto; padding:5px; background:#fff; width:100%;"></div>
342
+ <div class="form-tips" style="margin-top:5px;">
343
+ <i class="fa fa-info-circle"></i> Requires <b>Deploy</b> first
406
344
  </div>
407
345
  </div>
408
346
  </div>
409
347
 
410
- <!-- Mode: Manual Single -->
411
- <div id="mode-manual-single-row" class="mode-row">
348
+ <!-- Manual Single Mode -->
349
+ <div id="mode-manual-single-row" class="mode-row" style="display:none;">
412
350
  <div class="form-row">
413
- <label for="node-input-singleName"><i class="fa fa-font"></i> Var Name</label>
414
- <input type="text" id="node-input-singleName" placeholder="e.g. digital_nameplate.manufacturer_name">
351
+ <label for="node-input-singleName"><i class="fa fa-font"></i> Variable Name</label>
352
+ <input type="text" id="node-input-singleName" placeholder="e.g. manufacturer_name">
415
353
  </div>
416
354
  <div class="form-row">
417
- <label for="node-input-singleId"><i class="fa fa-hashtag"></i> Var ID</label>
355
+ <label for="node-input-singleId"><i class="fa fa-hashtag"></i> Variable ID</label>
418
356
  <input type="number" id="node-input-singleId" placeholder="e.g. 0">
419
357
  </div>
420
358
  <div class="form-tips">
421
- <i class="fa fa-info-circle"></i> <b>Manual Mode:</b> Use this if Auto-Discovery fails (Permissions/403). Enter the internal ID from your system.
359
+ <i class="fa fa-info-circle"></i> Use Manual Mode if Auto-Discovery fails (Permission 403)
422
360
  </div>
423
361
  </div>
424
362
 
425
- <!-- Mode: Manual Multi -->
426
- <div id="mode-manual-multi-row" class="mode-row">
363
+ <!-- Manual Multi Mode -->
364
+ <div id="mode-manual-multi-row" class="mode-row" style="display:none;">
427
365
  <div class="form-row">
428
- <label for="node-input-manualVariables-container"><i class="fa fa-table"></i> Mapping</label>
429
- <div class="form-row node-input-manualVariables-container-row" style="width: 100%; margin-top: 8px;">
366
+ <label><i class="fa fa-table"></i> Variables</label>
367
+ <div style="width:100%; margin-top:8px;">
430
368
  <ol id="node-input-manual-def-list"></ol>
431
369
  </div>
432
- <!-- Hidden field to store JSON string of definitions -->
433
- <input type="hidden" id="node-input-manualVariables" />
370
+ <input type="hidden" id="node-input-manualVariables">
434
371
  </div>
435
372
  <div class="form-tips">
436
- <i class="fa fa-info-circle"></i> <b>Manual Table:</b> Map strictly "Name -> ID". Only variables listed here will be output.
373
+ <i class="fa fa-info-circle"></i> Map Variable Names to IDs
437
374
  </div>
438
375
  </div>
439
376
 
440
- <hr>
441
-
377
+ <hr style="margin:15px 0;">
378
+
442
379
  <div class="form-row">
443
- <label for="node-input-pollingInterval"><i class="fa fa-clock-o"></i> Poll (ms)</label>
444
- <input type="number" id="node-input-pollingInterval" placeholder="0 (Disabled)">
380
+ <label for="node-input-triggerMode"><i class="fa fa-bolt"></i> Trigger</label>
381
+ <select id="node-input-triggerMode">
382
+ <option value="event">Event (on change)</option>
383
+ <option value="poll">Poll (interval)</option>
384
+ </select>
385
+ </div>
386
+
387
+ <div id="poll-interval-row" class="form-row" style="display:none;">
388
+ <label for="node-input-pollingInterval"><i class="fa fa-clock-o"></i> Interval (ms)</label>
389
+ <input type="number" id="node-input-pollingInterval" placeholder="e.g. 1000">
445
390
  </div>
446
391
  </script>
447
392
 
@@ -453,303 +398,28 @@
453
398
  <dt>Mode</dt>
454
399
  <dd>
455
400
  <ul>
456
- <li><b>Auto-Discovery:</b> Connects to the hub, downloads the variable list, and lets you select variables via checkboxes. Requires correct permissions (`hub.variables.readonly`).</li>
457
- <li><b>Manual (Single):</b> For reading a single variable when Discovery is blocked. Enter Name and ID manually.</li>
458
- <li><b>Manual (Multi):</b> For reading multiple variables when Discovery is blocked. Use the table to map Names to IDs.</li>
401
+ <li><b>Auto-Discovery:</b> Downloads variable list from hub (requires permissions)</li>
402
+ <li><b>Manual Single:</b> Enter one variable's Name and ID manually</li>
403
+ <li><b>Manual Multi:</b> Enter multiple variables via table</li>
459
404
  </ul>
460
405
  </dd>
461
406
 
462
- <dt>Provider</dt>
463
- <dd>The Data Hub Provider ID (e.g. `u_os_sbm`).</dd>
464
-
465
- <dt>Polling</dt>
466
- <dd>Interval in ms to force-read values. 0 = Disabled (Wait for Change Events).</dd>
407
+ <dt>Trigger</dt>
408
+ <dd>
409
+ <ul>
410
+ <li><b>Event:</b> Outputs only when value changes (default)</li>
411
+ <li><b>Poll:</b> Queries values at fixed interval</li>
412
+ </ul>
413
+ </dd>
467
414
  </dl>
468
415
 
469
- <h3>Important: Permission Issues / 403</h3>
416
+ <h3>Permissions / 403 Error</h3>
470
417
  <p>
471
- If you see "Empty List" or "Permission Violation", you must use one of the <b>Manual Modes</b>.
472
- Auto-Discovery relies on endpoints that are often restricted.
473
- In Manual Mode, you provide the `ID` (Number) which works even without Discovery permissions.
418
+ If Auto-Discovery fails with "Permission Violation" or "Empty List", use <b>Manual Mode</b>.
419
+ You need the Variable ID (number) which works even without discovery permissions.
474
420
  </p>
475
421
 
476
- <div style="margin-top: 10px; font-size: 0.8em; color: #666; border-top: 1px solid #eee; padding-top: 5px;">
422
+ <div style="margin-top:10px; font-size:0.8em; color:#666; border-top:1px solid #eee; padding-top:5px;">
477
423
  Developed by <a href="https://github.com/iotueli" target="_blank">iotueli</a>. Not an official Weidmüller product.
478
424
  </div>
479
- </script>
480
-
481
- <script type="text/javascript">
482
- RED.nodes.registerType('datahub-input', {
483
- category: 'u-OS',
484
- color: '#ff9900',
485
- defaults: {
486
- name: { value: "" },
487
- connection: { type: "uos-config", required: true },
488
- providerId: { value: "", required: true },
489
- variablesText: { value: "" }, // Legacy / Auto-Mode list
490
- manualVariables: { value: "" }, // Manual Table JSON/CSV
491
- singleName: { value: "" }, // Manual Single Name
492
- singleId: { value: "" }, // Manual Single ID
493
- mode: { value: "auto" }, // auto | manual_single | manual_multi
494
- pollingInterval: { value: 0, validate: RED.validators.number() }
495
- },
496
- inputs: 1,
497
- outputs: 1,
498
- icon: "datahub-input.svg",
499
- label: function () {
500
- if (this.name) return this.name;
501
- if (this.mode === 'manual_single' && this.singleName) return this.singleName;
502
- return "u-OS " + (this.providerId || "Data Hub");
503
- },
504
- paletteLabel: "u-OS Data Hub",
505
- oneditprepare: function () {
506
- const node = this;
507
-
508
- // --- 1. Mode Visibility Logic ---
509
- const modeSelect = $('#node-input-mode');
510
- const autoRow = $('#mode-auto-row');
511
- const manualSingleRow = $('#mode-manual-single-row');
512
- const manualMultiRow = $('#mode-manual-multi-row');
513
-
514
- // Set default if missing (backward compat)
515
- if (!$('#node-input-mode').val()) {
516
- $('#node-input-mode').val('auto');
517
- }
518
-
519
- const updateVisibility = () => {
520
- const mode = modeSelect.val();
521
- autoRow.hide();
522
- manualSingleRow.hide();
523
- manualMultiRow.hide();
524
-
525
- if (mode === 'auto') autoRow.show();
526
- else if (mode === 'manual_single') manualSingleRow.show();
527
- else if (mode === 'manual_multi') manualMultiRow.show();
528
- };
529
-
530
- modeSelect.on('change', updateVisibility);
531
- updateVisibility(); // Initial trigger
532
-
533
- // --- 2. Existing "Auto" Logic (Providers & Checkboxes) ---
534
- const $config = $('#node-input-connection');
535
- const $providerRefresh = $('#node-input-check-custom-provider');
536
- const $variables = $('input[type="checkbox"]');
537
- const $variablesHidden = $('#node-input-variablesText');
538
- const $varsRefresh = $('#node-input-refresh-vars');
539
-
540
- let initialProvider = node.providerId;
541
- let providerRequest = 0;
542
-
543
- // Define Helpers (providerValue, providerLabel etc) from original code
544
- const providerValue = (p) => p.id || p.providerId || p.name;
545
- const providerLabel = (p) => p.name || p.displayName || p.providerId || p.id;
546
-
547
- // --- 3. Existing Loading Logic ---
548
- const setProviderStatus = (msg) => { /* No-op or custom div if needed */ };
549
- const setProviderValue = (val) => { $('#node-input-providerId').val(val); };
550
-
551
- const setVariableStatus = (msg) => { $('#vars-status').text(msg); };
552
-
553
- const normalizeVariables = (payload) => {
554
- if (Array.isArray(payload)) return payload;
555
- if (payload && Array.isArray(payload.variables)) return payload.variables;
556
- return [];
557
- };
558
-
559
- const normalizeProviders = (payload) => {
560
- if (Array.isArray(payload)) return payload;
561
- if (payload && Array.isArray(payload.providers)) return payload.providers;
562
- return [];
563
- };
564
-
565
- const variableValue = (v) => v.key || v.name || v.path || v.id;
566
- const variableLabel = (v) => v.tagName || v.key || v.name || v.path || v.id;
567
-
568
- const syncVariablesField = () => {
569
- const selected = [];
570
- varsContainer.find('input[type="checkbox"]:checked').each(function () {
571
- selected.push($(this).val());
572
- });
573
- varsHiddenInput.val(selected.join(','));
574
- setVariableStatus(`${selected.length} selected`);
575
- };
576
-
577
- const loadVariables = (providerId, force = false) => {
578
- const configId = $config.val();
579
- if (!configId || !providerId) return;
580
-
581
- setVariableStatus('Loading...');
582
- varsContainer.empty().append('<div style="padding:10px; text-align:center; color:#888;"><i class="fa fa-spinner fa-spin"></i> Loading variables...</div>');
583
-
584
- $.getJSON(`uos/providers/${configId}/${providerId}/variables`)
585
- .done((payload) => {
586
- const list = normalizeVariables(payload);
587
- varsContainer.empty();
588
- if (!list.length) {
589
- varsContainer.append('<div style="padding:10px; text-align:center; color:#888;">No variables found for this provider.</div>');
590
- setVariableStatus('No variables found');
591
- return;
592
- }
593
-
594
- const currentVal = varsHiddenInput.val().split(',');
595
- // Sort by name/key
596
- list.sort((a, b) => (variableLabel(a) || '').localeCompare(variableLabel(b) || ''));
597
-
598
- list.forEach((v) => {
599
- const val = variableValue(v);
600
- const lbl = variableLabel(v);
601
- const checked = currentVal.includes(val) ? 'checked' : '';
602
- const row = $('<div/>').appendTo(varsContainer);
603
- $('<label/>').css({ display: 'block', cursor: 'pointer' })
604
- .append($('<input type="checkbox"/>').val(val).prop('checked', !!checked).on('change', syncVariablesField))
605
- .append($('<span/>').text(' ' + lbl))
606
- .appendTo(row);
607
- });
608
- syncVariablesField();
609
- })
610
- .fail((xhr) => {
611
- const error = xhr?.responseJSON?.error;
612
- let msg = error || xhr.statusText || 'Failed to load variables.';
613
- if (xhr.status === 404 && error === 'config not found') {
614
- msg = 'Config not deployed yet. Please Deploy first.';
615
- }
616
- varsContainer.empty().append(`<div style="padding:10px; color:#d66;">${msg}</div>`);
617
- setVariableStatus('Error loading variables');
618
- });
619
- };
620
-
621
- const populateProviders = (payload) => {
622
- const list = normalizeProviders(payload);
623
- providerSelect.empty();
624
- if (!list.length) {
625
- providerSelect.append('<option value="">No providers found</option>').prop('disabled', true);
626
- return;
627
- }
628
- let foundInitial = false;
629
- list.forEach((prov) => {
630
- const val = providerValue(prov);
631
- if (!val) return;
632
- const opt = $('<option/>').val(val).text(providerLabel(prov));
633
- providerSelect.append(opt);
634
- if (val === initialProvider) foundInitial = true;
635
- });
636
- providerSelect.prop('disabled', false);
637
-
638
- if (initialProvider && foundInitial) {
639
- providerSelect.val(initialProvider);
640
- } else if (initialProvider) {
641
- // Preserve unknown provider (e.g. from manual entry or offline)
642
- providerSelect.append($('<option/>').val(initialProvider).text(initialProvider + ' (unknown)'));
643
- providerSelect.val(initialProvider);
644
- }
645
-
646
- if (!providerSelect.val()) {
647
- providerSelect.val(providerSelect.find('option:first').val());
648
- }
649
-
650
- // Trigger variable load for selected
651
- const selected = providerSelect.val();
652
- if (selected) loadVariables(selected);
653
- };
654
-
655
- const loadProviders = (force = false) => {
656
- const configId = $config.val();
657
- if (!configId) {
658
- providerSelect.empty().append('<option value="">Select Config Node first</option>').prop('disabled', true);
659
- return;
660
- }
661
-
662
- const seq = ++providerRequest;
663
- // Cache logic omitted for brevity/safety - simplified direct load
664
- providerSelect.prop('disabled', true);
665
-
666
- $.getJSON(`uos/providers/${configId}`)
667
- .done((payload) => {
668
- if (seq !== providerRequest) return;
669
- populateProviders(payload);
670
- })
671
- .fail((xhr) => {
672
- const error = xhr?.responseJSON?.error;
673
- const msg = error || 'Failed to load providers';
674
- providerSelect.empty().append('<option value="">Error loading providers</option>').prop('disabled', true);
675
- });
676
- };
677
-
678
- // Event Listeners
679
- providerSelect.on('change', () => {
680
- const val = providerSelect.val();
681
- varsHiddenInput.val('');
682
- syncVariablesField();
683
- loadVariables(val);
684
- });
685
-
686
- $providerRefresh.on('click', () => loadProviders(true));
687
- $varsRefresh.on('click', () => {
688
- const v = providerSelect.val();
689
- if (v) loadVariables(v, true);
690
- });
691
-
692
- $('#node-input-select-all').on('click', () => {
693
- varsContainer.find('input[type="checkbox"]').prop('checked', true).trigger('change');
694
- });
695
- $('#node-input-select-none').on('click', () => {
696
- varsContainer.find('input[type="checkbox"]').prop('checked', false).trigger('change');
697
- });
698
-
699
- $config.on('change', () => {
700
- setTimeout(() => loadProviders(false), 50);
701
- });
702
-
703
- // Initial Load
704
- setTimeout(() => loadProviders(false), 100);
705
-
706
-
707
- // --- 4. Manual Table Logic ---
708
- const $manualList = $('#node-input-manual-def-list');
709
- const $manualHidden = $('#node-input-manualVariables');
710
-
711
- $manualList.editableList({
712
- addItem: function (row, index, data) {
713
- row.css({ display: 'flex', alignItems: 'center', gap: '5px' });
714
-
715
- $('<input/>', { class: 'node-input-manual-name', type: 'text', placeholder: 'Name', style: 'flex:1;' })
716
- .val(data.name || '')
717
- .appendTo(row);
718
-
719
- $('<input/>', { class: 'node-input-manual-id', type: 'number', placeholder: 'ID', style: 'width:80px;' })
720
- .val(data.id || '')
721
- .appendTo(row);
722
- },
723
- removable: true,
724
- header: $('<div></div>').append(
725
- $.parseHTML('<div style="display:flex; gap:5px; padding-left:5px;"><div style="flex:1;">Name (String)</div><div style="width:80px;">ID (Int)</div></div>')
726
- ),
727
- addButton: 'Add Mapping'
728
- });
729
-
730
- // Populate Table
731
- const currentManual = $manualHidden.val() || '';
732
- if (currentManual) {
733
- currentManual.split(',').forEach(entry => {
734
- const parts = entry.split(':');
735
- if (parts.length === 2) {
736
- const n = parts[0].trim();
737
- const i = parts[1].trim();
738
- if (n && i) $manualList.editableList('addItem', { name: n, id: i });
739
- }
740
- });
741
- }
742
-
743
- },
744
- oneditsave: function () {
745
- const $manualList = $('#node-input-manual-def-list');
746
- const items = [];
747
- $manualList.editableList('items').each(function () {
748
- const name = $(this).find('.node-input-manual-name').val().trim();
749
- const id = $(this).find('.node-input-manual-id').val().trim();
750
- if (name && id) {
751
- items.push(`${name}:${id}`);
752
- }
753
- });
754
- $('#node-input-manualVariables').val(items.join(','));
755
- }
425
+ </script>
@@ -27,9 +27,9 @@ module.exports = function (RED) {
27
27
  }
28
28
 
29
29
  this.providerId = config.providerId || 'sampleprovider';
30
- this.providerId = config.providerId || 'sampleprovider';
31
- this.pollingInterval = parseInt(config.pollingInterval, 10) || 0; // ms
32
30
  this.mode = config.mode || 'auto';
31
+ this.triggerMode = config.triggerMode || 'event';
32
+ this.pollingInterval = this.triggerMode === 'poll' ? (parseInt(config.pollingInterval, 10) || 1000) : 0;
33
33
 
34
34
  const text = config.variablesText || '';
35
35
  const manualText = config.manualVariables || '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "Node-RED nodes for u-OS Data Hub via NATS",
5
5
  "repository": {
6
6
  "type": "git",