node-red-contrib-uos-nats 1.3.5 → 1.3.46

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
@@ -56,7 +56,7 @@ Restart Node-RED. The nodes will appear in the **"Weidmüller DataHub"** categor
56
56
  Import this flow to test reading and writing immediately:
57
57
 
58
58
  ```json
59
- [{"id":"cdad2fa96dc6eeec","type":"datahub-input","z":"c221537c994b056a","name":"Read Zipcode","connection":"a0ba0e15c8dad779","providerId":"u_os_adm","manualVariables":"digital_nameplate.address_information.zipcode:2","triggerMode":"poll","pollingInterval":"1000","x":190,"y":100,"wires":[["315d179d66bf9b93"]]},{"id":"315d179d66bf9b93","type":"debug","z":"c221537c994b056a","name":"Debug Output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":440,"y":100,"wires":[]},{"id":"a0ba0e15c8dad779","type":"uos-config","host":"127.0.0.1","port":49360,"clientName":"hub"}]
59
+ [{"id":"cdad2fa96dc6eeec","type":"datahub-input","z":"c221537c994b056a","name":"Read Zipcode","connection":"a0ba0e15c8dad779","providerId":"u_os_adm","manualVariables":"digital_nameplate.address_information.zipcode:2","triggerMode":"event","pollingInterval":"1000","x":190,"y":100,"wires":[["315d179d66bf9b93"]]},{"id":"315d179d66bf9b93","type":"debug","z":"c221537c994b056a","name":"Debug Output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":440,"y":100,"wires":[]},{"id":"a0ba0e15c8dad779","type":"uos-config","clientName":"nodered-client"}]
60
60
  ```
61
61
 
62
62
  ---
@@ -92,7 +92,13 @@ Publishes your own data to the Data Hub.
92
92
  ## Troubleshooting
93
93
 
94
94
  - **Provider not visible?** Ensure **Provider ID** matches your **Client ID**. Easiest way: Leave Provider ID empty in the node.
95
- - **Connection Failed?** Check Host/IP and ensure Client ID/Secret are correct.
95
+ - **Node Status is Yellow?**
96
+ - `cooldown (10s)`: The node is waiting to protect the device. This is normal after an error.
97
+ - `provider offline`: The connection to NATS is OK, but the target (e.g. `u_os_sbm`) is not responding (503).
98
+ - `auth failed`: Check your OAuth Client Secret and Scopes.
99
+ - **Node Status is Red?**
100
+ - `illegal ID`: You used a reserved name like `u_os_sbm`. Rename your Client/Provider.
101
+ - `write error`: A command failed. Check Scopes (`hub.variables.readwrite`) or Fingerprint.
96
102
  - **Variable ID "undefined" or "ERR"?**
97
103
  - The ID column is hidden by default to avoid confusion. The node handles ID resolution automatically.
98
104
  - If a variable fails, check if the Key (name) is correct on the Data Hub.
package/lib/subjects.js CHANGED
@@ -18,3 +18,6 @@ export function readProviderDefinitionQuery(providerId) {
18
18
  export function registryStateEvent() {
19
19
  return `${VERSION_PREFIX}.${LOCATION_PREFIX}.registry.state.evt.changed`;
20
20
  }
21
+ export function writeVariablesCommand(providerId) {
22
+ return `${VERSION_PREFIX}.${LOCATION_PREFIX}.${providerId}.vars.cmd.write`;
23
+ }
@@ -5,7 +5,7 @@
5
5
  defaults: {
6
6
  name: { value: "" },
7
7
  connection: { type: "uos-config", required: true },
8
- providerId: { value: "", required: true },
8
+ providerId: { value: "", required: true }, // Changed default to empty to encourage selection
9
9
  manualVariables: { value: "" },
10
10
  triggerMode: { value: "event" },
11
11
  pollingInterval: { value: 0, validate: RED.validators.number() },
@@ -16,7 +16,7 @@
16
16
  icon: "datahub-read.svg",
17
17
  label: function () {
18
18
  if (this.name) return this.name;
19
- return "DataHub - Read";
19
+ return this.providerId || "DataHub - Read";
20
20
  },
21
21
  paletteLabel: "DataHub - Read",
22
22
  oneditprepare: function () {
@@ -30,11 +30,16 @@
30
30
 
31
31
  // Convert stored ms to unit for display
32
32
  let currentMs = node.pollingInterval || 0;
33
- let uiVal = currentMs;
34
33
  let uiUnit = node.pollingUnit || 'ms';
34
+ let uiVal = currentMs;
35
35
 
36
- // Heuristic helper if unit was missing (legacy support)
37
- if (!node.pollingUnit && currentMs > 0) {
36
+ // If unit is set, reverse convert for display
37
+ if (uiUnit === 'min') {
38
+ uiVal = currentMs / 60000;
39
+ } else if (uiUnit === 's') {
40
+ uiVal = currentMs / 1000;
41
+ } else if (!node.pollingUnit && currentMs > 0) {
42
+ // Heuristic fallback (Legacy): Guess unit if none was saved
38
43
  if (currentMs >= 60000 && currentMs % 60000 === 0) { uiVal = currentMs / 60000; uiUnit = 'min'; }
39
44
  else if (currentMs >= 1000 && currentMs % 1000 === 0) { uiVal = currentMs / 1000; uiUnit = 's'; }
40
45
  }
@@ -52,6 +57,66 @@
52
57
  $triggerMode.on('change', updateTriggerVisibility);
53
58
  updateTriggerVisibility();
54
59
 
60
+ // --- Provider Lookup Logic ---
61
+ $('#btn-lookup-provider').on('click', function () {
62
+ const configNodeId = $('#node-input-connection').val();
63
+ const $btn = $(this);
64
+
65
+ if (!configNodeId || configNodeId === "_ADD_") {
66
+ RED.notify("Please select a valid Configuration Node first.", "error");
67
+ return;
68
+ }
69
+
70
+ $btn.addClass('disabled').html('<i class="fa fa-spinner fa-spin"></i>');
71
+
72
+ $.getJSON('uos/providers/' + configNodeId, function (data) {
73
+ $btn.removeClass('disabled').html('<i class="fa fa-search"></i>');
74
+
75
+ if (!data || data.length === 0) {
76
+ RED.notify("No providers found.", "warning");
77
+ return;
78
+ }
79
+
80
+ // Build List
81
+ const providerList = data.map(p => {
82
+ const pid = p.id || p.Id || p;
83
+ return `<li class="red-ui-list-item provider-item" data-id="${pid}" style="cursor: pointer; padding: 8px; border-bottom: 1px solid #eee;">
84
+ <i class="fa fa-cube"></i> <b>${pid}</b>
85
+ </li>`;
86
+ }).join('');
87
+
88
+ const content = `<div style="padding:10px; font-size:12px; color:#666;">Select a provider to read from:</div>
89
+ <ul style="list-style: none; padding: 0; margin:0; max-height: 300px; overflow-y: auto; border:1px solid #ddd;">${providerList}</ul>`;
90
+
91
+ const $dialog = $('<div id="provider-lookup-dialog"></div>')
92
+ .html(content)
93
+ .dialog({
94
+ title: "Select Provider",
95
+ width: 400,
96
+ modal: true,
97
+ buttons: {
98
+ "Cancel": function () { $(this).dialog("close"); }
99
+ }
100
+ });
101
+
102
+ // Click Handler
103
+ $dialog.find('.provider-item').hover(
104
+ function () { $(this).css("background-color", "#eef"); },
105
+ function () { $(this).css("background-color", "white"); }
106
+ ).click(function () {
107
+ const selectedId = $(this).data('id');
108
+ $("#node-input-providerId").val(selectedId);
109
+ $dialog.dialog("close");
110
+ $dialog.remove(); // Cleanup
111
+ });
112
+
113
+ }).fail(function (jqxhr) {
114
+ $btn.removeClass('disabled').html('<i class="fa fa-search"></i>');
115
+ RED.notify("Could not fetch providers. Ensure Config Node is DEPLOYED.", "error");
116
+ });
117
+ });
118
+
119
+
55
120
  // --- Variable Selector Logic ---
56
121
  const $fetchBtn = $('#btn-fetch-vars');
57
122
  const $listContainer = $('#variable-list-container');
@@ -61,17 +126,14 @@
61
126
  const $clearAllBtn = $('#btn-clear-all');
62
127
  const $hiddenManual = $('#node-input-manualVariables');
63
128
 
64
- // Styles
65
- // Adjusted: ID column removed per user request (Key is main identifier)
66
129
  const rowStyle = "display:grid; grid-template-columns: 30px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px;";
67
- // const idStyle = ... (removed)
68
130
  const keyStyle = "overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px;";
69
131
 
70
- let currentVariables = []; // Stores {id, key, accessType, etc.}
132
+ let currentVariables = [];
71
133
 
72
134
  // Parse existing selection
73
135
  const getSelectedMap = () => {
74
- const selected = new Map(); // Key -> ID
136
+ const selected = new Map();
75
137
  const raw = $hiddenManual.val();
76
138
  if (raw) {
77
139
  raw.split(',').forEach(entry => {
@@ -94,7 +156,6 @@
94
156
  const selectedMap = getSelectedMap();
95
157
 
96
158
  vars.forEach(v => {
97
- // Robust ID handling
98
159
  let rawId = (v.id !== undefined && v.id !== null) ? v.id : v.Id;
99
160
  const safeId = (rawId !== undefined && rawId !== null) ? rawId : 'ERR';
100
161
  const isSelected = selectedMap.has(v.key);
@@ -105,15 +166,12 @@
105
166
  const cb = $('<input type="checkbox" class="var-checkbox">')
106
167
  .prop('checked', isSelected)
107
168
  .data('key', v.key)
108
- .data('id', safeId); // ID strictly stored in data attribute
169
+ .data('id', safeId);
109
170
  cbContainer.append(cb);
110
171
 
111
- // ID Column Removed from View
112
- // const idBadge = ...
113
-
114
172
  const label = $('<div>', { style: keyStyle, title: v.key }).text(v.key);
115
173
 
116
- row.append(cbContainer).append(label); // No ID badge
174
+ row.append(cbContainer).append(label);
117
175
  $listContainer.append(row);
118
176
  });
119
177
  };
@@ -122,7 +180,7 @@
122
180
  const rows = $listContainer.find('.var-row');
123
181
  term = term.toLowerCase();
124
182
  rows.each(function () {
125
- const text = $(this).find('div:nth-child(3)').text().toLowerCase(); // 3rd child is name
183
+ const text = $(this).find('div:nth-child(2)').text().toLowerCase(); // 2nd child is name (grid)
126
184
  $(this).toggle(text.indexOf(term) > -1);
127
185
  });
128
186
  };
@@ -156,21 +214,27 @@
156
214
  $statusMsg.text('Fetching...').css('color', 'blue');
157
215
  $listContainer.html('<div style="padding:20px; text-align:center;"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
158
216
 
159
- $.getJSON('uos/providers/' + configNodeId + '/' + providerId + '/variables', function (data) {
160
- $fetchBtn.prop('disabled', false);
161
- $statusMsg.text('');
162
- // Sort by ID (handle 'id' or 'Id')
163
- currentVariables = data.sort((a, b) => {
164
- const idA = (a.id !== undefined) ? a.id : a.Id;
165
- const idB = (b.id !== undefined) ? b.id : b.Id;
166
- return parseInt(idA) - parseInt(idB);
167
- });
168
- renderList(currentVariables);
169
- $statusMsg.text(`Loaded ${data.length} variables.`).css('color', 'green');
170
- }).fail(function (jqxhr) {
171
- $fetchBtn.prop('disabled', false);
172
- $statusMsg.text('Error: ' + (jqxhr.responseJSON?.error || 'Unknown')).css('color', 'red');
173
- $listContainer.empty();
217
+ // Headers for stateless support would go here if we had access to credentials in editor?
218
+ // We generally don't. But fetch providers works if deployed.
219
+
220
+ $.ajax({
221
+ url: 'uos/providers/' + configNodeId + '/' + providerId + '/variables',
222
+ success: function (data) {
223
+ $fetchBtn.prop('disabled', false);
224
+ $statusMsg.text('');
225
+ currentVariables = data.sort((a, b) => {
226
+ const idA = (a.id !== undefined) ? parseInt(a.id) : 0;
227
+ const idB = (b.id !== undefined) ? parseInt(b.id) : 0;
228
+ return idA - idB;
229
+ });
230
+ renderList(currentVariables);
231
+ $statusMsg.text(`Loaded ${data.length} variables.`).css('color', 'green');
232
+ },
233
+ error: function (jqxhr) {
234
+ $fetchBtn.prop('disabled', false);
235
+ $statusMsg.text('Error: ' + (jqxhr.responseJSON?.error || 'Unknown')).css('color', 'red');
236
+ $listContainer.html('<div style="padding:15px; color:#c00; text-align:center;">Failed to load variables.<br>Ensure Config is deployed.</div>');
237
+ }
174
238
  });
175
239
  });
176
240
 
@@ -179,11 +243,10 @@
179
243
  if (initialMap.size > 0 && currentVariables.length === 0) {
180
244
  const dummyVars = [];
181
245
  initialMap.forEach((id, key) => dummyVars.push({ id, key }));
182
- // Just in case stored ID is undefined, show it so user can see it's broken
183
246
  renderList(dummyVars);
184
- $statusMsg.text('Cached variables shown. Load again to refresh.').css('color', '#888');
247
+ $statusMsg.text('Cached selection shown. Load again to refresh.').css('color', '#888');
185
248
  } else {
186
- $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>');
249
+ $listContainer.html('<div style="padding:20px; text-align:center; color:#999;">Click <b>Load Variables</b> to browse.</div>');
187
250
  }
188
251
  },
189
252
  oneditsave: function () {
@@ -193,7 +256,6 @@
193
256
  if ($(this).prop('checked')) {
194
257
  const k = $(this).data('key');
195
258
  const i = $(this).data('id');
196
- // Important: Don't save if ID is 'ERR' or undefined
197
259
  if (k && i !== undefined && i !== 'ERR') {
198
260
  items.push(`${k}:${i}`);
199
261
  }
@@ -201,20 +263,15 @@
201
263
  });
202
264
  $('#node-input-manualVariables').val(items.join(','));
203
265
 
204
- // 2. Save Polling Interval (calculate ms)
266
+ // 2. Save Polling Interval
205
267
  const val = parseInt($('#node-input-pollingInterval').val(), 10) || 0;
206
268
  const unit = $('#node-input-pollingUnit').val();
207
269
  let multiplier = 1;
208
270
  if (unit === 's') multiplier = 1000;
209
271
  if (unit === 'min') multiplier = 60000;
210
-
211
- // We overwrite the raw value with calculated MS.
212
- // WAIT! The 'defaults' define pollingInterval. If we overwrite it here, the UI input needs to read it back correctly in oneditprepare.
213
- // Actually, standard pattern is to store the MS value in 'pollingInterval' and recover the Unit in UI.
214
- // But my oneditprepare logic for unit recovery was heuristic. Let's make it robust by trusting the calculation.
215
272
  const totalMs = val * multiplier;
216
273
  $('#node-input-pollingInterval').val(totalMs);
217
- $('#node-input-pollingUnit').val(unit); // Also save the unit
274
+ $('#node-input-pollingUnit').val(unit);
218
275
  }
219
276
  });
220
277
  </script>
@@ -232,38 +289,38 @@
232
289
 
233
290
  <div class="form-row">
234
291
  <label for="node-input-providerId"><i class="fa fa-server"></i> Provider ID</label>
235
- <div style="display:flex; gap:5px;">
292
+ <div style="display:flex;">
236
293
  <input type="text" id="node-input-providerId" placeholder="e.g. u_os_adm" style="flex:1;">
237
- <button id="btn-fetch-vars" class="red-ui-button"><i class="fa fa-refresh"></i> Load Variables</button>
294
+ <a id="btn-lookup-provider" class="red-ui-button" style="margin-left: 5px;" title="Search Providers"><i class="fa fa-search"></i></a>
238
295
  </div>
239
- <div id="fetch-status" style="margin-top:5px; font-size:0.9em; min-height:1.2em;"></div>
240
296
  </div>
241
297
 
242
- <div class="form-row" style="border:1px solid #ccc; padding:0; border-radius:4px; background:#fff;">
243
- <div style="background:#f7f7f7; padding:8px 10px; border-bottom:1px solid #ddd; display:flex; justify-content:space-between; align-items:center;">
244
- <span style="font-weight:bold; font-size:12px;"><i class="fa fa-list-ul"></i> Selection</span>
245
- <input type="text" id="node-input-var-search" placeholder="Filter..." style="width:120px; font-size:11px; padding:2px;">
298
+ <div class="form-row">
299
+ <div style="display:flex; justify-content:space-between; align-items:center;">
300
+ <label><i class="fa fa-list"></i> Variables</label>
301
+ <button id="btn-fetch-vars" class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i> Load Variables</button>
246
302
  </div>
303
+ <div id="fetch-status" style="font-size:0.9em; min-height:1.2em; text-align:right; margin-bottom:5px;"></div>
247
304
 
248
- <div style="display:grid; grid-template-columns: 30px 1fr; background:#eee; font-size:11px; font-weight:bold; padding:4px 0; border-bottom:1px solid #ccc;">
249
- <div style="text-align:center;"><i class="fa fa-check-square-o"></i></div>
250
- <div style="padding-left:10px;">Name</div>
251
- </div>
252
-
253
- <div id="variable-list-container" style="height:250px; overflow-y:auto; background:white;">
254
- <!-- Variables -->
255
- </div>
256
-
257
- <div style="background:#f7f7f7; padding:8px 10px; border-top:1px solid #ddd; display:flex; gap:10px;">
258
- <button id="btn-select-all" class="red-ui-button red-ui-button-small">Select All</button>
259
- <button id="btn-clear-all" class="red-ui-button red-ui-button-small">Clear All</button>
260
- <div style="flex:1; text-align:right; color:#888; font-size:11px; padding-top:4px;">Empty = Read ALL</div>
305
+ <div style="border:1px solid #ccc; padding:0; border-radius:4px; background:#fff;">
306
+ <div style="background:#f7f7f7; padding:4px 10px; border-bottom:1px solid #ddd; display:flex; justify-content:space-between; align-items:center;">
307
+ <span style="font-size:11px; font-weight:bold;">Select Variables</span>
308
+ <input type="text" id="node-input-var-search" placeholder="Filter..." style="width:100px; font-size:11px; padding:2px;">
309
+ </div>
310
+
311
+ <div id="variable-list-container" style="height:250px; overflow-y:auto; background:white;">
312
+ <!-- Variables -->
313
+ </div>
314
+
315
+ <div style="background:#f7f7f7; padding:5px 10px; border-top:1px solid #ddd; display:flex; gap:10px;">
316
+ <button id="btn-select-all" class="red-ui-button red-ui-button-small">All</button>
317
+ <button id="btn-clear-all" class="red-ui-button red-ui-button-small">None</button>
318
+ <div style="flex:1; text-align:right; color:#888; font-size:10px; padding-top:4px;">Empty selection reads ALL variables</div>
319
+ </div>
261
320
  </div>
262
321
  <input type="hidden" id="node-input-manualVariables">
263
322
  </div>
264
323
 
265
- <hr style="margin:15px 0;">
266
-
267
324
  <div class="form-row">
268
325
  <label for="node-input-triggerMode"><i class="fa fa-bolt"></i> Trigger</label>
269
326
  <select id="node-input-triggerMode" style="width:70%;">
@@ -284,112 +341,11 @@
284
341
  </script>
285
342
 
286
343
  <script type="text/html" data-help-name="datahub-input">
287
- <p><b>DataHub - Read</b> reads variables from u-OS Data Hub providers and outputs their values as JSON messages.</p>
288
-
289
- <h3>Quick Start</h3>
344
+ <p><b>DataHub - Read</b> reads variables from u-OS Data Hub providers.</p>
345
+ <h3>Configuration</h3>
290
346
  <ol>
291
- <li>Select a <b>u-OS Config</b> node (create one if needed)</li>
292
- <li>Enter the <b>Provider ID</b> (e.g. <code>u_os_adm</code>)</li>
293
- <li>Add variables using the <b>Variables Table</b></li>
294
- <li>Deploy and send a message to the input port to trigger a read</li>
347
+ <li><b>Config:</b> Select your u-OS connection.</li>
348
+ <li><b>Provider ID:</b> ID of the data source (e.g., <code>u_os_adm</code>). Use the search button to find available providers.</li>
349
+ <li><b>Variables:</b> Click "Load Variables" to select specific data points. Leave empty to read everything.</li>
295
350
  </ol>
296
-
297
- <h3>Configuration</h3>
298
-
299
- <h4>1. Config Node</h4>
300
- <p>
301
- Select your u-OS connection settings. If you haven't created one yet, click the <b>pencil icon</b> next to "Config" to create a new uos-config node.
302
- </p>
303
-
304
- <h4>2. Provider ID</h4>
305
- <p>
306
- The name of the data source you want to read from.
307
- </p>
308
- <p><b>How to find it:</b></p>
309
- <ul>
310
- <li><b>Web UI:</b> u-Control → Data Hub → Providers → Note the Provider ID</li>
311
- <li><b>Common examples:</b> <code>u_os_adm</code>, <code>hub</code>, <code>custom-provider</code></li>
312
- </ul>
313
-
314
- <h4>3. Variables Table</h4>
315
- <p>
316
- Use the <b>Load Variables</b> button to browse and select variables from the provider.
317
- Selected variables are automatically added to the list.
318
- </p>
319
-
320
- <h5>Manual Entry (Fallback)</h5>
321
- <p>
322
- If the "Load Variables" feature is unavailable, you can manually add variables by editing the <code>manualVariables</code> property (Advanced).
323
- </p>
324
-
325
- <h4>4. Trigger Mode</h4>
326
- <dl>
327
- <dt>Event (on change) – Default</dt>
328
- <dd>Efficient. Outputs only when values change. Recommended for most use cases.</dd>
329
-
330
- <dt>Poll (interval)</dt>
331
- <dd>Forces periodic reads (e.g. every 1000ms). Use if you need guaranteed updates regardless of changes.</dd>
332
- </dl>
333
-
334
- <h3>Output Format</h3>
335
- <p>When triggered (via input port), the node outputs:</p>
336
- <pre><code>{
337
- "type": "snapshot",
338
- "variables": [
339
- {
340
- "providerId": "u_os_adm",
341
- "id": 0,
342
- "key": "manufacturer_name",
343
- "value": "Weidmüller",
344
- "quality": "GOOD",
345
- "timestampNs": 1234567890000000000
346
- }
347
- ]
348
- }</code></pre>
349
-
350
- <h3>Triggering Reads</h3>
351
- <p>
352
- Connect an <b>Inject</b> node to the input port. Sending any message triggers an immediate read.
353
- </p>
354
- <p>
355
- <b>Event Mode:</b> Also outputs automatically when values change.<br>
356
- <b>Poll Mode:</b> Reads at the specified interval (e.g. every 1000ms).
357
- </p>
358
-
359
- <h3>⚠️ Important: Don't Read Your Own Output Provider</h3>
360
- <p>
361
- <b>Do NOT</b> use the provider created by your <b>DataHub - OUT</b> node (e.g. <code>nodered</code>) as input.
362
- </p>
363
- <p><b>Why?</b></p>
364
- <ul>
365
- <li>The Output provider only exists <b>while Node-RED is running</b></li>
366
- <li>On restart, the provider disappears → Input node fails</li>
367
- <li>This creates a circular dependency</li>
368
- </ul>
369
- <p><b>What to use instead:</b></p>
370
- <ul>
371
- <li>Read from <b>system providers</b> (e.g. <code>u_os_adm</code>, <code>hub</code>)</li>
372
- <li>Read from <b>other apps/devices</b> (not your own Node-RED)</li>
373
- </ul>
374
-
375
- <h3>Troubleshooting</h3>
376
-
377
- <h4>No Output</h4>
378
- <ul>
379
- <li>✓ Config node deployed?</li>
380
- <li>✓ Provider ID correct?</li>
381
- <li>✓ Variable IDs correct?</li>
382
- <li>✓ Inject signal sent to input port?</li>
383
- </ul>
384
-
385
- <h4>"Variable not found"</h4>
386
- <ul>
387
- <li>Double-check the IDs in your table</li>
388
- <li>Verify IDs match those in the Python config or Web UI</li>
389
- </ul>
390
-
391
- <div style="margin-top:15px; padding-top:10px; border-top:1px solid #ddd; font-size:0.85em; color:#666;">
392
- <p><b>Developed by <a href="https://www.linkedin.com/in/iotueli/" target="_blank">IoTUeli</a></b></p>
393
- <p>Not an official Weidmüller product. Community contribution.</p>
394
- </div>
395
351
  </script>