node-red-contrib-uos-nats 0.1.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 +92 -0
- package/icons/white/datahub-input.svg +4 -0
- package/icons/white/datahub-output.svg +4 -0
- package/lib/auth.js +27 -0
- package/lib/config.js +23 -0
- package/lib/consumer.js +41 -0
- package/lib/fbs/weidmueller/ucontrol/hub/duration.js +67 -0
- package/lib/fbs/weidmueller/ucontrol/hub/provider-definition-changed-event.js +69 -0
- package/lib/fbs/weidmueller/ucontrol/hub/provider-definition-state.js +20 -0
- package/lib/fbs/weidmueller/ucontrol/hub/provider-definition.js +118 -0
- package/lib/fbs/weidmueller/ucontrol/hub/provider-list.js +76 -0
- package/lib/fbs/weidmueller/ucontrol/hub/provider.js +59 -0
- package/lib/fbs/weidmueller/ucontrol/hub/providers-changed-event.js +69 -0
- package/lib/fbs/weidmueller/ucontrol/hub/read-provider-definition-query-request.js +51 -0
- package/lib/fbs/weidmueller/ucontrol/hub/read-provider-definition-query-response.js +68 -0
- package/lib/fbs/weidmueller/ucontrol/hub/read-providers-query-request.js +51 -0
- package/lib/fbs/weidmueller/ucontrol/hub/read-providers-query-response.js +69 -0
- package/lib/fbs/weidmueller/ucontrol/hub/read-variables-query-request.js +88 -0
- package/lib/fbs/weidmueller/ucontrol/hub/read-variables-query-response.js +69 -0
- package/lib/fbs/weidmueller/ucontrol/hub/state-changed-event.js +67 -0
- package/lib/fbs/weidmueller/ucontrol/hub/state.js +14 -0
- package/lib/fbs/weidmueller/ucontrol/hub/timestamp.js +62 -0
- package/lib/fbs/weidmueller/ucontrol/hub/uuid.js +43 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-access-type.js +17 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-data-type.js +15 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-definition.js +115 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-list.js +106 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-quality.js +31 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-value-boolean.js +57 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-value-duration.js +58 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-value-float64.js +57 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-value-int64.js +57 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-value-string.js +58 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-value-timestamp.js +58 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable-value.js +45 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variable.js +131 -0
- package/lib/fbs/weidmueller/ucontrol/hub/variables-changed-event.js +71 -0
- package/lib/fbs/weidmueller/ucontrol/hub/write-variables-command.js +69 -0
- package/lib/fbs/weidmueller/ucontrol/hub.js +17 -0
- package/lib/models.js +1 -0
- package/lib/payloads.js +182 -0
- package/lib/provider.js +51 -0
- package/lib/simulation.js +54 -0
- package/lib/subjects.js +11 -0
- package/nodes/datahub-input.html +157 -0
- package/nodes/datahub-input.js +129 -0
- package/nodes/datahub-output.html +36 -0
- package/nodes/datahub-output.js +189 -0
- package/nodes/uos-config.html +48 -0
- package/nodes/uos-config.js +173 -0
- package/package.json +25 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
function fetchProviders(configId) {
|
|
4
|
+
if (!configId) return Promise.resolve([]);
|
|
5
|
+
return $.getJSON(`uos/providers/${configId}`);
|
|
6
|
+
}
|
|
7
|
+
function fetchVariables(configId, providerId) {
|
|
8
|
+
if (!configId || !providerId) return Promise.resolve([]);
|
|
9
|
+
return $.getJSON(`uos/providers/${configId}/${providerId}/variables`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
RED.nodes.registerType('datahub-input', {
|
|
13
|
+
category: 'input',
|
|
14
|
+
color: '#ff9f43',
|
|
15
|
+
defaults: {
|
|
16
|
+
name: { value: '' },
|
|
17
|
+
connection: { type: 'uos-config', required: true },
|
|
18
|
+
providerId: { value: '', required: true },
|
|
19
|
+
variableMode: { value: 'all' },
|
|
20
|
+
variables: { value: '[]' }
|
|
21
|
+
},
|
|
22
|
+
inputs: 0,
|
|
23
|
+
outputs: 1,
|
|
24
|
+
icon: 'white/datahub-input.svg',
|
|
25
|
+
label: function() {
|
|
26
|
+
return this.name || `DataHub Input ${this.providerId || ''}`;
|
|
27
|
+
},
|
|
28
|
+
labelStyle: function() {
|
|
29
|
+
return this.name ? 'node_label_italic' : '';
|
|
30
|
+
},
|
|
31
|
+
oneditprepare: function() {
|
|
32
|
+
const providerField = $('#node-input-providerId');
|
|
33
|
+
const modeField = $('#node-input-variableMode');
|
|
34
|
+
const singleField = $('#node-input-variableSingle');
|
|
35
|
+
const multiField = $('#node-input-variableMulti');
|
|
36
|
+
const multiContainer = $('#node-input-variableMulti-container');
|
|
37
|
+
const singleContainer = $('#node-input-variableSingle-container');
|
|
38
|
+
const configField = $('#node-input-connection');
|
|
39
|
+
let cachedVariables = [];
|
|
40
|
+
|
|
41
|
+
function applyMode() {
|
|
42
|
+
const mode = modeField.val();
|
|
43
|
+
if (mode === 'single') {
|
|
44
|
+
singleContainer.show();
|
|
45
|
+
multiContainer.hide();
|
|
46
|
+
} else if (mode === 'multi') {
|
|
47
|
+
singleContainer.hide();
|
|
48
|
+
multiContainer.show();
|
|
49
|
+
} else {
|
|
50
|
+
singleContainer.hide();
|
|
51
|
+
multiContainer.hide();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function populateVariables(list) {
|
|
56
|
+
cachedVariables = list;
|
|
57
|
+
singleField.empty();
|
|
58
|
+
multiField.empty();
|
|
59
|
+
list.forEach(v => {
|
|
60
|
+
$('<option>').val(v.key).text(v.key).appendTo(singleField);
|
|
61
|
+
$('<option>').val(v.key).text(v.key).appendTo(multiField);
|
|
62
|
+
});
|
|
63
|
+
const stored = [];
|
|
64
|
+
try { stored.push(...JSON.parse($('#node-input-variables').val() || '[]')); } catch(e) {}
|
|
65
|
+
if (stored.length) {
|
|
66
|
+
singleField.val(stored[0] || '');
|
|
67
|
+
multiField.val(stored);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loadVariables() {
|
|
72
|
+
const cfg = configField.val();
|
|
73
|
+
const provider = providerField.val();
|
|
74
|
+
if (!cfg || !provider) {
|
|
75
|
+
populateVariables([]);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
fetchVariables(cfg, provider).then((vars) => {
|
|
79
|
+
const normalized = (vars || []).map(v => ({ key: v.key || v.id }));
|
|
80
|
+
populateVariables(normalized);
|
|
81
|
+
}).catch(() => populateVariables([]));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadProviders() {
|
|
85
|
+
const node = this;
|
|
86
|
+
const cfg = configField.val();
|
|
87
|
+
providerField.empty();
|
|
88
|
+
if (!cfg) {
|
|
89
|
+
providerField.append('<option value="">-- select config first --</option>');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
fetchProviders(cfg).then((providers) => {
|
|
93
|
+
providerField.append('<option value="">-- choose provider --</option>');
|
|
94
|
+
(providers || []).forEach((p) => {
|
|
95
|
+
$('<option>').val(p.id).text(p.id).appendTo(providerField);
|
|
96
|
+
});
|
|
97
|
+
if (node.providerId) {
|
|
98
|
+
providerField.val(node.providerId);
|
|
99
|
+
}
|
|
100
|
+
loadVariables();
|
|
101
|
+
}).catch(() => {
|
|
102
|
+
providerField.append('<option value="">(error loading providers)</option>');
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
modeField.on('change', applyMode);
|
|
107
|
+
providerField.on('change', loadVariables);
|
|
108
|
+
configField.on('change', loadProviders.bind(this));
|
|
109
|
+
|
|
110
|
+
applyMode();
|
|
111
|
+
loadProviders.call(this);
|
|
112
|
+
},
|
|
113
|
+
oneditsave: function() {
|
|
114
|
+
const mode = $('#node-input-variableMode').val();
|
|
115
|
+
let selected = [];
|
|
116
|
+
if (mode === 'single') {
|
|
117
|
+
const val = $('#node-input-variableSingle').val();
|
|
118
|
+
if (val) selected = [val];
|
|
119
|
+
} else if (mode === 'multi') {
|
|
120
|
+
selected = $('#node-input-variableMulti').val() || [];
|
|
121
|
+
}
|
|
122
|
+
$('#node-input-variables').val(JSON.stringify(selected));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
})();
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<input type="hidden" id="node-input-variables">
|
|
129
|
+
<div class="form-row">
|
|
130
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
131
|
+
<input type="text" id="node-input-name">
|
|
132
|
+
</div>
|
|
133
|
+
<div class="form-row">
|
|
134
|
+
<label for="node-input-connection"><i class="fa fa-cube"></i> u-OS Config</label>
|
|
135
|
+
<input type="text" id="node-input-connection">
|
|
136
|
+
</div>
|
|
137
|
+
<div class="form-row">
|
|
138
|
+
<label for="node-input-providerId"><i class="fa fa-list"></i> Provider</label>
|
|
139
|
+
<select id="node-input-providerId"></select>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="form-row">
|
|
142
|
+
<label for="node-input-variableMode"><i class="fa fa-filter"></i> Variables</label>
|
|
143
|
+
<select id="node-input-variableMode">
|
|
144
|
+
<option value="all">All variables</option>
|
|
145
|
+
<option value="single">Single variable</option>
|
|
146
|
+
<option value="multi">Multi selection</option>
|
|
147
|
+
</select>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="form-row" id="node-input-variableSingle-container" style="display:none;">
|
|
150
|
+
<label><i class="fa fa-bullseye"></i> Variable</label>
|
|
151
|
+
<select id="node-input-variableSingle" style="width:100%"></select>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="form-row" id="node-input-variableMulti-container" style="display:none;">
|
|
154
|
+
<label><i class="fa fa-list-ul"></i> Variables</label>
|
|
155
|
+
<select id="node-input-variableMulti" multiple size="8" style="width:100%"> </select>
|
|
156
|
+
</div>
|
|
157
|
+
<p>Outputs messages with <code>{{payload.type}}</code> = <code>snapshot</code> or <code>change</code> and a <code>variables</code> array containing the selected Data Hub values.</p>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { pathToFileURL } = require('url');
|
|
3
|
+
const flatbuffers = require('flatbuffers');
|
|
4
|
+
|
|
5
|
+
const payloadModuleUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'payloads.js')).href;
|
|
6
|
+
const subjectsModuleUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'subjects.js')).href;
|
|
7
|
+
const readResponseUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'fbs', 'weidmueller', 'ucontrol', 'hub', 'read-variables-query-response.js')).href;
|
|
8
|
+
const changeEventUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'fbs', 'weidmueller', 'ucontrol', 'hub', 'variables-changed-event.js')).href;
|
|
9
|
+
|
|
10
|
+
const loadModules = () => Promise.all([
|
|
11
|
+
import(payloadModuleUrl),
|
|
12
|
+
import(subjectsModuleUrl),
|
|
13
|
+
import(readResponseUrl),
|
|
14
|
+
import(changeEventUrl),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const normalizeKey = (key) => (key ? String(key).trim() : '');
|
|
18
|
+
|
|
19
|
+
module.exports = function (RED) {
|
|
20
|
+
function DataHubInputNode(config) {
|
|
21
|
+
RED.nodes.createNode(this, config);
|
|
22
|
+
const connection = RED.nodes.getNode(config.connection);
|
|
23
|
+
if (!connection) {
|
|
24
|
+
this.status({ fill: 'red', shape: 'ring', text: 'missing config' });
|
|
25
|
+
this.error('Please select a u-OS config node.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.providerId = config.providerId || 'sampleprovider';
|
|
30
|
+
this.variableMode = config.variableMode || 'all';
|
|
31
|
+
try {
|
|
32
|
+
this.variables = JSON.parse(config.variables || '[]').map(normalizeKey).filter((k) => k);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
this.variables = [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let nc;
|
|
39
|
+
let sub;
|
|
40
|
+
let closed = false;
|
|
41
|
+
const defMap = new Map();
|
|
42
|
+
|
|
43
|
+
const shouldInclude = (key) => {
|
|
44
|
+
if (this.variableMode === 'all' || !this.variables.length) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const needle = normalizeKey(key);
|
|
48
|
+
if (this.variableMode === 'single') {
|
|
49
|
+
return needle === this.variables[0];
|
|
50
|
+
}
|
|
51
|
+
return this.variables.includes(needle);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const processStates = (states) => {
|
|
55
|
+
return states
|
|
56
|
+
.map((state) => ({
|
|
57
|
+
providerId: this.providerId,
|
|
58
|
+
id: state.id,
|
|
59
|
+
key: defMap.get(state.id)?.key || state.id,
|
|
60
|
+
value: state.value,
|
|
61
|
+
quality: state.quality,
|
|
62
|
+
timestampNs: state.timestampNs,
|
|
63
|
+
}))
|
|
64
|
+
.filter((state) => shouldInclude(state.key));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const start = async () => {
|
|
68
|
+
try {
|
|
69
|
+
this.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
|
|
70
|
+
const [payloads, subjects, readRespMod, changeEventMod] = await loadModules();
|
|
71
|
+
const { ReadVariablesQueryResponse } = readRespMod;
|
|
72
|
+
const { VariablesChangedEvent } = changeEventMod;
|
|
73
|
+
const definitions = await connection.fetchProviderVariables(this.providerId);
|
|
74
|
+
definitions.forEach((def) => defMap.set(def.id, def));
|
|
75
|
+
nc = await connection.acquire();
|
|
76
|
+
this.status({ fill: 'green', shape: 'dot', text: 'connected' });
|
|
77
|
+
|
|
78
|
+
const snapshotMsg = await nc.request(subjects.readVariablesQuery(this.providerId), payloads.buildReadVariablesQuery(), { timeout: 2000 });
|
|
79
|
+
const bb = new flatbuffers.ByteBuffer(snapshotMsg.data);
|
|
80
|
+
const snapshotObj = ReadVariablesQueryResponse.getRootAsReadVariablesQueryResponse(bb);
|
|
81
|
+
const states = payloads.decodeVariableList(snapshotObj.variables());
|
|
82
|
+
const filteredSnapshot = processStates(states);
|
|
83
|
+
if (filteredSnapshot.length) {
|
|
84
|
+
this.send({ payload: { type: 'snapshot', variables: filteredSnapshot } });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sub = nc.subscribe(subjects.varsChangedEvent(this.providerId));
|
|
88
|
+
(async () => {
|
|
89
|
+
for await (const msg of sub) {
|
|
90
|
+
const eventBB = new flatbuffers.ByteBuffer(msg.data);
|
|
91
|
+
const event = VariablesChangedEvent.getRootAsVariablesChangedEvent(eventBB);
|
|
92
|
+
const changeStates = payloads.decodeVariableList(event.changedVariables());
|
|
93
|
+
const filtered = processStates(changeStates);
|
|
94
|
+
if (filtered.length === 0) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
this.send({ payload: { type: 'change', variables: filtered } });
|
|
98
|
+
}
|
|
99
|
+
})().catch((err) => this.warn(`subscription error: ${err.message}`));
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
this.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
103
|
+
this.error(err.message);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
start();
|
|
108
|
+
|
|
109
|
+
this.on('close', async (done) => {
|
|
110
|
+
if (closed) {
|
|
111
|
+
done();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
closed = true;
|
|
115
|
+
try {
|
|
116
|
+
if (sub) {
|
|
117
|
+
await sub.drain();
|
|
118
|
+
}
|
|
119
|
+
await connection.release();
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
this.warn(`closing error: ${err.message}`);
|
|
123
|
+
}
|
|
124
|
+
done();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
RED.nodes.registerType('datahub-input', DataHubInputNode);
|
|
129
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
(function() {
|
|
3
|
+
RED.nodes.registerType('datahub-output', {
|
|
4
|
+
category: 'output',
|
|
5
|
+
color: '#ff9f43',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
connection: { type: 'uos-config', required: true },
|
|
9
|
+
providerId: { value: 'nodered', required: true }
|
|
10
|
+
},
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 1,
|
|
13
|
+
icon: 'white/datahub-output.svg',
|
|
14
|
+
label: function() {
|
|
15
|
+
return this.name || `DataHub Output ${this.providerId || ''}`;
|
|
16
|
+
},
|
|
17
|
+
labelStyle: function() {
|
|
18
|
+
return this.name ? 'node_label_italic' : '';
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
})();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
26
|
+
<input type="text" id="node-input-name">
|
|
27
|
+
</div>
|
|
28
|
+
<div class="form-row">
|
|
29
|
+
<label for="node-input-connection"><i class="fa fa-cube"></i> u-OS Config</label>
|
|
30
|
+
<input type="text" id="node-input-connection">
|
|
31
|
+
</div>
|
|
32
|
+
<div class="form-row">
|
|
33
|
+
<label for="node-input-providerId"><i class="fa fa-id-badge"></i> Provider ID</label>
|
|
34
|
+
<input type="text" id="node-input-providerId" placeholder="nodered">
|
|
35
|
+
</div>
|
|
36
|
+
<p>Send JSON payloads (including nested structures). The node auto-registers variables based on the object keys and publishes them to the configured provider. Nested objects become dot-separated keys (e.g. <code>folder.status</code>).</p>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { pathToFileURL } = require('url');
|
|
3
|
+
|
|
4
|
+
const payloadModuleUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'payloads.js')).href;
|
|
5
|
+
const subjectsModuleUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'subjects.js')).href;
|
|
6
|
+
|
|
7
|
+
const loadModules = () => Promise.all([
|
|
8
|
+
import(payloadModuleUrl),
|
|
9
|
+
import(subjectsModuleUrl),
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const inferType = (value) => {
|
|
13
|
+
if (typeof value === 'boolean') return 'BOOLEAN';
|
|
14
|
+
if (typeof value === 'number') {
|
|
15
|
+
return Number.isInteger(value) ? 'INT64' : 'FLOAT64';
|
|
16
|
+
}
|
|
17
|
+
return 'STRING';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const defaultValue = (type) => {
|
|
21
|
+
switch (type) {
|
|
22
|
+
case 'BOOLEAN':
|
|
23
|
+
return false;
|
|
24
|
+
case 'INT64':
|
|
25
|
+
case 'FLOAT64':
|
|
26
|
+
return 0;
|
|
27
|
+
case 'STRING':
|
|
28
|
+
default:
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const flattenPayload = (value, prefix = '') => {
|
|
34
|
+
const entries = [];
|
|
35
|
+
const path = (key) => (prefix ? `${prefix}.${key}` : key);
|
|
36
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
37
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
38
|
+
entries.push(...flattenPayload(val, path(key)));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else if (Array.isArray(value)) {
|
|
42
|
+
value.forEach((val, idx) => {
|
|
43
|
+
entries.push(...flattenPayload(val, prefix ? `${prefix}[${idx}]` : `[${idx}]`));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const keyName = prefix || 'value';
|
|
48
|
+
entries.push({ key: keyName, value });
|
|
49
|
+
}
|
|
50
|
+
return entries;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
module.exports = function (RED) {
|
|
54
|
+
function DataHubOutputNode(config) {
|
|
55
|
+
RED.nodes.createNode(this, config);
|
|
56
|
+
const connection = RED.nodes.getNode(config.connection);
|
|
57
|
+
if (!connection) {
|
|
58
|
+
this.status({ fill: 'red', shape: 'ring', text: 'missing config' });
|
|
59
|
+
this.error('Please select a u-OS config node.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.providerId = config.providerId || 'nodered';
|
|
63
|
+
|
|
64
|
+
const defMap = new Map();
|
|
65
|
+
const definitions = [];
|
|
66
|
+
const stateMap = new Map();
|
|
67
|
+
let nextId = 0;
|
|
68
|
+
let fingerprint = 0;
|
|
69
|
+
let nc;
|
|
70
|
+
let sub;
|
|
71
|
+
|
|
72
|
+
const ensureDefinition = (key, dataType) => {
|
|
73
|
+
const normalized = key.trim();
|
|
74
|
+
if (defMap.has(normalized)) {
|
|
75
|
+
return { def: defMap.get(normalized), created: false };
|
|
76
|
+
}
|
|
77
|
+
const def = {
|
|
78
|
+
id: nextId += 1,
|
|
79
|
+
key: normalized,
|
|
80
|
+
dataType,
|
|
81
|
+
access: 'READ_WRITE',
|
|
82
|
+
};
|
|
83
|
+
defMap.set(normalized, def);
|
|
84
|
+
definitions.push(def);
|
|
85
|
+
stateMap.set(def.id, {
|
|
86
|
+
id: def.id,
|
|
87
|
+
value: defaultValue(dataType),
|
|
88
|
+
timestampNs: Date.now() * 1_000_000,
|
|
89
|
+
quality: 'GOOD',
|
|
90
|
+
});
|
|
91
|
+
return { def, created: true };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const sendDefinitionUpdate = async (payloads, subjects) => {
|
|
95
|
+
const { payload, fingerprint: fp } = payloads.buildProviderDefinitionEvent(definitions);
|
|
96
|
+
fingerprint = fp;
|
|
97
|
+
await nc.publish(subjects.providerDefinitionChanged(this.providerId), payload);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleRead = async (payloads, msg) => {
|
|
101
|
+
if (!msg.reply)
|
|
102
|
+
return;
|
|
103
|
+
const snapshot = Array.from(stateMap.values());
|
|
104
|
+
const response = payloads.buildReadVariablesResponse(definitions, snapshot, fingerprint);
|
|
105
|
+
await nc.publish(msg.reply, response);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const start = async () => {
|
|
109
|
+
try {
|
|
110
|
+
this.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
|
|
111
|
+
const [payloads, subjects] = await loadModules();
|
|
112
|
+
nc = await connection.acquire();
|
|
113
|
+
await sendDefinitionUpdate(payloads, subjects);
|
|
114
|
+
sub = nc.subscribe(subjects.readVariablesQuery(this.providerId), {
|
|
115
|
+
callback: (err, msg) => {
|
|
116
|
+
if (err) {
|
|
117
|
+
this.warn(`Read request error: ${err.message}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
handleRead(payloads, msg).catch((error) => this.warn(error.message));
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
this.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
124
|
+
|
|
125
|
+
this.on('input', async (msg, send, done) => {
|
|
126
|
+
try {
|
|
127
|
+
if (!msg || !msg.payload || typeof msg.payload !== 'object') {
|
|
128
|
+
done(new Error('Payload must be an object describing your structure.'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const entries = flattenPayload(msg.payload);
|
|
132
|
+
if (!entries.length) {
|
|
133
|
+
done();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const [payloadsMod, subjectsMod] = await loadModules();
|
|
137
|
+
let definitionsChanged = false;
|
|
138
|
+
const states = [];
|
|
139
|
+
entries.forEach(({ key, value }) => {
|
|
140
|
+
const { def, created } = ensureDefinition(key, inferType(value));
|
|
141
|
+
if (created) {
|
|
142
|
+
definitionsChanged = true;
|
|
143
|
+
}
|
|
144
|
+
const state = {
|
|
145
|
+
id: def.id,
|
|
146
|
+
value,
|
|
147
|
+
timestampNs: Date.now() * 1_000_000,
|
|
148
|
+
quality: 'GOOD',
|
|
149
|
+
};
|
|
150
|
+
states.push(state);
|
|
151
|
+
stateMap.set(def.id, state);
|
|
152
|
+
});
|
|
153
|
+
if (definitionsChanged) {
|
|
154
|
+
await sendDefinitionUpdate(payloadsMod, subjectsMod);
|
|
155
|
+
}
|
|
156
|
+
const payload = payloadsMod.buildVariablesChangedEvent(definitions, states, fingerprint);
|
|
157
|
+
await nc.publish(subjectsMod.varsChangedEvent(this.providerId), payload);
|
|
158
|
+
send(msg);
|
|
159
|
+
done();
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
done(err);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
this.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
168
|
+
this.error(err.message);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
start();
|
|
173
|
+
|
|
174
|
+
this.on('close', async (done) => {
|
|
175
|
+
try {
|
|
176
|
+
if (sub) {
|
|
177
|
+
await sub.drain();
|
|
178
|
+
}
|
|
179
|
+
await connection.release();
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
this.warn(`closing error: ${err.message}`);
|
|
183
|
+
}
|
|
184
|
+
done();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
RED.nodes.registerType('datahub-output', DataHubOutputNode);
|
|
189
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('uos-config', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
host: { value: '192.168.10.100', required: true },
|
|
6
|
+
port: { value: 49360, required: true, validate: RED.validators.number() },
|
|
7
|
+
clientName: { value: 'nodered', required: true },
|
|
8
|
+
tokenEndpoint: { value: '', required: false },
|
|
9
|
+
scope: { value: 'hub.variables.provide hub.variables.readwrite', required: true },
|
|
10
|
+
},
|
|
11
|
+
credentials: {
|
|
12
|
+
clientId: { type: 'text', required: true },
|
|
13
|
+
clientSecret: { type: 'password', required: true },
|
|
14
|
+
},
|
|
15
|
+
label: function () {
|
|
16
|
+
return this.clientName ? `u-OS ${this.clientName}` : 'u-OS NATS';
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="form-row">
|
|
22
|
+
<label for="node-config-input-host"><i class="fa fa-globe"></i> Host</label>
|
|
23
|
+
<input type="text" id="node-config-input-host" placeholder="127.0.0.1">
|
|
24
|
+
</div>
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label for="node-config-input-port"><i class="fa fa-plug"></i> Port</label>
|
|
27
|
+
<input type="number" id="node-config-input-port">
|
|
28
|
+
</div>
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-config-input-clientName"><i class="fa fa-user"></i> Client Name</label>
|
|
31
|
+
<input type="text" id="node-config-input-clientName" placeholder="nodered">
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-config-input-clientId"><i class="fa fa-id-card"></i> Client ID</label>
|
|
35
|
+
<input type="text" id="node-config-input-clientId">
|
|
36
|
+
</div>
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<label for="node-config-input-clientSecret"><i class="fa fa-key"></i> Client Secret</label>
|
|
39
|
+
<input type="password" id="node-config-input-clientSecret">
|
|
40
|
+
</div>
|
|
41
|
+
<div class="form-row">
|
|
42
|
+
<label for="node-config-input-tokenEndpoint"><i class="fa fa-lock"></i> Token URL</label>
|
|
43
|
+
<input type="text" id="node-config-input-tokenEndpoint" placeholder="https://HOST/oauth2/token">
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-row">
|
|
46
|
+
<label for="node-config-input-scope"><i class="fa fa-list"></i> Scope</label>
|
|
47
|
+
<input type="text" id="node-config-input-scope">
|
|
48
|
+
</div>
|