node-red-contrib-uos-nats 0.1.70 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,29 @@ Restart Node-RED. The nodes appear in the **"u-OS DataHub NATS"** category in th
37
37
 
38
38
  ---
39
39
 
40
+ ## What's New in v0.2.0
41
+
42
+ ### 🎯 Variable Key Support
43
+ Write Node now supports **Variable Keys** (e.g., `machine.temp`) in addition to numeric IDs!
44
+ - Automatic Key→ID resolution via provider definition query
45
+ - Cached for performance (5 min TTL)
46
+ - More user-friendly than remembering IDs
47
+
48
+ ### 🎨 Custom Icons
49
+ Each node now has a unique, meaningful icon:
50
+ - **Read:** Database with down arrow (data out)
51
+ - **Provider:** Broadcast antenna (publishing)
52
+ - **Write:** Database with up arrow (commands in)
53
+
54
+ ### 📦 Example Flows
55
+ Ready-to-import example flows included in `examples/` directory:
56
+ - `basic-read-write.json` - Getting started with Read & Write
57
+ - `advanced-provider.json` - Creating your own provider
58
+
59
+ **Import:** Node-RED menu (☰) → Import → select file from `examples/`
60
+
61
+ ---
62
+
40
63
  ## Why NATS Instead of REST API?
41
64
 
42
65
  The u-OS Data Hub offers both **NATS** (this package) and **REST API** access. Here's why NATS is the better choice for Node-RED:
@@ -0,0 +1,63 @@
1
+ # Example Flows for node-red-contrib-uos-nats
2
+
3
+ This directory contains ready-to-import example flows demonstrating the use of u-OS Data Hub nodes.
4
+
5
+ ## How to Import
6
+
7
+ 1. Open Node-RED
8
+ 2. Click the menu (☰) → **Import**
9
+ 3. Click **"select a file to import"**
10
+ 4. Choose one of the `.json` files from this directory
11
+ 5. Click **Import**
12
+
13
+ ## Available Examples
14
+
15
+ ### 1. `basic-read-write.json`
16
+
17
+ **basic-read-write.json** - Basic example showing:
18
+ - Reading variables from an existing provider (`u_os_adm`)
19
+ - Writing values to a provider variable
20
+ - Debug nodes to see the data flow
21
+
22
+ **What you'll learn:**
23
+ - How to configure the u-OS Config node
24
+ - How to use the DataHub - Read node
25
+ - How to use the DataHub - Write node
26
+
27
+ ---
28
+
29
+ ### 2. `advanced-provider.json`
30
+
31
+ **Advanced Provider Example** - Shows how to:
32
+ - Create your own Data Hub provider
33
+ - Auto-generate random data every 5 seconds
34
+ - Publish variables with nested structure
35
+
36
+ **What you'll learn:**
37
+ - How to use the DataHub - Provider node
38
+ - How to structure data for publishing
39
+ - How other apps can subscribe to your data in real-time
40
+
41
+ **Check your provider:**
42
+ After deploying, go to:
43
+ ```
44
+ u-OS Web UI → Data Hub → Providers → "nodered"
45
+ ```
46
+ You'll see all your published variables!
47
+
48
+ ---
49
+
50
+ ## Configuration
51
+
52
+ Both examples use a placeholder config node. **You must update:**
53
+
54
+ 1. **Host:** IP address of your u-OS device (e.g., `192.168.10.100`)
55
+ 2. **Client ID & Secret:** Get from u-OS Control Center → Identity & access → Clients
56
+ 3. **Provider IDs:** Change to match your system's providers
57
+ 4. **Variable IDs/Keys:** Update to match your variables
58
+
59
+ ---
60
+
61
+ ## Need Help?
62
+
63
+ Check the main [README.md](../README.md) for detailed documentation on each node.
@@ -0,0 +1,87 @@
1
+ [
2
+ {
3
+ "id": "example-flow-2",
4
+ "type": "tab",
5
+ "label": "u-OS DataHub Example: Provider",
6
+ "disabled": false,
7
+ "info": "Advanced example showing how to create your own Data Hub provider.\n\n**What this does:**\n- Creates a provider named 'nodered' (or your clientName)\n- Publishes random temperature data every 5 seconds\n- Other apps can subscribe to this data in real-time!\n\n**Setup:**\n1. Configure the uos-config node\n2. Deploy\n3. Check u-OS Web UI → Data Hub → Providers → 'nodered' to see your variables!"
8
+ },
9
+ {
10
+ "id": "inject-provider",
11
+ "type": "inject",
12
+ "z": "example-flow-2",
13
+ "name": "Every 5 seconds",
14
+ "repeat": "5",
15
+ "crontab": "",
16
+ "once": true,
17
+ "onceDelay": 0.1,
18
+ "topic": "",
19
+ "payload": "",
20
+ "payloadType": "date",
21
+ "x": 160,
22
+ "y": 100,
23
+ "wires": [
24
+ [
25
+ "function-random"
26
+ ]
27
+ ]
28
+ },
29
+ {
30
+ "id": "function-random",
31
+ "type": "function",
32
+ "z": "example-flow-2",
33
+ "name": "Generate Random Data",
34
+ "func": "function randomBetween(min, max) {\n return Math.random() * (max - min) + min;\n}\n\nmsg.payload = {\n machine: {\n status: \"running\",\n speed: Math.floor(randomBetween(1000, 2000)),\n details: {\n temp: randomBetween(30, 80),\n pressure: randomBetween(1.0, 5.0)\n }\n },\n timestamp: new Date().toISOString()\n};\n\nreturn msg;",
35
+ "outputs": 1,
36
+ "timeout": 0,
37
+ "noerr": 0,
38
+ "initialize": "",
39
+ "finalize": "",
40
+ "libs": [],
41
+ "x": 400,
42
+ "y": 100,
43
+ "wires": [
44
+ [
45
+ "provider-node-example"
46
+ ]
47
+ ]
48
+ },
49
+ {
50
+ "id": "provider-node-example",
51
+ "type": "datahub-output",
52
+ "z": "example-flow-2",
53
+ "name": "Publish to DataHub",
54
+ "connection": "config-example-2",
55
+ "providerId": "",
56
+ "x": 660,
57
+ "y": 100,
58
+ "wires": [
59
+ [
60
+ "debug-provider"
61
+ ]
62
+ ]
63
+ },
64
+ {
65
+ "id": "debug-provider",
66
+ "type": "debug",
67
+ "z": "example-flow-2",
68
+ "name": "Published Data",
69
+ "active": true,
70
+ "tosidebar": true,
71
+ "console": false,
72
+ "tostatus": false,
73
+ "complete": "payload",
74
+ "targetType": "msg",
75
+ "x": 890,
76
+ "y": 100,
77
+ "wires": []
78
+ },
79
+ {
80
+ "id": "config-example-2",
81
+ "type": "uos-config",
82
+ "host": "192.168.10.100",
83
+ "port": 49360,
84
+ "clientName": "nodered",
85
+ "scope": "hub.variables.provide hub.variables.readonly hub.variables.readwrite"
86
+ }
87
+ ]
@@ -0,0 +1,102 @@
1
+ [
2
+ {
3
+ "id": "example-flow-1",
4
+ "type": "tab",
5
+ "label": "u-OS DataHub Example: Read & Write",
6
+ "disabled": false,
7
+ "info": "Basic example showing how to read from and write to the u-OS Data Hub.\n\n**Setup:**\n1. Configure the uos-config node with your u-OS device credentials\n2. Update Provider IDs to match your system\n3. Update Variable IDs/Keys to match your variables\n4. Deploy and test!"
8
+ },
9
+ {
10
+ "id": "read-node-example",
11
+ "type": "datahub-input",
12
+ "z": "example-flow-1",
13
+ "name": "Read Zipcode",
14
+ "connection": "config-example",
15
+ "providerId": "u_os_adm",
16
+ "manualVariables": "digital_nameplate.address_information.zipcode:2",
17
+ "triggerMode": "event",
18
+ "pollingInterval": "1000",
19
+ "x": 150,
20
+ "y": 100,
21
+ "wires": [
22
+ [
23
+ "debug-read"
24
+ ]
25
+ ]
26
+ },
27
+ {
28
+ "id": "debug-read",
29
+ "type": "debug",
30
+ "z": "example-flow-1",
31
+ "name": "Display Read Data",
32
+ "active": true,
33
+ "tosidebar": true,
34
+ "console": false,
35
+ "tostatus": false,
36
+ "complete": "payload",
37
+ "targetType": "msg",
38
+ "x": 380,
39
+ "y": 100,
40
+ "wires": []
41
+ },
42
+ {
43
+ "id": "inject-write",
44
+ "type": "inject",
45
+ "z": "example-flow-1",
46
+ "name": "Click to Write",
47
+ "repeat": "",
48
+ "crontab": "",
49
+ "once": false,
50
+ "onceDelay": 0.1,
51
+ "topic": "",
52
+ "payload": "false",
53
+ "payloadType": "bool",
54
+ "x": 150,
55
+ "y": 200,
56
+ "wires": [
57
+ [
58
+ "write-node-example"
59
+ ]
60
+ ]
61
+ },
62
+ {
63
+ "id": "write-node-example",
64
+ "type": "datahub-write",
65
+ "z": "example-flow-1",
66
+ "name": "Write Value",
67
+ "connection": "config-example",
68
+ "providerId": "u_os_adm",
69
+ "variableId": "5",
70
+ "variableKey": "",
71
+ "x": 340,
72
+ "y": 200,
73
+ "wires": [
74
+ [
75
+ "debug-write"
76
+ ]
77
+ ]
78
+ },
79
+ {
80
+ "id": "debug-write",
81
+ "type": "debug",
82
+ "z": "example-flow-1",
83
+ "name": "Write Confirmation",
84
+ "active": true,
85
+ "tosidebar": true,
86
+ "console": false,
87
+ "tostatus": false,
88
+ "complete": "payload",
89
+ "targetType": "msg",
90
+ "x": 560,
91
+ "y": 200,
92
+ "wires": []
93
+ },
94
+ {
95
+ "id": "config-example",
96
+ "type": "uos-config",
97
+ "host": "192.168.10.100",
98
+ "port": 49360,
99
+ "clientName": "nodered",
100
+ "scope": "hub.variables.provide hub.variables.readonly hub.variables.readwrite"
101
+ }
102
+ ]
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 60">
2
+ <!-- Broadcast/antenna symbol -->
3
+ <circle cx="20" cy="30" r="3" fill="white"/>
4
+
5
+ <!-- Inner waves -->
6
+ <path d="M 12,22 Q 8,30 12,38" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
7
+ <path d="M 28,22 Q 32,30 28,38" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
8
+
9
+ <!-- Outer waves -->
10
+ <path d="M 7,14 Q 2,30 7,46" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
11
+ <path d="M 33,14 Q 38,30 33,46" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
12
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 60">
2
+ <!-- Database cylinder -->
3
+ <ellipse cx="20" cy="15" rx="14" ry="5" fill="none" stroke="white" stroke-width="2.5"/>
4
+ <line x1="6" y1="15" x2="6" y2="35" stroke="white" stroke-width="2.5"/>
5
+ <line x1="34" y1="15" x2="34" y2="35" stroke="white" stroke-width="2.5"/>
6
+ <ellipse cx="20" cy="35" rx="14" ry="5" fill="none" stroke="white" stroke-width="2.5"/>
7
+
8
+ <!-- Arrow pointing DOWN (data flowing out) -->
9
+ <line x1="20" y1="40" x2="20" y2="55" stroke="white" stroke-width="2.5"/>
10
+ <polyline points="15,50 20,55 25,50" fill="none" stroke="white" stroke-width="2.5" stroke-linejoin="round"/>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 60">
2
+ <!-- Database cylinder -->
3
+ <ellipse cx="20" cy="45" rx="14" ry="5" fill="none" stroke="white" stroke-width="2.5"/>
4
+ <line x1="6" y1="45" x2="6" y2="25" stroke="white" stroke-width="2.5"/>
5
+ <line x1="34" y1="45" x2="34" y2="25" stroke="white" stroke-width="2.5"/>
6
+ <ellipse cx="20" cy="25" rx="14" ry="5" fill="none" stroke="white" stroke-width="2.5"/>
7
+
8
+ <!-- Arrow pointing UP (data flowing in/command) -->
9
+ <line x1="20" y1="20" x2="20" y2="5" stroke="white" stroke-width="2.5"/>
10
+ <polyline points="15,10 20,5 25,10" fill="none" stroke="white" stroke-width="2.5" stroke-linejoin="round"/>
11
+ </svg>
@@ -12,7 +12,7 @@
12
12
  },
13
13
  inputs: 1,
14
14
  outputs: 1,
15
- icon: "white/datahub-input.svg",
15
+ icon: "white/datahub-read.svg",
16
16
  label: function () {
17
17
  if (this.name) return this.name;
18
18
  return "DataHub - IN";
@@ -9,7 +9,7 @@
9
9
  },
10
10
  inputs: 1,
11
11
  outputs: 1,
12
- icon: "white/datahub-output.svg",
12
+ icon: "white/datahub-provider.svg",
13
13
  oneditprepare: function () { },
14
14
  label: function () {
15
15
  return this.name || `DataHub - OUT ${this.providerId || ''}`;
@@ -7,18 +7,41 @@
7
7
  connection: { type: "uos-config", required: true },
8
8
  providerId: { value: "", required: true },
9
9
  variableId: { value: "", required: false },
10
- variableName: { value: "", required: false }
10
+ variableKey: { value: "", required: false }
11
11
  },
12
12
  inputs: 1,
13
13
  outputs: 1,
14
- icon: "white/datahub-output.svg",
14
+ icon: "white/datahub-write.svg",
15
15
  label: function () {
16
16
  if (this.name) return this.name;
17
- return `Write ${this.providerId || 'provider'}`;
17
+ const target = this.variableKey || this.variableId || 'var';
18
+ return `Write → ${this.providerId}:${target}`;
18
19
  },
19
20
  paletteLabel: "DataHub - Write",
20
21
  oneditprepare: function () {
21
- // Nothing special needed for now
22
+ // Validate: at least one of ID or Key required
23
+ const validateVar = () => {
24
+ const hasId = $('#node-input-variableId').val();
25
+ const hasKey = $('#node-input-variableKey').val();
26
+ if (!hasId && !hasKey) {
27
+ $('#var-validation-error').show();
28
+ return false;
29
+ } else {
30
+ $('#var-validation-error').hide();
31
+ return true;
32
+ }
33
+ };
34
+
35
+ $('#node-input-variableId, #node-input-variableKey').on('change keyup', validateVar);
36
+ },
37
+ oneditsave: function () {
38
+ // Final validation before save
39
+ const hasId = this.variableId;
40
+ const hasKey = this.variableKey;
41
+ if (!hasId && !hasKey) {
42
+ alert('Please provide either Variable ID or Variable Key');
43
+ return false;
44
+ }
22
45
  }
23
46
  });
24
47
  </script>
@@ -41,10 +64,23 @@
41
64
 
42
65
  <div class="form-row">
43
66
  <label for="node-input-variableId"><i class="fa fa-hashtag"></i> Variable ID</label>
44
- <input type="number" id="node-input-variableId" placeholder="e.g. 5">
67
+ <input type="number" id="node-input-variableId" placeholder="e.g. 5 (optional)">
68
+ </div>
69
+
70
+ <div class="form-row">
71
+ <label for="node-input-variableKey"><i class="fa fa-key"></i> Variable Key</label>
72
+ <input type="text" id="node-input-variableKey" placeholder="e.g. machine.temp (optional)">
73
+ </div>
74
+
75
+ <div id="var-validation-error" class="form-tips" style="display:none; color:#c00;">
76
+ <i class="fa fa-warning"></i> <b>Either Variable ID or Variable Key is required</b>
45
77
  </div>
46
78
 
47
79
  <div class="form-tips">
80
+ <i class="fa fa-info-circle"></i> Provide <b>either</b> Variable ID (numeric) <b>or</b> Variable Key (text). Key will be resolved to ID automatically.
81
+ </div>
82
+
83
+ <div class="form-tips" style="margin-top:10px;">
48
84
  <i class="fa fa-info-circle"></i> Send <code>msg.payload</code> with the value to write (e.g., <code>true</code>, <code>42</code>, <code>"hello"</code>)
49
85
  </div>
50
86
  </script>
@@ -1,4 +1,54 @@
1
1
  import { encodeWriteVariablesCommand } from '../lib/payloads.js';
2
+ import { buildReadProviderDefinitionQuery, decodeProviderDefinition } from '../lib/payloads.js';
3
+
4
+ // Simple cache for provider definitions (5 min TTL)
5
+ const providerCache = new Map();
6
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
7
+
8
+ async function resolveVariableKey(nc, providerId, key, node) {
9
+ const cacheKey = `${providerId}`;
10
+ const cached = providerCache.get(cacheKey);
11
+
12
+ // Check cache
13
+ if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
14
+ const variable = cached.definition.variables.find(v => v.key === key);
15
+ if (variable) {
16
+ node.debug && node.debug(`Key '${key}' resolved to ID ${variable.id} (cached)`);
17
+ return variable.id;
18
+ }
19
+ }
20
+
21
+ // Query provider definition
22
+ try {
23
+ const query = buildReadProviderDefinitionQuery();
24
+ const subject = `v1.loc.${providerId}.def.query`;
25
+
26
+ const response = await nc.request(subject, query, { timeout: 3000 });
27
+ const definition = decodeProviderDefinition(response.data);
28
+
29
+ if (!definition) {
30
+ throw new Error(`Provider ${providerId} not found or no definition returned`);
31
+ }
32
+
33
+ // Cache the definition
34
+ providerCache.set(cacheKey, {
35
+ definition,
36
+ timestamp: Date.now()
37
+ });
38
+
39
+ // Find variable by key
40
+ const variable = definition.variables.find(v => v.key === key);
41
+ if (!variable) {
42
+ throw new Error(`Variable key '${key}' not found in provider ${providerId}`);
43
+ }
44
+
45
+ node.debug && node.debug(`Key '${key}' resolved to ID ${variable.id}`);
46
+ return variable.id;
47
+
48
+ } catch (err) {
49
+ throw new Error(`Failed to resolve key '${key}': ${err.message}`);
50
+ }
51
+ }
2
52
 
3
53
  export default function (RED) {
4
54
  function DataHubWriteNode(config) {
@@ -16,6 +66,8 @@ export default function (RED) {
16
66
  // Store configuration
17
67
  this.providerId = config.providerId?.trim();
18
68
  this.variableId = config.variableId ? parseInt(config.variableId, 10) : null;
69
+ this.variableKey = config.variableKey?.trim();
70
+ this.resolvedId = null; // Cached resolved ID
19
71
 
20
72
  if (!this.providerId) {
21
73
  node.error('Provider ID is required');
@@ -23,13 +75,25 @@ export default function (RED) {
23
75
  return;
24
76
  }
25
77
 
26
- if (this.variableId === null || isNaN(this.variableId)) {
27
- node.error('Variable ID is required and must be a number');
28
- node.status({ fill: 'red', shape: 'dot', text: 'invalid variable ID' });
78
+ // Validate: either ID or Key required
79
+ if (!this.variableId && !this.variableKey) {
80
+ node.error('Either Variable ID or Variable Key is required');
81
+ node.status({ fill: 'red', shape: 'dot', text: 'missing variable' });
29
82
  return;
30
83
  }
31
84
 
32
- node.status({ fill: 'green', shape: 'ring', text: 'ready' });
85
+ // If ID provided and valid, use it
86
+ if (this.variableId && !isNaN(this.variableId)) {
87
+ this.resolvedId = this.variableId;
88
+ node.status({ fill: 'green', shape: 'ring', text: 'ready' });
89
+ } else if (this.variableKey) {
90
+ // Key provided - will resolve on first message
91
+ node.status({ fill: 'yellow', shape: 'ring', text: 'key needs resolution' });
92
+ } else {
93
+ node.error('Invalid Variable ID');
94
+ node.status({ fill: 'red', shape: 'dot', text: 'invalid ID' });
95
+ return;
96
+ }
33
97
 
34
98
  // Handle incoming messages
35
99
  node.on('input', async function (msg) {
@@ -49,10 +113,19 @@ export default function (RED) {
49
113
  return;
50
114
  }
51
115
 
116
+ // Resolve variable ID if needed
117
+ let varId = node.resolvedId;
118
+ if (!varId && node.variableKey) {
119
+ node.status({ fill: 'yellow', shape: 'dot', text: 'resolving key...' });
120
+ varId = await resolveVariableKey(nc, node.providerId, node.variableKey, node);
121
+ node.resolvedId = varId; // Cache for future messages
122
+ node.status({ fill: 'green', shape: 'ring', text: 'ready' });
123
+ }
124
+
52
125
  // Build write command
53
126
  const writeCommand = encodeWriteVariablesCommand([
54
127
  {
55
- id: node.variableId,
128
+ id: varId,
56
129
  value: value
57
130
  }
58
131
  ]);
@@ -67,7 +140,8 @@ export default function (RED) {
67
140
  msg.payload = {
68
141
  success: true,
69
142
  providerId: node.providerId,
70
- variableId: node.variableId,
143
+ variableId: varId,
144
+ variableKey: node.variableKey || null,
71
145
  value: value
72
146
  };
73
147
  node.send(msg);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "0.1.70",
4
- "description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Supports event-based subscriptions and manual variable mapping.",
3
+ "version": "0.2.0",
4
+ "description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
5
5
  "author": {
6
6
  "name": "IoTUeli",
7
7
  "url": "https://www.linkedin.com/in/iotueli/"
@@ -44,6 +44,13 @@
44
44
  "nats": "^2.19.0",
45
45
  "node-fetch": "^2.6.7"
46
46
  },
47
+ "files": [
48
+ "nodes/",
49
+ "lib/",
50
+ "icons/",
51
+ "examples/",
52
+ "README.md"
53
+ ],
47
54
  "node-red": {
48
55
  "version": ">=2.0.0",
49
56
  "nodes": {