node-red-contrib-uos-nats 0.1.65 → 0.1.67
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 +252 -165
- package/lib/payloads.js +186 -83
- package/nodes/datahub-input.html +2 -2
- package/nodes/datahub-output.html +2 -2
- package/nodes/datahub-write.html +127 -0
- package/nodes/datahub-write.js +87 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
**Unofficial Node-RED Package for u-OS Data Hub**
|
|
4
4
|
|
|
5
|
-
Built and maintained by [IoTUeli](https://
|
|
5
|
+
Built and maintained by [IoTUeli](https://iotueli.ch). This is **not** an official Weidmüller product.
|
|
6
6
|
Repository: <https://github.com/uiff/nats-NodeRed-Node-uc20>
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## What is this?
|
|
11
11
|
|
|
12
|
-
Node-RED nodes to **read** and **
|
|
12
|
+
Node-RED nodes to **read**, **write**, and **provide** variables for the **Weidmüller u-OS Data Hub** via NATS protocol.
|
|
13
13
|
|
|
14
|
-
### The
|
|
14
|
+
### The Four Nodes
|
|
15
15
|
|
|
16
16
|
1. **u-OS Config** – Connection settings (Host, OAuth credentials, NATS connection)
|
|
17
|
-
2. **DataHub -
|
|
18
|
-
3. **DataHub -
|
|
17
|
+
2. **DataHub - Read** – Read variables from Data Hub providers
|
|
18
|
+
3. **DataHub - Provider** – Create your own provider (publish variables)
|
|
19
|
+
4. **DataHub - Write** – Send write commands to other providers
|
|
19
20
|
|
|
20
21
|
---
|
|
21
22
|
|
|
@@ -71,14 +72,14 @@ Process & forward to other systems
|
|
|
71
72
|
- Delayed reactions
|
|
72
73
|
- Higher CPU usage
|
|
73
74
|
|
|
74
|
-
### Use NATS when
|
|
75
|
+
### Use NATS when
|
|
75
76
|
|
|
76
77
|
✅ You need **real-time reactions** to value changes
|
|
77
78
|
✅ You want to **create providers** (publish data to Data Hub)
|
|
78
79
|
✅ You need **event subscriptions** (get notified on changes)
|
|
79
80
|
✅ You're building **scalable industrial workflows**
|
|
80
81
|
|
|
81
|
-
### Use REST API when
|
|
82
|
+
### Use REST API when
|
|
82
83
|
|
|
83
84
|
⚠️ You only need **occasional manual reads**
|
|
84
85
|
⚠️ You're debugging or doing one-time queries
|
|
@@ -86,164 +87,174 @@ Process & forward to other systems
|
|
|
86
87
|
|
|
87
88
|
---
|
|
88
89
|
|
|
89
|
-
##
|
|
90
|
+
## Quick Start Guide
|
|
90
91
|
|
|
91
|
-
###
|
|
92
|
-
|
|
92
|
+
### Step 1: Create OAuth Client in u-OS
|
|
93
|
+
|
|
94
|
+
Before configuring Node-RED, create an OAuth client on your u-OS device:
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
1. Open the **u-OS Web Interface** (e.g., `http://192.168.10.100`)
|
|
97
|
+
2. Go to **System** → **Access Control** → **OAuth Clients**
|
|
98
|
+
3. Click **"Add Client"**
|
|
99
|
+
4. Enter:
|
|
100
|
+
- **Name:** `nodered`
|
|
101
|
+
- **Scopes:** Select **all** `hub.variables.*` scopes:
|
|
102
|
+
- `hub.variables.provide` (for creating providers)
|
|
103
|
+
- `hub.variables.readonly` (for reading)
|
|
104
|
+
- `hub.variables.readwrite` (for writing)
|
|
105
|
+
5. **Save** and copy the **Client ID** and **Client Secret**
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
- Open any DataHub node (IN or OUT)
|
|
98
|
-
- Click the pencil icon next to "Config"
|
|
99
|
-
- Click "Add new uos-config..."
|
|
107
|
+
### Step 2: Configure u-OS Config Node in Node-RED
|
|
100
108
|
|
|
101
|
-
|
|
109
|
+
1. Drag any **DataHub - IN** or **DataHub - OUT** node onto the canvas
|
|
110
|
+
2. Double-click it to open settings
|
|
111
|
+
3. Click the **pencil icon** next to "Config"
|
|
112
|
+
4. Select **"Add new uos-config..."**
|
|
113
|
+
5. Fill in:
|
|
102
114
|
|
|
103
115
|
| Field | Example | Description |
|
|
104
116
|
|-------|---------|-------------|
|
|
105
|
-
| **Host** | `192.168.10.100` | IP
|
|
106
|
-
| **Port** | `49360` | NATS port (default
|
|
107
|
-
| **Client Name** | `nodered` | Unique name for this
|
|
108
|
-
| **Client ID** | `my-oauth-client` |
|
|
109
|
-
| **Client Secret** | `****************` |
|
|
110
|
-
| **Scopes** | `hub.variables.*` | Leave default or customize (see below) |
|
|
111
|
-
|
|
112
|
-
3. **Create OAuth Client in Control Center:**
|
|
113
|
-
- Open the u-Control Web Interface
|
|
114
|
-
- Go to **System** → **Access Control** → **OAuth Clients**
|
|
115
|
-
- Click **"Add Client"**
|
|
116
|
-
- **Name:** `nodered`
|
|
117
|
-
- **Scopes:** Select all `hub.variables.*` (provide, readonly, readwrite)
|
|
118
|
-
- **Copy the Client ID and Secret** into Node-RED
|
|
117
|
+
| **Host** | `192.168.10.100` | IP of your u-OS device |
|
|
118
|
+
| **Port** | `49360` | NATS port (default) |
|
|
119
|
+
| **Client Name** | `nodered` | Unique name for this instance |
|
|
120
|
+
| **Client ID** | `my-oauth-client` | From Step 1 |
|
|
121
|
+
| **Client Secret** | `****************` | From Step 1 |
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- ✅ Success: Shows "Connected" + granted scopes
|
|
123
|
-
- ❌ Error: Check Host/Port/Credentials
|
|
123
|
+
6. Click **"Test Connection"** to verify
|
|
124
|
+
7. On success, click **"Add"** then **"Done"**
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
### Step 3: Deploy Your First Flow
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
Import this example flow to test both reading and writing:
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
```json
|
|
131
|
+
[{"id":"cdad2fa96dc6eeec","type":"datahub-input","z":"c221537c994b056a","name":"","connection":"a0ba0e15c8dad779","providerId":"u_os_adm","manualVariables":"digital_nameplate.address_information.zipcode:2","triggerMode":"poll","pollingInterval":"100","x":110,"y":40,"wires":[["315d179d66bf9b93"]]},{"id":"315d179d66bf9b93","type":"debug","z":"c221537c994b056a","name":"debug 7","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":740,"y":40,"wires":[]},{"id":"09f29f6bfc4e1be2","type":"inject","z":"c221537c994b056a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":140,"wires":[["43b2fcf73c370f7c"]]},{"id":"43b2fcf73c370f7c","type":"function","z":"c221537c994b056a","name":"Random Data","func":"function randomBetween(min, max) {\n return Math.random() * (max - min) + min;\n}\n\nmsg.payload = {\n machine: {\n status: \"running\",\n details: {\n temp: randomBetween(30, 80)\n }\n }\n};\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":140,"wires":[["a90304487fe19b3a"]]},{"id":"a90304487fe19b3a","type":"datahub-output","z":"c221537c994b056a","name":"","connection":"a0ba0e15c8dad779","providerId":"","x":500,"y":140,"wires":[["e53fa58e4c1987ba"]]},{"id":"e53fa58e4c1987ba","type":"debug","z":"c221537c994b056a","name":"debug 6","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":740,"y":140,"wires":[]},{"id":"a0ba0e15c8dad779","type":"uos-config","host":"127.0.0.1","port":49360,"clientName":"hub","scope":"hub.variables.provide hub.variables.readwrite hub.variables.readonly"}]
|
|
132
|
+
```
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
**What this flow does:**
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
-
|
|
136
|
+
**Top Row** (Reading):
|
|
137
|
+
- **DataHub - IN** reads `zipcode` variable from provider `u_os_adm`
|
|
138
|
+
- Polls every 100ms
|
|
139
|
+
- Outputs to Debug node
|
|
136
140
|
|
|
137
|
-
|
|
138
|
-
- **
|
|
139
|
-
- **
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
**Bottom Row** (Writing):
|
|
142
|
+
- **Inject** node triggers data generation
|
|
143
|
+
- **Function** node creates random temperature data
|
|
144
|
+
- **DataHub - OUT** publishes to Data Hub as provider `hub`
|
|
145
|
+
- Creates variables: `machine.status` and `machine.details.temp`
|
|
142
146
|
|
|
143
|
-
**
|
|
147
|
+
**To customize:**
|
|
148
|
+
1. Edit the **DataHub - IN** node:
|
|
149
|
+
- Change `Provider ID` to match your system
|
|
150
|
+
- Update variable mappings in the table
|
|
151
|
+
2. Edit the **Function** node to generate your data structure
|
|
152
|
+
3. Click **Deploy**
|
|
144
153
|
|
|
145
|
-
|
|
146
|
-
Since auto-discovery often fails due to permissions, you **manually map** variable names to their IDs.
|
|
154
|
+
---
|
|
147
155
|
|
|
148
|
-
|
|
156
|
+
## Finding Variable IDs
|
|
149
157
|
|
|
150
|
-
**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
→ Use these IDs!
|
|
158
|
+
The **DataHub - IN** node requires variable IDs (numbers). Here's how to find them:
|
|
159
|
+
|
|
160
|
+
### Option 1: u-OS Web Interface
|
|
161
|
+
|
|
162
|
+
1. Open **Data Hub** → **Providers**
|
|
163
|
+
2. Click on your target provider (e.g., `u_os_adm`)
|
|
164
|
+
3. Click **Variables** tab
|
|
165
|
+
4. Note the **ID** column (e.g., `0`, `1`, `2`)
|
|
159
166
|
|
|
160
|
-
|
|
161
|
-
1. Open **Data Hub** → **Providers** → Select your provider
|
|
162
|
-
2. Click on **Variables**
|
|
163
|
-
3. Note the **ID** column (usually 0, 1, 2, ...)
|
|
167
|
+
### Option 2: REST API Query
|
|
164
168
|
|
|
165
|
-
**Option C: From REST API**
|
|
166
169
|
```bash
|
|
167
170
|
curl -H "Authorization: Bearer $TOKEN" \
|
|
168
|
-
http://192.168.10.100/datahub/v1/providers/
|
|
171
|
+
http://192.168.10.100/datahub/v1/providers/u_os_adm/variables
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Response shows variable definitions with IDs:
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"variables": [
|
|
178
|
+
{"id": 0, "key": "manufacturer_name", ...},
|
|
179
|
+
{"id": 2, "key": "digital_nameplate.address_information.zipcode", ...}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
169
182
|
```
|
|
170
183
|
|
|
171
|
-
|
|
184
|
+
### Option 3: Check Other Apps
|
|
185
|
+
|
|
186
|
+
If you have other Data Hub clients (apps, PLCs), check their variable definitions to find the IDs.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## DataHub - Read Node
|
|
191
|
+
|
|
192
|
+
### Purpose
|
|
193
|
+
Subscribe to variables from a Data Hub provider and output their values as JSON messages.
|
|
194
|
+
|
|
195
|
+
### Configuration
|
|
196
|
+
|
|
197
|
+
1. **Config Node:** Select your u-OS connection
|
|
198
|
+
2. **Provider ID:** Enter the provider name (e.g., `u_os_adm`, `hub`, `u_os_sbm`)
|
|
199
|
+
3. **Variables Table:** Manually map variable names to IDs
|
|
172
200
|
|
|
173
201
|
| Variable Name | ID |
|
|
174
202
|
|--------------|-----|
|
|
175
203
|
| `manufacturer_name` | `0` |
|
|
176
|
-
| `
|
|
204
|
+
| `zipcode` | `2` |
|
|
177
205
|
|
|
178
206
|
Click **"Add Variable"** for each entry.
|
|
179
207
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
| No output | Check: (1) Config deployed? (2) Provider ID correct? (3) Variable IDs correct? (4) Inject signal sent? |
|
|
206
|
-
| "Variable not found" | Double-check IDs in the table. Use Python config or Web UI to verify. |
|
|
207
|
-
| Permission errors | This is expected! That's why we use the manual table. |
|
|
208
|
+
4. **Trigger Mode:**
|
|
209
|
+
- **Event (on change):** Efficient. Outputs only when values change.
|
|
210
|
+
- **Poll (interval):** Forces periodic reads (e.g., every 100ms).
|
|
211
|
+
|
|
212
|
+
### Output Format
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"type": "snapshot",
|
|
217
|
+
"variables": [
|
|
218
|
+
{
|
|
219
|
+
"providerId": "u_os_adm",
|
|
220
|
+
"id": 2,
|
|
221
|
+
"key": "zipcode",
|
|
222
|
+
"value": "12345",
|
|
223
|
+
"quality": "GOOD",
|
|
224
|
+
"timestampNs": 1234567890000000000
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Triggering Reads
|
|
231
|
+
|
|
232
|
+
Connect an **Inject** node to the input port to trigger manual reads.
|
|
208
233
|
|
|
209
234
|
---
|
|
210
235
|
|
|
211
|
-
##
|
|
236
|
+
## DataHub - Provider Node
|
|
212
237
|
|
|
213
238
|
### Purpose
|
|
214
|
-
|
|
239
|
+
Creates a real Data Hub provider that publishes variables. Other applications can subscribe to your data in real-time.
|
|
215
240
|
|
|
216
241
|
### How It Works
|
|
217
242
|
|
|
218
243
|
The OUT node:
|
|
219
|
-
1. **Registers as a Provider** on the Data Hub (
|
|
244
|
+
1. **Registers as a Provider** on the Data Hub (uses `Client Name` from Config)
|
|
220
245
|
2. **Publishes variable definitions** automatically when new variables are sent
|
|
221
246
|
3. **Sends value updates** via NATS when you send JSON messages
|
|
222
|
-
4. **Answers read requests** from other consumers
|
|
223
|
-
5. **Supports event-driven subscriptions** - other apps get updates **instantly**
|
|
224
|
-
|
|
225
|
-
**Important:** This provider only exists **while Node-RED is running**. When you restart Node-RED, the provider re-registers automatically.
|
|
226
|
-
|
|
227
|
-
### Real-World Use Cases
|
|
228
|
-
|
|
229
|
-
✅ **IoT Data Collection:** Node-RED reads sensor data (Modbus, MQTT, etc.) and publishes it to the Data Hub
|
|
230
|
-
✅ **Edge Processing:** Process data locally in Node-RED, then share results with other apps
|
|
231
|
-
✅ **System Integration:** Bridge between different protocols (e.g., OPC UA → Data Hub)
|
|
232
|
-
✅ **Custom Dashboards:** Other apps can subscribe to Node-RED's variables for visualization
|
|
233
|
-
|
|
234
|
-
### Setup Steps
|
|
247
|
+
4. **Answers read requests** from other consumers
|
|
248
|
+
5. **Supports event-driven subscriptions** - other apps get updates **instantly**
|
|
235
249
|
|
|
236
|
-
|
|
237
|
-
- Choose your **u-OS Config** node
|
|
250
|
+
### Configuration
|
|
238
251
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- Or enter a custom provider ID (e.g., `my-machine-data`)
|
|
252
|
+
1. **Config Node:** Select your u-OS connection
|
|
253
|
+
2. **Provider ID:** Leave **empty** to use `Client Name` (recommended)
|
|
242
254
|
|
|
243
|
-
|
|
255
|
+
### Send Data
|
|
244
256
|
|
|
245
|
-
|
|
246
|
-
Send a JSON object with your data:
|
|
257
|
+
Send JSON to the input:
|
|
247
258
|
|
|
248
259
|
```json
|
|
249
260
|
{
|
|
@@ -255,91 +266,167 @@ Send a JSON object with your data:
|
|
|
255
266
|
}
|
|
256
267
|
```
|
|
257
268
|
|
|
258
|
-
This creates
|
|
259
|
-
- `temperature` → ID 0
|
|
260
|
-
- `machine.status` → ID 1
|
|
261
|
-
- `machine.speed` → ID 2
|
|
269
|
+
This creates:
|
|
270
|
+
- `temperature` → Variable ID 0
|
|
271
|
+
- `machine.status` → Variable ID 1
|
|
272
|
+
- `machine.speed` → Variable ID 2
|
|
262
273
|
|
|
263
|
-
**
|
|
264
|
-
-
|
|
265
|
-
-
|
|
266
|
-
-
|
|
274
|
+
**Other apps can now:**
|
|
275
|
+
- Subscribe to value changes (event-driven, instant updates)
|
|
276
|
+
- Query current values (on-demand reads)
|
|
277
|
+
- View in u-OS Web UI (**Data Hub** → **Providers** → `nodered`)
|
|
267
278
|
|
|
268
279
|
### Event-Driven Communication
|
|
269
280
|
|
|
270
|
-
When you send a message to the OUT node:
|
|
271
|
-
|
|
272
281
|
```
|
|
273
|
-
[
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
[Python App] [Dashboard] [Other Node-RED]
|
|
280
|
-
(subscribes) (subscribes) (subscribes)
|
|
282
|
+
[Node-RED: DataHub - OUT] → [u-OS Data Hub (NATS)]
|
|
283
|
+
↓
|
|
284
|
+
┌─────────────┼─────────────┐
|
|
285
|
+
↓ ↓ ↓
|
|
286
|
+
[Other Apps] [Dashboards] [PLCs]
|
|
287
|
+
(subscribe) (subscribe) (subscribe)
|
|
281
288
|
```
|
|
282
289
|
|
|
283
|
-
**All subscribers receive
|
|
284
|
-
|
|
285
|
-
#### Step 4: Deploy & Test
|
|
286
|
-
1. Connect a **Function** or **Inject** node
|
|
287
|
-
2. Click **Deploy**
|
|
288
|
-
3. Check the u-Control Web Interface → **Data Hub** → Your Provider
|
|
290
|
+
**All subscribers receive updates IMMEDIATELY** - no polling needed!
|
|
289
291
|
|
|
290
292
|
---
|
|
291
293
|
|
|
292
|
-
##
|
|
294
|
+
## DataHub - Write Node
|
|
295
|
+
|
|
296
|
+
### Purpose
|
|
297
|
+
Send **write commands** to **external Data Hub providers** to change variable values. This is different from the Provider node - you're controlling **other** systems, not creating your own provider.
|
|
298
|
+
|
|
299
|
+
### Use Cases
|
|
300
|
+
|
|
301
|
+
✅ **Control external devices** - Toggle flags, update setpoints
|
|
302
|
+
✅ **Machine control** - Start/stop operations via Data Hub
|
|
303
|
+
✅ **Configuration updates** - Change parameters in other apps
|
|
304
|
+
✅ **Integration scenarios** - Write to PLCs, other Node-RED instances, etc.
|
|
305
|
+
|
|
306
|
+
### Configuration
|
|
307
|
+
|
|
308
|
+
1. **Config Node:** Select your u-OS connection
|
|
309
|
+
2. **Provider ID:** Target provider to write to (e.g., `u_os_adm`, `u_os_sbm`)
|
|
310
|
+
3. **Variable ID:** Numeric ID of the variable to write
|
|
311
|
+
|
|
312
|
+
### Finding Variable IDs
|
|
313
|
+
|
|
314
|
+
Same as Read node - check u-OS Web UI:
|
|
315
|
+
|
|
316
|
+
1. Go to **Data Hub** → **Providers** → Select target provider
|
|
317
|
+
2. Click **Variables** tab
|
|
318
|
+
3. Note the **ID** column
|
|
293
319
|
|
|
320
|
+
### Input Format
|
|
321
|
+
|
|
322
|
+
Send the new value as `msg.payload`:
|
|
323
|
+
|
|
324
|
+
**Toggle Boolean:**
|
|
325
|
+
```javascript
|
|
326
|
+
msg.payload = false;
|
|
327
|
+
return msg;
|
|
294
328
|
```
|
|
295
|
-
[Inject] → [DataHub - IN] → [Debug]
|
|
296
|
-
(u_os_sbm)
|
|
297
329
|
|
|
298
|
-
|
|
299
|
-
|
|
330
|
+
**Update Number:**
|
|
331
|
+
```javascript
|
|
332
|
+
msg.payload = 42.5;
|
|
333
|
+
return msg;
|
|
300
334
|
```
|
|
301
335
|
|
|
302
|
-
**
|
|
336
|
+
**Change String:**
|
|
337
|
+
```javascript
|
|
338
|
+
msg.payload = "stopped";
|
|
339
|
+
return msg;
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Output
|
|
343
|
+
|
|
344
|
+
Outputs confirmation when write command is sent:
|
|
345
|
+
|
|
303
346
|
```json
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
347
|
+
{
|
|
348
|
+
"success": true,
|
|
349
|
+
"providerId": "u_os_adm",
|
|
350
|
+
"variableId": 5,
|
|
351
|
+
"value": false
|
|
352
|
+
}
|
|
307
353
|
```
|
|
308
354
|
|
|
355
|
+
### Example Flow
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
[Inject: false] → [DataHub - Write] → [Debug]
|
|
359
|
+
(Provider: u_os_adm,
|
|
360
|
+
Variable ID: 5)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Important Notes
|
|
364
|
+
|
|
365
|
+
⚠️ **Requires write permissions:** OAuth client must have `hub.variables.readwrite` scope
|
|
366
|
+
⚠️ **Provider must accept writes:** Some providers are read-only
|
|
367
|
+
⚠️ **Variable must exist:** The variable ID must be valid
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Troubleshooting
|
|
372
|
+
|
|
373
|
+
### No Output from IN Node
|
|
374
|
+
|
|
375
|
+
✓ Config node deployed?
|
|
376
|
+
✓ Provider ID correct? (check u-OS Web UI → Data Hub → Providers)
|
|
377
|
+
✓ Variable IDs correct? (check u-OS Web UI → Variables)
|
|
378
|
+
✓ Inject signal sent to input port?
|
|
379
|
+
|
|
380
|
+
### "Variable not found" Error
|
|
381
|
+
|
|
382
|
+
- Double-check IDs in the Variables table
|
|
383
|
+
- Verify IDs match those in u-OS Web UI
|
|
384
|
+
|
|
385
|
+
### Connection Test Fails
|
|
386
|
+
|
|
387
|
+
- Check Host/Port are correct and device is reachable
|
|
388
|
+
- Verify Client ID/Secret match exactly
|
|
389
|
+
- Ensure OAuth client exists in u-OS
|
|
390
|
+
- Verify all `hub.variables.*` scopes are granted
|
|
391
|
+
|
|
392
|
+
### Why Manual IDs?
|
|
393
|
+
|
|
394
|
+
Auto-discovery requires special permissions on the provider definition endpoint, which are often restricted for security. The manual table works **without** this permission by querying specific IDs directly.
|
|
395
|
+
|
|
309
396
|
---
|
|
310
397
|
|
|
311
398
|
## FAQ
|
|
312
399
|
|
|
313
|
-
### Q:
|
|
314
|
-
**A:**
|
|
400
|
+
### Q: Can I use the provider created by Provider node in the Read node?
|
|
401
|
+
**A:** **No, don't do this!** The Provider node's provider only exists while Node-RED runs. On restart, it disappears and the Read node fails. Read from **system providers** (`u_os_sbm`, `u_os_adm`) or other persistent apps instead.
|
|
402
|
+
|
|
403
|
+
### Q: What's the difference between Provider and Write nodes?
|
|
404
|
+
**A:**
|
|
405
|
+
- **DataHub - Provider:** Creates your **own** provider. Other apps read **from** you.
|
|
406
|
+
- **DataHub - Write:** Sends commands **to other** providers. You write **to** them.
|
|
315
407
|
|
|
316
408
|
### Q: Where do I get Client ID/Secret?
|
|
317
|
-
**A:** u-
|
|
409
|
+
**A:** u-OS Web Interface → **System** → **Access Control** → **OAuth Clients** → **Add Client**
|
|
318
410
|
|
|
319
411
|
### Q: What are the required OAuth scopes?
|
|
320
412
|
**A:**
|
|
321
|
-
- **Read
|
|
322
|
-
- **
|
|
413
|
+
- **Read Node:** `hub.variables.readonly`
|
|
414
|
+
- **Provider Node:** `hub.variables.provide` + `hub.variables.readwrite`
|
|
415
|
+
- **Write Node:** `hub.variables.readwrite`
|
|
323
416
|
- **Recommended:** Select all `hub.variables.*` scopes when creating the client
|
|
324
417
|
|
|
325
418
|
### Q: Can I use this outside the local network?
|
|
326
|
-
**A:** Yes, if your u-
|
|
419
|
+
**A:** Yes, if your u-OS device is reachable over the network and you configure the correct Host/Port.
|
|
327
420
|
|
|
328
421
|
### Q: Event vs Poll - which is better?
|
|
329
422
|
**A:** **Event** (default) is more efficient. Use **Poll** only if you need guaranteed periodic readings regardless of value changes.
|
|
330
423
|
|
|
331
424
|
---
|
|
332
425
|
|
|
333
|
-
## Changelog
|
|
334
|
-
|
|
335
|
-
See [GitHub Releases](https://github.com/uiff/nats-NodeRed-Node-uc20/releases)
|
|
336
|
-
|
|
337
|
-
---
|
|
338
|
-
|
|
339
426
|
## Support
|
|
340
427
|
|
|
341
428
|
**Issues, Questions, or Feature Requests:**
|
|
342
|
-
Contact [IoTUeli](https://
|
|
429
|
+
Contact [IoTUeli](https://iotueli.ch) or open an issue on [GitHub](https://github.com/uiff/nats-NodeRed-Node-uc20)
|
|
343
430
|
|
|
344
431
|
---
|
|
345
432
|
|
package/lib/payloads.js
CHANGED
|
@@ -17,6 +17,10 @@ import { VariableValueBoolean, VariableValueBooleanT } from './fbs/weidmueller/u
|
|
|
17
17
|
import { TimestampT } from './fbs/weidmueller/ucontrol/hub/timestamp.js';
|
|
18
18
|
import { ReadVariablesQueryResponseT } from './fbs/weidmueller/ucontrol/hub/read-variables-query-response.js';
|
|
19
19
|
import { ReadVariablesQueryRequestT } from './fbs/weidmueller/ucontrol/hub/read-variables-query-request.js';
|
|
20
|
+
import { ReadVariablesQueryRequest } from './fbs/weidmueller/ucontrol/hub/read-variables-query-request.js';
|
|
21
|
+
import { ReadProviderDefinitionQueryRequest } from './fbs/weidmueller/ucontrol/hub/read-provider-definition-query-request.js';
|
|
22
|
+
import { WriteVariablesCommandT } from './fbs/weidmueller/ucontrol/hub/write-variables-command.js';
|
|
23
|
+
|
|
20
24
|
const DEFAULT_QUALITY = 'GOOD';
|
|
21
25
|
export function buildProviderDefinitionEvent(defs) {
|
|
22
26
|
const fingerprint = computeFingerprint(defs);
|
|
@@ -44,7 +48,6 @@ export function buildReadVariablesResponse(defs, states, fingerprint) {
|
|
|
44
48
|
return builder.asUint8Array();
|
|
45
49
|
}
|
|
46
50
|
// Helper to omit field if null/empty
|
|
47
|
-
import { ReadVariablesQueryRequest } from './fbs/weidmueller/ucontrol/hub/read-variables-query-request.js';
|
|
48
51
|
|
|
49
52
|
export function buildReadVariablesQuery(ids) {
|
|
50
53
|
const builder = new flatbuffers.Builder(128);
|
|
@@ -64,7 +67,6 @@ export function buildReadVariablesQuery(ids) {
|
|
|
64
67
|
return builder.asUint8Array();
|
|
65
68
|
}
|
|
66
69
|
// Helper to build definition query
|
|
67
|
-
import { ReadProviderDefinitionQueryRequest } from './fbs/weidmueller/ucontrol/hub/read-provider-definition-query-request.js';
|
|
68
70
|
|
|
69
71
|
export function buildReadProviderDefinitionQuery() {
|
|
70
72
|
const builder = new flatbuffers.Builder(32);
|
|
@@ -73,6 +75,52 @@ export function buildReadProviderDefinitionQuery() {
|
|
|
73
75
|
builder.finish(offset);
|
|
74
76
|
return builder.asUint8Array();
|
|
75
77
|
}
|
|
78
|
+
|
|
79
|
+
// Encode write variables command
|
|
80
|
+
export function encodeWriteVariablesCommand(variables) {
|
|
81
|
+
// variables: [{id: number, value: any}, ...]
|
|
82
|
+
const varList = new VariableListT();
|
|
83
|
+
varList.items = variables.map(v => {
|
|
84
|
+
const varT = new VariableT();
|
|
85
|
+
varT.id = v.id;
|
|
86
|
+
varT.quality = VariableQuality.GOOD;
|
|
87
|
+
varT.timestamp = nowNs();
|
|
88
|
+
|
|
89
|
+
// Encode value based on type
|
|
90
|
+
const val = v.value;
|
|
91
|
+
if (typeof val === 'boolean') {
|
|
92
|
+
const boolVal = new VariableValueBooleanT();
|
|
93
|
+
boolVal.value = val;
|
|
94
|
+
varT.value = boolVal;
|
|
95
|
+
varT.valueType = VariableValue.Boolean;
|
|
96
|
+
} else if (Number.isInteger(val)) {
|
|
97
|
+
const intVal = new VariableValueInt64T();
|
|
98
|
+
intVal.value = BigInt(val);
|
|
99
|
+
varT.value = intVal;
|
|
100
|
+
varT.valueType = VariableValue.Int64;
|
|
101
|
+
} else if (typeof val === 'number') {
|
|
102
|
+
const floatVal = new VariableValueFloat64T();
|
|
103
|
+
floatVal.value = val;
|
|
104
|
+
varT.value = floatVal;
|
|
105
|
+
varT.valueType = VariableValue.Float64;
|
|
106
|
+
} else if (typeof val === 'string') {
|
|
107
|
+
const strVal = new VariableValueStringT();
|
|
108
|
+
strVal.value = val;
|
|
109
|
+
varT.value = strVal;
|
|
110
|
+
varT.valueType = VariableValue.String;
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error(`Unsupported value type: ${typeof val}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return varT;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const cmd = new WriteVariablesCommandT(varList);
|
|
119
|
+
const builder = new flatbuffers.Builder(512);
|
|
120
|
+
builder.finish(cmd.pack(builder));
|
|
121
|
+
return builder.asUint8Array();
|
|
122
|
+
}
|
|
123
|
+
|
|
76
124
|
export function decodeVariableList(list) {
|
|
77
125
|
if (!list)
|
|
78
126
|
return [];
|
|
@@ -98,121 +146,176 @@ export function decodeVariableList(list) {
|
|
|
98
146
|
case VariableValue.Boolean: {
|
|
99
147
|
const holder = new VariableValueBoolean();
|
|
100
148
|
item.value(holder);
|
|
101
|
-
decoded =
|
|
149
|
+
decoded = holder.value();
|
|
102
150
|
break;
|
|
103
151
|
}
|
|
104
|
-
case VariableValue.String:
|
|
105
|
-
default: {
|
|
152
|
+
case VariableValue.String: {
|
|
106
153
|
const holder = new VariableValueString();
|
|
107
154
|
item.value(holder);
|
|
108
|
-
decoded = holder.value()
|
|
155
|
+
decoded = holder.value();
|
|
109
156
|
break;
|
|
110
157
|
}
|
|
158
|
+
default:
|
|
159
|
+
decoded = null;
|
|
111
160
|
}
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const nanos = ts ? ts.nanos() : 0;
|
|
161
|
+
const quality = decodeQuality(item.quality());
|
|
162
|
+
const timestampNs = decodeTimestamp(item.timestamp());
|
|
115
163
|
result.push({
|
|
116
164
|
id: item.id(),
|
|
117
165
|
value: decoded,
|
|
118
|
-
|
|
119
|
-
|
|
166
|
+
quality,
|
|
167
|
+
timestampNs,
|
|
120
168
|
});
|
|
121
169
|
}
|
|
122
170
|
return result;
|
|
123
171
|
}
|
|
124
|
-
|
|
172
|
+
import { ProviderList } from './fbs/weidmueller/ucontrol/hub/provider-list.js';
|
|
173
|
+
import { ReadProvidersQueryResponse } from './fbs/weidmueller/ucontrol/hub/read-providers-query-response.js';
|
|
125
174
|
import { ReadProviderDefinitionQueryResponse } from './fbs/weidmueller/ucontrol/hub/read-provider-definition-query-response.js';
|
|
126
|
-
|
|
127
|
-
export function
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!providerDef) return [];
|
|
133
|
-
|
|
175
|
+
import { VariableList } from './fbs/weidmueller/ucontrol/hub/variable-list.js';
|
|
176
|
+
export function decodeProviderList(bb) {
|
|
177
|
+
const response = ReadProvidersQueryResponse.getRootAsReadProvidersQueryResponse(bb);
|
|
178
|
+
const list = response.providers();
|
|
179
|
+
if (!list)
|
|
180
|
+
return [];
|
|
134
181
|
const result = [];
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Inverse mapping of DataType/AccessType needed if we want full fidelity,
|
|
141
|
-
// but for now we mainly need ID and Key.
|
|
142
|
-
result.push({
|
|
143
|
-
id: item.id(),
|
|
144
|
-
key: item.key(),
|
|
145
|
-
dataType: 'UNKNOWN', // Mapping back from Enum to String if strictly needed
|
|
146
|
-
access: item.accessType() === VariableAccessType.READ_WRITE ? 'READ_WRITE' : 'READ_ONLY'
|
|
147
|
-
});
|
|
182
|
+
for (let i = 0; i < list.itemsLength(); i += 1) {
|
|
183
|
+
const prov = list.items(i);
|
|
184
|
+
if (!prov)
|
|
185
|
+
continue;
|
|
186
|
+
result.push({ id: prov.id() || '' });
|
|
148
187
|
}
|
|
149
188
|
return result;
|
|
150
189
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
function buildVariableList(defs, states, fingerprint) {
|
|
162
|
-
const statesMap = new Map(states.map((s) => [s.id, s]));
|
|
163
|
-
const items = [];
|
|
164
|
-
for (const def of defs) {
|
|
165
|
-
const state = statesMap.get(def.id);
|
|
166
|
-
if (!state)
|
|
190
|
+
export function decodeProviderDefinition(bb) {
|
|
191
|
+
const response = ReadProviderDefinitionQueryResponse.getRootAsReadProviderDefinitionQueryResponse(bb);
|
|
192
|
+
const def = response.providerDefinition();
|
|
193
|
+
if (!def)
|
|
194
|
+
return null;
|
|
195
|
+
const variableDefs = [];
|
|
196
|
+
for (let i = 0; i < def.variableDefinitionsLength(); i += 1) {
|
|
197
|
+
const varDef = def.variableDefinitions(i);
|
|
198
|
+
if (!varDef)
|
|
167
199
|
continue;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
variable.quality = VariableQuality.GOOD;
|
|
175
|
-
items.push(variable);
|
|
200
|
+
variableDefs.push({
|
|
201
|
+
id: varDef.id(),
|
|
202
|
+
key: varDef.key() || '',
|
|
203
|
+
dataType: decodeDataType(varDef.dataType()),
|
|
204
|
+
access: decodeAccessType(varDef.accessType()),
|
|
205
|
+
});
|
|
176
206
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return list;
|
|
207
|
+
return {
|
|
208
|
+
fingerprint: def.fingerprint(),
|
|
209
|
+
variables: variableDefs,
|
|
210
|
+
};
|
|
182
211
|
}
|
|
183
|
-
function
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return ts;
|
|
212
|
+
function buildVariableList(defs, states, fingerprint) {
|
|
213
|
+
const variables = defs.map((def) => encodeVariableState(def, states[def.id]));
|
|
214
|
+
const list = new VariableListT(fingerprint, variables);
|
|
215
|
+
return list;
|
|
188
216
|
}
|
|
189
|
-
function
|
|
190
|
-
|
|
217
|
+
function encodeVariableState(def, state) {
|
|
218
|
+
const varT = new VariableT();
|
|
219
|
+
varT.id = def.id;
|
|
220
|
+
varT.quality = VariableQuality[state?.quality?.toUpperCase?.()] ?? VariableQuality[DEFAULT_QUALITY];
|
|
221
|
+
varT.timestamp = state?.timestamp !== undefined ? new TimestampT(BigInt(state.timestamp)) : nowNs();
|
|
222
|
+
const value = state?.value;
|
|
223
|
+
switch (def.dataType) {
|
|
191
224
|
case 'INT64': {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
225
|
+
const intVal = new VariableValueInt64T();
|
|
226
|
+
intVal.value = (value !== null && value !== undefined) ? BigInt(value) : BigInt(0);
|
|
227
|
+
varT.value = intVal;
|
|
228
|
+
varT.valueType = VariableValue.Int64;
|
|
229
|
+
break;
|
|
195
230
|
}
|
|
196
231
|
case 'FLOAT64': {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
232
|
+
const floatVal = new VariableValueFloat64T();
|
|
233
|
+
floatVal.value = (value !== null && value !== undefined) ? Number(value) : 0.0;
|
|
234
|
+
varT.value = floatVal;
|
|
235
|
+
varT.valueType = VariableValue.Float64;
|
|
236
|
+
break;
|
|
200
237
|
}
|
|
201
238
|
case 'BOOLEAN': {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
239
|
+
const boolVal = new VariableValueBooleanT();
|
|
240
|
+
boolVal.value = (value !== null && value !== undefined) ? Boolean(value) : false;
|
|
241
|
+
varT.value = boolVal;
|
|
242
|
+
varT.valueType = VariableValue.Boolean;
|
|
243
|
+
break;
|
|
205
244
|
}
|
|
206
245
|
case 'STRING':
|
|
207
246
|
default: {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
247
|
+
const strVal = new VariableValueStringT();
|
|
248
|
+
strVal.value = (value !== null && value !== undefined) ? String(value) : '';
|
|
249
|
+
varT.value = strVal;
|
|
250
|
+
varT.valueType = VariableValue.String;
|
|
251
|
+
break;
|
|
211
252
|
}
|
|
212
253
|
}
|
|
254
|
+
return varT;
|
|
255
|
+
}
|
|
256
|
+
function nowNs() {
|
|
257
|
+
return new TimestampT(BigInt(Date.now() * 1_000_000));
|
|
258
|
+
}
|
|
259
|
+
function decodeTimestamp(ts) {
|
|
260
|
+
return ts ? Number(ts.value()) : 0;
|
|
261
|
+
}
|
|
262
|
+
function decodeDataType(dt) {
|
|
263
|
+
switch (dt) {
|
|
264
|
+
case VariableDataType.INT64:
|
|
265
|
+
return 'INT64';
|
|
266
|
+
case VariableDataType.FLOAT64:
|
|
267
|
+
return 'FLOAT64';
|
|
268
|
+
case VariableDataType.BOOLEAN:
|
|
269
|
+
return 'BOOLEAN';
|
|
270
|
+
case VariableDataType.STRING:
|
|
271
|
+
default:
|
|
272
|
+
return 'STRING';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function decodeAccessType(dt) {
|
|
276
|
+
switch (dt) {
|
|
277
|
+
case VariableAccessType.READWRITE:
|
|
278
|
+
return 'READWRITE';
|
|
279
|
+
case VariableAccessType.READONLY:
|
|
280
|
+
default:
|
|
281
|
+
return 'READONLY';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function decodeQuality(q) {
|
|
285
|
+
switch (q) {
|
|
286
|
+
case VariableQuality.BAD:
|
|
287
|
+
return 'BAD';
|
|
288
|
+
case VariableQuality.UNCERTAIN:
|
|
289
|
+
return 'UNCERTAIN';
|
|
290
|
+
case VariableQuality.GOOD_LOCAL_OVERRIDE:
|
|
291
|
+
return 'GOOD_LOCAL_OVERRIDE';
|
|
292
|
+
case VariableQuality.GOOD:
|
|
293
|
+
default:
|
|
294
|
+
return 'GOOD';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function toFlatDefinition(def) {
|
|
298
|
+
const varDef = new VariableDefinitionT();
|
|
299
|
+
varDef.id = def.id;
|
|
300
|
+
varDef.key = def.key;
|
|
301
|
+
varDef.dataType = mapDataType(def.dataType);
|
|
302
|
+
varDef.accessType = mapAccessType(def.access);
|
|
303
|
+
if (def.experimental !== undefined && def.experimental !== null) {
|
|
304
|
+
varDef.experimental = Boolean(def.experimental);
|
|
305
|
+
}
|
|
306
|
+
return varDef;
|
|
307
|
+
}
|
|
308
|
+
function mapAccessType(access) {
|
|
309
|
+
switch (access?.toUpperCase?.()) {
|
|
310
|
+
case 'READWRITE':
|
|
311
|
+
return VariableAccessType.READWRITE;
|
|
312
|
+
case 'READONLY':
|
|
313
|
+
default:
|
|
314
|
+
return VariableAccessType.READONLY;
|
|
315
|
+
}
|
|
213
316
|
}
|
|
214
|
-
function mapDataType(
|
|
215
|
-
switch (
|
|
317
|
+
function mapDataType(dataType) {
|
|
318
|
+
switch (dataType?.toUpperCase?.()) {
|
|
216
319
|
case 'INT64':
|
|
217
320
|
return VariableDataType.INT64;
|
|
218
321
|
case 'FLOAT64':
|
package/nodes/datahub-input.html
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
if (this.name) return this.name;
|
|
18
18
|
return "DataHub - IN";
|
|
19
19
|
},
|
|
20
|
-
paletteLabel: "DataHub -
|
|
20
|
+
paletteLabel: "DataHub - Read",
|
|
21
21
|
oneditprepare: function () {
|
|
22
22
|
const node = this;
|
|
23
23
|
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
</script>
|
|
132
132
|
|
|
133
133
|
<script type="text/html" data-help-name="datahub-input">
|
|
134
|
-
<p>
|
|
134
|
+
<p><b>DataHub - Read</b> reads variables from u-OS Data Hub providers and outputs their values as JSON messages.</p>
|
|
135
135
|
|
|
136
136
|
<h3>Quick Start</h3>
|
|
137
137
|
<ol>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
label: function () {
|
|
15
15
|
return this.name || `DataHub - OUT ${this.providerId || ''}`;
|
|
16
16
|
},
|
|
17
|
-
paletteLabel: "DataHub -
|
|
17
|
+
paletteLabel: "DataHub - Provider",
|
|
18
18
|
labelStyle: function () {
|
|
19
19
|
return this.name ? 'node_label_italic' : '';
|
|
20
20
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</script>
|
|
38
38
|
|
|
39
39
|
<script type="text/html" data-help-name="datahub-output">
|
|
40
|
-
<p><
|
|
40
|
+
<p><b>DataHub - Provider</b> creates your own Data Hub provider that other applications can read from in real-time.</p>
|
|
41
41
|
<dl>
|
|
42
42
|
<dt>u-OS Config</dt>
|
|
43
43
|
<dd>Connection node with host/port and OAuth credentials.</dd>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('datahub-write', {
|
|
3
|
+
category: 'u-OS DataHub NATS',
|
|
4
|
+
color: '#e74c3c',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
connection: { type: "uos-config", required: true },
|
|
8
|
+
providerId: { value: "", required: true },
|
|
9
|
+
variableId: { value: "", required: false },
|
|
10
|
+
variableName: { value: "", required: false }
|
|
11
|
+
},
|
|
12
|
+
inputs: 1,
|
|
13
|
+
outputs: 1,
|
|
14
|
+
icon: "white/datahub-output.svg",
|
|
15
|
+
label: function () {
|
|
16
|
+
if (this.name) return this.name;
|
|
17
|
+
return `Write → ${this.providerId || 'provider'}`;
|
|
18
|
+
},
|
|
19
|
+
paletteLabel: "DataHub - Write",
|
|
20
|
+
oneditprepare: function () {
|
|
21
|
+
// Nothing special needed for now
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<script type="text/html" data-template-name="datahub-write">
|
|
27
|
+
<div class="form-row">
|
|
28
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
29
|
+
<input type="text" id="node-input-name" placeholder="Optional">
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="form-row">
|
|
33
|
+
<label for="node-input-connection"><i class="fa fa-globe"></i> Config</label>
|
|
34
|
+
<input type="text" id="node-input-connection">
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<label for="node-input-providerId"><i class="fa fa-server"></i> Provider ID</label>
|
|
39
|
+
<input type="text" id="node-input-providerId" placeholder="e.g. u_os_adm">
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="form-row">
|
|
43
|
+
<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">
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="form-tips">
|
|
48
|
+
<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
|
+
</div>
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<script type="text/html" data-help-name="datahub-write">
|
|
53
|
+
<p><b>DataHub - Write</b> sends write commands to external Data Hub providers to change variable values.</p>
|
|
54
|
+
|
|
55
|
+
<h3>Purpose</h3>
|
|
56
|
+
<p>Control variables in <b>other providers</b> (not your own). Use this to:</p>
|
|
57
|
+
<ul>
|
|
58
|
+
<li>Toggle flags (e.g., <code>enable_mode = true</code>)</li>
|
|
59
|
+
<li>Update setpoints (e.g., <code>target_temp = 25.5</code>)</li>
|
|
60
|
+
<li>Control external devices via Data Hub</li>
|
|
61
|
+
</ul>
|
|
62
|
+
|
|
63
|
+
<h3>Configuration</h3>
|
|
64
|
+
|
|
65
|
+
<h4>Provider ID</h4>
|
|
66
|
+
<p>The target provider to write to (e.g., <code>u_os_adm</code>, <code>u_os_sbm</code>).</p>
|
|
67
|
+
<p><b>Find it:</b> u-OS Web UI → Data Hub → Providers</p>
|
|
68
|
+
|
|
69
|
+
<h4>Variable ID</h4>
|
|
70
|
+
<p>The numeric ID of the variable to write.</p>
|
|
71
|
+
<p><b>Find it:</b> u-OS Web UI → Data Hub → Providers → (Select Provider) → Variables tab</p>
|
|
72
|
+
|
|
73
|
+
<h3>Input Format</h3>
|
|
74
|
+
<p>Send the new value as <code>msg.payload</code>:</p>
|
|
75
|
+
|
|
76
|
+
<h4>Example: Toggle Boolean</h4>
|
|
77
|
+
<pre><code>msg.payload = false;
|
|
78
|
+
return msg;</code></pre>
|
|
79
|
+
|
|
80
|
+
<h4>Example: Update Number</h4>
|
|
81
|
+
<pre><code>msg.payload = 42.5;
|
|
82
|
+
return msg;</code></pre>
|
|
83
|
+
|
|
84
|
+
<h4>Example: Change String</h4>
|
|
85
|
+
<pre><code>msg.payload = "stopped";
|
|
86
|
+
return msg;</code></pre>
|
|
87
|
+
|
|
88
|
+
<h3>Output</h3>
|
|
89
|
+
<p>Outputs a confirmation message when the write command is sent:</p>
|
|
90
|
+
<pre><code>{
|
|
91
|
+
"success": true,
|
|
92
|
+
"providerId": "u_os_adm",
|
|
93
|
+
"variableId": 5,
|
|
94
|
+
"value": false
|
|
95
|
+
}</code></pre>
|
|
96
|
+
|
|
97
|
+
<h3>Example Flow</h3>
|
|
98
|
+
<pre><code>[Inject: false] → [DataHub - Write] → [Debug]
|
|
99
|
+
(Provider: u_os_adm, Variable ID: 5)</code></pre>
|
|
100
|
+
|
|
101
|
+
<h3>Important Notes</h3>
|
|
102
|
+
<ul>
|
|
103
|
+
<li>⚠️ <b>Requires write permissions:</b> Your OAuth client must have <code>hub.variables.readwrite</code> scope</li>
|
|
104
|
+
<li>⚠️ <b>Provider must accept writes:</b> Some providers are read-only</li>
|
|
105
|
+
<li>⚠️ <b>Variable must exist:</b> The variable ID must be valid in the target provider</li>
|
|
106
|
+
</ul>
|
|
107
|
+
|
|
108
|
+
<h3>Troubleshooting</h3>
|
|
109
|
+
|
|
110
|
+
<h4>No Response / Error</h4>
|
|
111
|
+
<ul>
|
|
112
|
+
<li>Check Provider ID is correct (u-OS Web UI → Data Hub → Providers)</li>
|
|
113
|
+
<li>Verify Variable ID exists (Providers → Variables tab)</li>
|
|
114
|
+
<li>Ensure OAuth client has <code>hub.variables.readwrite</code> scope</li>
|
|
115
|
+
</ul>
|
|
116
|
+
|
|
117
|
+
<h4>Permission Denied</h4>
|
|
118
|
+
<ul>
|
|
119
|
+
<li>Check OAuth client scopes in u-OS (System → Access Control → OAuth Clients)</li>
|
|
120
|
+
<li>Verify provider allows writes (some system providers are read-only)</li>
|
|
121
|
+
</ul>
|
|
122
|
+
|
|
123
|
+
<div style="margin-top:15px; padding-top:10px; border-top:1px solid #ddd; font-size:0.85em; color:#666;">
|
|
124
|
+
<p><b>Developed by <a href="https://iotueli.ch" target="_blank">IoTUeli</a></b></p>
|
|
125
|
+
<p>Not an official Weidmüller product. Community contribution.</p>
|
|
126
|
+
</div>
|
|
127
|
+
</script>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { encodeWriteVariablesCommand } from '../lib/payloads.js';
|
|
2
|
+
|
|
3
|
+
export default function (RED) {
|
|
4
|
+
function DataHubWriteNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
// Get config node
|
|
9
|
+
const configNode = RED.nodes.getNode(config.connection);
|
|
10
|
+
if (!configNode) {
|
|
11
|
+
node.error('Missing u-OS config');
|
|
12
|
+
node.status({ fill: 'red', shape: 'dot', text: 'no config' });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Store configuration
|
|
17
|
+
this.providerId = config.providerId?.trim();
|
|
18
|
+
this.variableId = config.variableId ? parseInt(config.variableId, 10) : null;
|
|
19
|
+
|
|
20
|
+
if (!this.providerId) {
|
|
21
|
+
node.error('Provider ID is required');
|
|
22
|
+
node.status({ fill: 'red', shape: 'dot', text: 'no provider ID' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
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' });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
node.status({ fill: 'green', shape: 'ring', text: 'ready' });
|
|
33
|
+
|
|
34
|
+
// Handle incoming messages
|
|
35
|
+
node.on('input', async function (msg) {
|
|
36
|
+
const value = msg.payload;
|
|
37
|
+
|
|
38
|
+
if (value === undefined || value === null) {
|
|
39
|
+
node.warn('msg.payload is empty, nothing to write');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Get NATS connection from config node
|
|
45
|
+
const nc = await configNode.getNatsConnection();
|
|
46
|
+
if (!nc) {
|
|
47
|
+
node.error('NATS connection not available');
|
|
48
|
+
node.status({ fill: 'red', shape: 'dot', text: 'no connection' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build write command
|
|
53
|
+
const writeCommand = encodeWriteVariablesCommand([
|
|
54
|
+
{
|
|
55
|
+
id: node.variableId,
|
|
56
|
+
value: value
|
|
57
|
+
}
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// Publish write command
|
|
61
|
+
const subject = `v1.loc.${node.providerId}.vars.cmd.write`;
|
|
62
|
+
nc.publish(subject, writeCommand);
|
|
63
|
+
|
|
64
|
+
node.status({ fill: 'green', shape: 'dot', text: `wrote: ${value}` });
|
|
65
|
+
|
|
66
|
+
// Output confirmation
|
|
67
|
+
msg.payload = {
|
|
68
|
+
success: true,
|
|
69
|
+
providerId: node.providerId,
|
|
70
|
+
variableId: node.variableId,
|
|
71
|
+
value: value
|
|
72
|
+
};
|
|
73
|
+
node.send(msg);
|
|
74
|
+
|
|
75
|
+
} catch (err) {
|
|
76
|
+
node.error(`Write failed: ${err.message}`, msg);
|
|
77
|
+
node.status({ fill: 'red', shape: 'dot', text: 'write error' });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
node.on('close', function () {
|
|
82
|
+
node.status({});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
RED.nodes.registerType('datahub-write', DataHubWriteNode);
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read and
|
|
3
|
+
"version": "0.1.67",
|
|
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.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "IoTUeli",
|
|
7
7
|
"url": "https://www.linkedin.com/in/iotueli/"
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
"plc",
|
|
32
32
|
"variables",
|
|
33
33
|
"oauth2",
|
|
34
|
-
"flatbuffers"
|
|
34
|
+
"flatbuffers",
|
|
35
|
+
"write",
|
|
36
|
+
"read",
|
|
37
|
+
"provider"
|
|
35
38
|
],
|
|
36
39
|
"engines": {
|
|
37
40
|
"node": ">=14.0.0"
|
|
@@ -46,7 +49,8 @@
|
|
|
46
49
|
"nodes": {
|
|
47
50
|
"uos-config": "nodes/uos-config.js",
|
|
48
51
|
"datahub-input": "nodes/datahub-input.js",
|
|
49
|
-
"datahub-output": "nodes/datahub-output.js"
|
|
52
|
+
"datahub-output": "nodes/datahub-output.js",
|
|
53
|
+
"datahub-write": "nodes/datahub-write.js"
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
}
|