node-red-contrib-uos-nats 0.1.8 → 0.1.10
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 +21 -21
- package/nodes/datahub-input.html +11 -4
- package/nodes/datahub-output.html +9 -7
- package/nodes/datahub-output.js +13 -2
- package/nodes/uos-config.js +7 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,38 +31,38 @@ Fields:
|
|
|
31
31
|
- **Host / Port** – IP address of your controller (e.g. `192.168.10.100`) and the NATS port `49360`.
|
|
32
32
|
- **Client Name** – used for the NATS inbox prefix (`_INBOX.<name>`).
|
|
33
33
|
- **Client ID / Secret** – OAuth2 client credentials created in the Control Center.
|
|
34
|
-
- **Scope** –
|
|
34
|
+
- **Scope** – defaults to `hub.variables.provide hub.variables.readwrite hub.variables.readonly`. Note: The API does not require a specific "read providers" scope; listing providers is covered by the standard variable scopes.
|
|
35
35
|
- **Granted scopes** – click *Refresh* to query the token endpoint and show the scopes currently granted to that client.
|
|
36
36
|
|
|
37
|
-
The config node automatically fetches tokens via Client Credentials flow
|
|
37
|
+
The config node automatically fetches tokens via Client Credentials flow.
|
|
38
38
|
|
|
39
39
|
## DataHub Input Node
|
|
40
40
|
|
|
41
|
-
- Select the u-OS config node, then choose one of the discovered providers from the dropdown
|
|
42
|
-
-
|
|
43
|
-
-
|
|
41
|
+
- Select the u-OS config node, then choose one of the discovered providers from the dropdown.
|
|
42
|
+
- **Troubleshooting**: If the dropdown remains empty, check the Node-RED debug tab. The node logs the API response count. Ensure your OAuth client has `hub.variables.readonly` permission.
|
|
43
|
+
- Pick the variables you need from the multi-select list. Leave it empty to receive all variables.
|
|
44
|
+
|
|
45
|
+
## DataHub Output Node
|
|
46
|
+
|
|
47
|
+
- Reuses the u-OS config node. The provider ID defaults to `nodered` and is created automatically upon the first message.
|
|
48
|
+
- Send a JSON object to the input pin. **Nested objects are supported** and create subcategories automatically:
|
|
44
49
|
```json
|
|
45
50
|
{
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"key": "diagnostics.status_text",
|
|
52
|
-
"value": "running",
|
|
53
|
-
"quality": "GOOD",
|
|
54
|
-
"timestampNs": 1700000000000
|
|
51
|
+
"machine": {
|
|
52
|
+
"temperature": 45.2,
|
|
53
|
+
"status": {
|
|
54
|
+
"active": true,
|
|
55
|
+
"mode": "remote"
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
}
|
|
57
58
|
}
|
|
58
59
|
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
This creates/updates the following variables:
|
|
61
|
+
- `machine.temperature` (FLOAT64)
|
|
62
|
+
- `machine.status.active` (BOOLEAN)
|
|
63
|
+
- `machine.status.mode` (STRING)
|
|
62
64
|
|
|
63
|
-
-
|
|
64
|
-
- Send a JSON object to the input pin. Nested objects become dot-separated keys (e.g. `{ "line1": { "status": "ok" } }` ⇒ `line1.status`).
|
|
65
|
-
- The node infers data types (INT64/FLOAT64/BOOLEAN/STRING) and publishes `VariablesChangedEvent`s. New keys trigger an automatic provider definition update.
|
|
65
|
+
- The node infers data types (INT64/FLOAT64/BOOLEAN/STRING) and automatically publishes definition updates when new keys are seen.
|
|
66
66
|
- Read requests (`v1.loc.<provider>.vars.qry.read`) are answered using the most recent values, so other consumers can subscribe to your Node-RED provider.
|
|
67
67
|
|
|
68
68
|
## Example Flow
|
package/nodes/datahub-input.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
inputs: 0,
|
|
12
12
|
outputs: 1,
|
|
13
13
|
icon: "white/datahub-input.svg",
|
|
14
|
-
oneditprepare: function() {
|
|
14
|
+
oneditprepare: function () {
|
|
15
15
|
const node = this;
|
|
16
16
|
const $config = $('#node-input-connection');
|
|
17
17
|
const $providerHidden = $('#node-input-providerId');
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
.filter((entry) => entry.length > 0);
|
|
30
30
|
|
|
31
31
|
const normalizeProviders = (payload) => {
|
|
32
|
+
console.log('normalizeProviders payload:', payload);
|
|
32
33
|
if (!payload) {
|
|
33
34
|
return [];
|
|
34
35
|
}
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
const normalizeVariables = (payload) => {
|
|
46
|
+
console.log('normalizeVariables payload:', payload);
|
|
45
47
|
if (!payload) {
|
|
46
48
|
return [];
|
|
47
49
|
}
|
|
@@ -54,8 +56,11 @@
|
|
|
54
56
|
return [];
|
|
55
57
|
};
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
+
// API returns [{ "id": "..." }] for providers
|
|
60
|
+
const providerLabel = (provider) => provider.displayName || provider.name || provider.id || provider.providerId || 'Unnamed provider';
|
|
61
|
+
const providerValue = (provider) => provider.id || provider.providerId || provider.name || '';
|
|
62
|
+
|
|
63
|
+
// API returns [{ "key": "..." }] for variables
|
|
59
64
|
const variableValue = (variable) => variable.key || variable.name || variable.path || variable.id || '';
|
|
60
65
|
const variableLabel = (variable) => variable.tagName || variable.key || variable.name || variable.path || variable.id || 'Unnamed variable';
|
|
61
66
|
|
|
@@ -130,6 +135,7 @@
|
|
|
130
135
|
setVariableStatus('Select one or multiple variables (Ctrl/Cmd + click). Leave empty to receive all.');
|
|
131
136
|
})
|
|
132
137
|
.fail((xhr) => {
|
|
138
|
+
console.error('Variable load failed', xhr);
|
|
133
139
|
const msg = xhr?.responseJSON?.error || xhr.statusText || 'Failed to load variables.';
|
|
134
140
|
setVariableStatus(msg);
|
|
135
141
|
$variables.prop('disabled', true);
|
|
@@ -191,6 +197,7 @@
|
|
|
191
197
|
loadVariables(selected);
|
|
192
198
|
})
|
|
193
199
|
.fail((xhr) => {
|
|
200
|
+
console.error('Provider load failed', xhr);
|
|
194
201
|
const msg = xhr?.responseJSON?.error || xhr.statusText || 'Failed to load providers.';
|
|
195
202
|
showManualProvider(`Provider list failed (${msg}). Enter the provider ID manually or grant hub.variables.readonly.`);
|
|
196
203
|
});
|
|
@@ -286,4 +293,4 @@
|
|
|
286
293
|
{ "name": "diagnostics.status_text", "value": "ready" }
|
|
287
294
|
]
|
|
288
295
|
}</pre>
|
|
289
|
-
</script>
|
|
296
|
+
</script>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
inputs: 1,
|
|
11
11
|
outputs: 1,
|
|
12
12
|
icon: "white/datahub-output.svg",
|
|
13
|
-
oneditprepare: function() {},
|
|
13
|
+
oneditprepare: function () { },
|
|
14
14
|
label: function () {
|
|
15
15
|
return this.name || `Output ${this.providerId || ''}`;
|
|
16
16
|
},
|
|
@@ -43,12 +43,14 @@
|
|
|
43
43
|
<dt>Provider ID</dt>
|
|
44
44
|
<dd>Defaults to <code>nodered</code>; change it if you want to publish under another provider name.</dd>
|
|
45
45
|
</dl>
|
|
46
|
-
<p>Send a JSON object as <code>msg.payload</code>. Nested objects are flattened using dot-notation:</p>
|
|
46
|
+
<p>Send a JSON object as <code>msg.payload</code>. Nested objects are flattened using dot-notation to create <strong>subcategories</strong> automatically:</p>
|
|
47
47
|
<pre>{
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
48
|
+
"machine": {
|
|
49
|
+
"status": "running",
|
|
50
|
+
"details": {
|
|
51
|
+
"temp": 45.2
|
|
52
|
+
}
|
|
51
53
|
}
|
|
52
54
|
}</pre>
|
|
53
|
-
<p>becomes variables <code>
|
|
54
|
-
</script>
|
|
55
|
+
<p>becomes variables <code>machine.status</code> and <code>machine.details.temp</code> in the Data Hub.</p>
|
|
56
|
+
</script>
|
package/nodes/datahub-output.js
CHANGED
|
@@ -35,12 +35,16 @@ const flattenPayload = (value, prefix = '') => {
|
|
|
35
35
|
const path = (key) => (prefix ? `${prefix}.${key}` : key);
|
|
36
36
|
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
37
37
|
Object.entries(value).forEach(([key, val]) => {
|
|
38
|
-
|
|
38
|
+
if (val !== undefined) {
|
|
39
|
+
entries.push(...flattenPayload(val, path(key)));
|
|
40
|
+
}
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
else if (Array.isArray(value)) {
|
|
42
44
|
value.forEach((val, idx) => {
|
|
43
|
-
|
|
45
|
+
if (val !== undefined) {
|
|
46
|
+
entries.push(...flattenPayload(val, prefix ? `${prefix}[${idx}]` : `[${idx}]`));
|
|
47
|
+
}
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
else {
|
|
@@ -129,14 +133,21 @@ module.exports = function (RED) {
|
|
|
129
133
|
return;
|
|
130
134
|
}
|
|
131
135
|
const entries = flattenPayload(msg.payload);
|
|
136
|
+
|
|
137
|
+
// Optimization: If payload is empty after flattening (e.g. only undefined values), stop here
|
|
132
138
|
if (!entries.length) {
|
|
133
139
|
done();
|
|
134
140
|
return;
|
|
135
141
|
}
|
|
142
|
+
|
|
136
143
|
const [payloadsMod, subjectsMod] = await loadModules();
|
|
137
144
|
let definitionsChanged = false;
|
|
138
145
|
const states = [];
|
|
146
|
+
|
|
139
147
|
entries.forEach(({ key, value }) => {
|
|
148
|
+
// Ensure we don't accidentally send undefined/null as value if logic slipped through
|
|
149
|
+
if (value === undefined || value === null) return;
|
|
150
|
+
|
|
140
151
|
const { def, created } = ensureDefinition(key, inferType(value));
|
|
141
152
|
if (created) {
|
|
142
153
|
definitionsChanged = true;
|
package/nodes/uos-config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fetch = require('node-fetch');
|
|
2
2
|
const { connect } = require('nats');
|
|
3
|
-
const DEFAULT_SCOPE = 'hub.variables.provide hub.variables.readwrite hub.variables.readonly';
|
|
3
|
+
const DEFAULT_SCOPE = 'hub.variables.provide hub.variables.readwrite hub.variables.readonly'; // hub.providers.read removed as it does not exist
|
|
4
4
|
|
|
5
5
|
if (!process.env.NODE_TLS_REJECT_UNAUTHORIZED) {
|
|
6
6
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
@@ -98,7 +98,9 @@ module.exports = function (RED) {
|
|
|
98
98
|
if (!res.ok) {
|
|
99
99
|
throw new Error(`Provider list failed: ${res.status}`);
|
|
100
100
|
}
|
|
101
|
-
|
|
101
|
+
const json = await res.json();
|
|
102
|
+
this.log(`Fetched ${Array.isArray(json) ? json.length : 'unknown'} providers from API`);
|
|
103
|
+
return json;
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
this.fetchProviderVariables = async (providerId) => {
|
|
@@ -112,7 +114,9 @@ module.exports = function (RED) {
|
|
|
112
114
|
if (!res.ok) {
|
|
113
115
|
throw new Error(`Variable list failed: ${res.status}`);
|
|
114
116
|
}
|
|
115
|
-
|
|
117
|
+
const json = await res.json();
|
|
118
|
+
this.log(`Fetched ${Array.isArray(json) ? json.length : 'unknown'} variables via API for provider ${providerId}`);
|
|
119
|
+
return json;
|
|
116
120
|
};
|
|
117
121
|
|
|
118
122
|
this.acquire = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Node-RED nodes for u-OS Data Hub via NATS",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -31,4 +31,4 @@
|
|
|
31
31
|
"datahub-output": "nodes/datahub-output.js"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|