@frangoteam/fuxa-min 1.2.10 → 1.3.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/api/auth/index.js +141 -3
- package/api/command/index.js +10 -4
- package/api/diagnose/index.js +12 -4
- package/api/index.js +41 -8
- package/api/jwt-helper.js +15 -2
- package/api/path-helper.js +41 -0
- package/api/projects/index.js +27 -14
- package/api/reports/reports.service.ts +12 -2
- package/api/resources/index.js +30 -9
- package/api/scheduler/index.js +21 -1
- package/dist/3rdpartylicenses.txt +139 -7
- package/dist/assets/i18n/de.json +10 -0
- package/dist/assets/i18n/en.json +17 -3
- package/dist/assets/i18n/es.json +12 -0
- package/dist/assets/i18n/fr.json +10 -0
- package/dist/assets/i18n/ja.json +15 -6
- package/dist/assets/i18n/ko.json +12 -0
- package/dist/assets/i18n/pt.json +9 -2
- package/dist/assets/i18n/ru.json +11 -0
- package/dist/assets/i18n/sv.json +10 -1
- package/dist/assets/i18n/tr.json +8 -1
- package/dist/assets/i18n/ua.json +9 -2
- package/dist/assets/i18n/zh-cn.json +10 -0
- package/dist/assets/i18n/zh-tw.json +11 -1
- package/dist/index.html +2 -2
- package/dist/main.bafae830903d548e.js +329 -0
- package/dist/polyfills.d7de05f9af2fb559.js +1 -0
- package/dist/reports.service.js +11 -1
- package/dist/{runtime.8ef63094e52a66ba.js → runtime.9136a61a9b98f987.js} +1 -1
- package/dist/{scripts.40b60f02658462e4.js → scripts.d9e6ee984bf6f3b7.js} +1 -1
- package/dist/styles.545e37beb3e671ba.css +1 -0
- package/integrations/node-red/index.js +91 -24
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.js +8 -2
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.js +12 -12
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.js +14 -10
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.js +8 -2
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.js +24 -20
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.html +56 -5
- package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.js +8 -2
- package/main.js +41 -17
- package/package.json +10 -5
- package/runtime/devices/adsclient/index.js +1 -1
- package/runtime/devices/bacnet/index.js +66 -32
- package/runtime/devices/ethernetip/index.js +1 -1
- package/runtime/devices/gpio/index.js +1 -1
- package/runtime/devices/odbc/index.js +5 -5
- package/runtime/devices/template/index.js +14 -14
- package/runtime/storage/daqstorage.js +28 -2
- package/runtime/storage/influxdb/index.js +1 -1
- package/runtime/storage/questdb/index.js +224 -0
- package/runtime/utils.js +5 -0
- package/settings.default.js +13 -3
- package/dist/main.020ca34630a3363a.js +0 -329
- package/dist/polyfills.c8e7db9850a3ad8b.js +0 -1
- package/dist/styles.03cc550382689976.css +0 -1
|
@@ -34,8 +34,8 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
|
|
|
34
34
|
// Minimal Node-RED settings; extend only what is really needed
|
|
35
35
|
const redSettings = {
|
|
36
36
|
httpAdminRoot: '/nodered/',
|
|
37
|
-
//
|
|
38
|
-
httpNodeRoot: '/',
|
|
37
|
+
// Serve Node-RED HTTP nodes under /dashboard to avoid intercepting FUXA routes
|
|
38
|
+
httpNodeRoot: '/dashboard',
|
|
39
39
|
userDir,
|
|
40
40
|
nodesDir: [path.join(__dirname, 'node-red-contrib-fuxa')],
|
|
41
41
|
flowFile: 'flows.json',
|
|
@@ -44,7 +44,7 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
|
|
|
44
44
|
tours: { enabled: false },
|
|
45
45
|
},
|
|
46
46
|
// Dashboard will be exposed at /dashboard/...
|
|
47
|
-
ui: { path: '/
|
|
47
|
+
ui: { path: '/' },
|
|
48
48
|
// Values used by FlowFuse dashboard's ui_base.js for layout saves
|
|
49
49
|
// These mirror the FUXA HTTP bind address so Node-RED can call its own /nodered/flows API
|
|
50
50
|
uiHost: settings.uiHost,
|
|
@@ -99,10 +99,8 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
|
|
|
99
99
|
path: require('path'),
|
|
100
100
|
util: require('util'),
|
|
101
101
|
os: require('os'),
|
|
102
|
-
child_process: require('child_process'),
|
|
103
102
|
http: require('http'),
|
|
104
103
|
https: require('https'),
|
|
105
|
-
net: require('net'),
|
|
106
104
|
dgram: require('dgram'),
|
|
107
105
|
dns: require('dns'),
|
|
108
106
|
url: require('url'),
|
|
@@ -114,40 +112,109 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
|
|
|
114
112
|
buffer: require('buffer'),
|
|
115
113
|
sqlite3: require('sqlite3'),
|
|
116
114
|
serialport: require('serialport'),
|
|
115
|
+
// Dangerous modules are opt-in only (see settings.nodeRedUnsafeModules)
|
|
116
|
+
...(settings.nodeRedUnsafeModules ? {
|
|
117
|
+
child_process: require('child_process'),
|
|
118
|
+
net: require('net'),
|
|
119
|
+
} : {}),
|
|
117
120
|
},
|
|
118
121
|
};
|
|
119
122
|
|
|
120
123
|
// Initialize Node-RED on the existing HTTP server (must be done before server.listen)
|
|
121
124
|
RED.init(server, redSettings);
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
const getCookieValue = (req, name) => {
|
|
127
|
+
const cookieHeader = req.headers.cookie;
|
|
128
|
+
if (!cookieHeader) return null;
|
|
129
|
+
const cookies = cookieHeader.split(';');
|
|
130
|
+
for (const cookie of cookies) {
|
|
131
|
+
const [key, ...rest] = cookie.trim().split('=');
|
|
132
|
+
if (key === name) {
|
|
133
|
+
return rest.join('=');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const verifyApiKey = (runtimeRef, apiKey) => {
|
|
140
|
+
return runtimeRef.apiKeys.getApiKeys().then(stored => {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
return stored.find(k => {
|
|
143
|
+
if (!k || k.key !== apiKey || k.enabled === false) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!k.expires) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
const expiresAt = new Date(k.expires).getTime();
|
|
150
|
+
return !isNaN(expiresAt) && expiresAt > now;
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Allow public dashboard UI and socket.io; require JWT or API key for admin/editor/flows when security is enabled
|
|
124
156
|
const allowDashboard = (req, res, next) => {
|
|
125
|
-
|
|
157
|
+
const url = req.originalUrl || req.url || req.path;
|
|
126
158
|
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
// Public dashboard UI and its HTTP APIs (served from httpNodeRoot/ui.path)
|
|
160
|
+
if (url.includes('/dashboard') || url.includes('/socket.io')) return next();
|
|
129
161
|
|
|
130
|
-
|
|
131
|
-
|
|
162
|
+
if (!settings.secureEnabled || settings.nodeRedAuthMode === 'legacy-open') {
|
|
163
|
+
return next();
|
|
164
|
+
}
|
|
132
165
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
166
|
+
const apiKey = req.headers['x-api-key'];
|
|
167
|
+
if (apiKey) {
|
|
168
|
+
return verifyApiKey(runtime, apiKey)
|
|
169
|
+
.then(validKey => {
|
|
170
|
+
if (!validKey) {
|
|
171
|
+
return res.status(401).json({ error: "unauthorized_error", message: "Invalid API Key" });
|
|
172
|
+
}
|
|
173
|
+
return next();
|
|
174
|
+
})
|
|
175
|
+
.catch(err => {
|
|
176
|
+
logger.error(`api-key validation failed: ${err}`);
|
|
177
|
+
return res.status(500).json({ error: "unexpected_error", message: "ApiKey validation failed" });
|
|
178
|
+
});
|
|
179
|
+
}
|
|
137
180
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
181
|
+
const headerToken = req.headers['x-access-token'];
|
|
182
|
+
const queryToken = req.query?.token;
|
|
183
|
+
const cookieToken = getCookieValue(req, 'nodered_auth');
|
|
184
|
+
// Prefer explicit tokens over cookie to avoid stale cookie blocking valid logins
|
|
185
|
+
const token = headerToken || queryToken || cookieToken;
|
|
186
|
+
if (!token) {
|
|
187
|
+
return res.status(401).json({ error: "unauthorized_error", message: "Authentication required!" });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return authJwt.verify(token)
|
|
191
|
+
.then(() => {
|
|
192
|
+
if (queryToken) {
|
|
193
|
+
res.cookie('nodered_auth', token, {
|
|
194
|
+
httpOnly: true,
|
|
195
|
+
sameSite: 'lax',
|
|
196
|
+
secure: !!settings.https,
|
|
197
|
+
});
|
|
198
|
+
if (req.method === 'GET') {
|
|
199
|
+
const cleanUrl = new URL(req.originalUrl, `http://${req.headers.host}`);
|
|
200
|
+
cleanUrl.searchParams.delete('token');
|
|
201
|
+
return res.redirect(cleanUrl.pathname + cleanUrl.search);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return next();
|
|
205
|
+
})
|
|
206
|
+
.catch(() => {
|
|
207
|
+
if (cookieToken) {
|
|
208
|
+
res.clearCookie('nodered_auth');
|
|
209
|
+
}
|
|
210
|
+
return res.status(401).json({ error: "unauthorized_error", message: "Invalid token!" });
|
|
211
|
+
});
|
|
145
212
|
};
|
|
146
213
|
|
|
147
214
|
// Mount Node-RED admin/editor under /nodered; HTTP nodes (including dashboard)
|
|
148
|
-
// are served from httpNodeRoot ('/') so they appear at /dashboard/... etc.
|
|
215
|
+
// are served from httpNodeRoot ('/dashboard') so they appear at /dashboard/... etc.
|
|
149
216
|
app.use('/nodered', allowDashboard, RED.httpAdmin);
|
|
150
|
-
app.use('/', allowDashboard, RED.httpNode);
|
|
217
|
+
app.use('/dashboard', allowDashboard, RED.httpNode);
|
|
151
218
|
|
|
152
219
|
await RED.start();
|
|
153
220
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
color: '#a6bbcf',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value:""},
|
|
7
|
-
tag: {value:"",
|
|
7
|
+
tag: {value:""}, // Keep: tag name for display and backward compatibility
|
|
8
|
+
tagId: {value:""}, // New: unique identifier (optional, for backward compatibility)
|
|
8
9
|
from: {value:""},
|
|
9
10
|
to: {value:""}
|
|
10
11
|
},
|
|
@@ -12,17 +13,65 @@
|
|
|
12
13
|
outputs:1,
|
|
13
14
|
icon: "white-globe.png",
|
|
14
15
|
label: function() {
|
|
15
|
-
|
|
16
|
+
if (this.name) {
|
|
17
|
+
return this.name;
|
|
18
|
+
}
|
|
19
|
+
// Display tag name or tagId
|
|
20
|
+
return this.tag || this.tagId || "get daq";
|
|
16
21
|
},
|
|
17
22
|
oneditprepare: function() {
|
|
23
|
+
var node = this;
|
|
24
|
+
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
|
|
25
|
+
|
|
18
26
|
$.getJSON('/nodered/fuxa/devices', function(data) {
|
|
19
27
|
var datalist = $('#fuxa-tags-daq');
|
|
20
28
|
datalist.empty();
|
|
29
|
+
|
|
21
30
|
data.forEach(function(device) {
|
|
22
31
|
device.tags.forEach(function(tag) {
|
|
23
|
-
|
|
32
|
+
var fullName = device.name + ' - ' + tag.name;
|
|
33
|
+
// datalist option value format: tag.name(tag.id)
|
|
34
|
+
var optionValue = tag.name + '(' + tag.id + ')';
|
|
35
|
+
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
|
|
36
|
+
tagMap[tag.id] = {
|
|
37
|
+
name: tag.name,
|
|
38
|
+
deviceName: device.name,
|
|
39
|
+
fullName: fullName
|
|
40
|
+
};
|
|
24
41
|
});
|
|
25
42
|
});
|
|
43
|
+
|
|
44
|
+
// Listen for tagSelected input changes
|
|
45
|
+
$('#node-input-tagSelected').on('change', function() {
|
|
46
|
+
var selectedValue = $(this).val();
|
|
47
|
+
// Parse format: tag.name(tag.id)
|
|
48
|
+
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
|
|
49
|
+
if (match) {
|
|
50
|
+
var tagName = match[1];
|
|
51
|
+
var tagId = match[2];
|
|
52
|
+
|
|
53
|
+
// Set the actual stored fields
|
|
54
|
+
$('#node-input-tag').val(tagName);
|
|
55
|
+
$('#node-input-tagId').val(tagId);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Initialize display (when editing existing node)
|
|
60
|
+
if (node.tagId && tagMap[node.tagId]) {
|
|
61
|
+
// Has tagId, construct tagSelected value
|
|
62
|
+
var tagName = node.tag || tagMap[node.tagId].name;
|
|
63
|
+
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
|
|
64
|
+
} else if (node.tag && !node.tagId) {
|
|
65
|
+
// Old node: only has tag (tag.name), need to find corresponding tagId
|
|
66
|
+
// Note: if there are duplicate names, only the first match will be found
|
|
67
|
+
for (var tagId in tagMap) {
|
|
68
|
+
if (tagMap[tagId].name === node.tag) {
|
|
69
|
+
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
|
|
70
|
+
$('#node-input-tagId').val(tagId);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
26
75
|
});
|
|
27
76
|
|
|
28
77
|
// Convert timestamp format for datetime-local inputs
|
|
@@ -64,10 +113,12 @@
|
|
|
64
113
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
65
114
|
</div>
|
|
66
115
|
<div class="form-row">
|
|
67
|
-
<label for="node-input-
|
|
68
|
-
<input type="text" id="node-input-
|
|
116
|
+
<label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
|
|
117
|
+
<input type="text" id="node-input-tagSelected" list="fuxa-tags-daq" placeholder="Select Tag">
|
|
69
118
|
<datalist id="fuxa-tags-daq"></datalist>
|
|
70
119
|
</div>
|
|
120
|
+
<input type="hidden" id="node-input-tag">
|
|
121
|
+
<input type="hidden" id="node-input-tagId">
|
|
71
122
|
<div class="form-row">
|
|
72
123
|
<label for="node-input-from"><i class="fa fa-clock-o"></i> From (timestamp)</label>
|
|
73
124
|
<input type="datetime-local" id="node-input-from" placeholder="From timestamp (optional)">
|
|
@@ -6,7 +6,13 @@ module.exports = function(RED) {
|
|
|
6
6
|
|
|
7
7
|
this.on('input', async function(msg) {
|
|
8
8
|
try {
|
|
9
|
-
|
|
9
|
+
// Prefer config.tagId, fallback to config.tag for backward compatibility
|
|
10
|
+
var tagId = config.tagId;
|
|
11
|
+
if (!tagId && config.tag) {
|
|
12
|
+
// Backward compatibility: old nodes use tag.name, need to convert to tagId
|
|
13
|
+
tagId = fuxa.getTagId(config.tag, null);
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
if (tagId) {
|
|
11
17
|
var fromts = config.from || msg.from || Date.now() - 3600000; // default 1 hour ago
|
|
12
18
|
var tots = config.to || msg.to || Date.now();
|
|
@@ -14,7 +20,7 @@ module.exports = function(RED) {
|
|
|
14
20
|
msg.payload = data;
|
|
15
21
|
node.send(msg);
|
|
16
22
|
} else {
|
|
17
|
-
node.error('Tag not found: ' + config.tag, msg);
|
|
23
|
+
node.error('Tag not found: ' + (config.tag || config.tagId), msg);
|
|
18
24
|
}
|
|
19
25
|
} catch (err) {
|
|
20
26
|
node.error(err, msg);
|
|
@@ -4,23 +4,72 @@
|
|
|
4
4
|
color: '#a6bbcf',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value:""},
|
|
7
|
-
tag: {value:"",
|
|
7
|
+
tag: {value:""}, // Keep: tag name for display and backward compatibility
|
|
8
|
+
tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
|
|
8
9
|
},
|
|
9
10
|
inputs:0,
|
|
10
11
|
outputs:1,
|
|
11
12
|
icon: "white-globe.png",
|
|
12
13
|
label: function() {
|
|
13
|
-
|
|
14
|
+
if (this.name) {
|
|
15
|
+
return this.name;
|
|
16
|
+
}
|
|
17
|
+
// Display tag name or tagId
|
|
18
|
+
return this.tag || this.tagId || "get tag change";
|
|
14
19
|
},
|
|
15
20
|
oneditprepare: function() {
|
|
21
|
+
var node = this;
|
|
22
|
+
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
|
|
23
|
+
|
|
16
24
|
$.getJSON('/nodered/fuxa/devices', function(data) {
|
|
17
25
|
var datalist = $('#fuxa-change-tags');
|
|
18
26
|
datalist.empty();
|
|
27
|
+
|
|
19
28
|
data.forEach(function(device) {
|
|
20
29
|
device.tags.forEach(function(tag) {
|
|
21
|
-
|
|
30
|
+
var fullName = device.name + ' - ' + tag.name;
|
|
31
|
+
// datalist option value format: tag.name(tag.id)
|
|
32
|
+
var optionValue = tag.name + '(' + tag.id + ')';
|
|
33
|
+
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
|
|
34
|
+
tagMap[tag.id] = {
|
|
35
|
+
name: tag.name,
|
|
36
|
+
deviceName: device.name,
|
|
37
|
+
fullName: fullName
|
|
38
|
+
};
|
|
22
39
|
});
|
|
23
40
|
});
|
|
41
|
+
|
|
42
|
+
// Listen for tagSelected input changes
|
|
43
|
+
$('#node-input-tagSelected').on('change', function() {
|
|
44
|
+
var selectedValue = $(this).val();
|
|
45
|
+
// Parse format: tag.name(tag.id)
|
|
46
|
+
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
|
|
47
|
+
if (match) {
|
|
48
|
+
var tagName = match[1];
|
|
49
|
+
var tagId = match[2];
|
|
50
|
+
|
|
51
|
+
// Set the actual stored fields
|
|
52
|
+
$('#node-input-tag').val(tagName);
|
|
53
|
+
$('#node-input-tagId').val(tagId);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Initialize display (when editing existing node)
|
|
58
|
+
if (node.tagId && tagMap[node.tagId]) {
|
|
59
|
+
// Has tagId, construct tagSelected value
|
|
60
|
+
var tagName = node.tag || tagMap[node.tagId].name;
|
|
61
|
+
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
|
|
62
|
+
} else if (node.tag && !node.tagId) {
|
|
63
|
+
// Old node: only has tag (tag.name), need to find corresponding tagId
|
|
64
|
+
// Note: if there are duplicate names, only the first match will be found
|
|
65
|
+
for (var tagId in tagMap) {
|
|
66
|
+
if (tagMap[tagId].name === node.tag) {
|
|
67
|
+
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
|
|
68
|
+
$('#node-input-tagId').val(tagId);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
24
73
|
});
|
|
25
74
|
}
|
|
26
75
|
});
|
|
@@ -32,10 +81,12 @@
|
|
|
32
81
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
33
82
|
</div>
|
|
34
83
|
<div class="form-row">
|
|
35
|
-
<label for="node-input-
|
|
36
|
-
<input type="text" id="node-input-
|
|
84
|
+
<label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
|
|
85
|
+
<input type="text" id="node-input-tagSelected" list="fuxa-change-tags" placeholder="Select Tag">
|
|
37
86
|
<datalist id="fuxa-change-tags"></datalist>
|
|
38
87
|
</div>
|
|
88
|
+
<input type="hidden" id="node-input-tag">
|
|
89
|
+
<input type="hidden" id="node-input-tagId">
|
|
39
90
|
</script>
|
|
40
91
|
|
|
41
92
|
<script type="text/x-red" data-help-name="get-tag-change">
|
|
@@ -7,26 +7,26 @@ module.exports = function(RED) {
|
|
|
7
7
|
// Store previous values to detect actual changes
|
|
8
8
|
var previousValues = {};
|
|
9
9
|
|
|
10
|
+
// Prefer config.tagId, fallback to config.tag for backward compatibility
|
|
11
|
+
var configuredTagId = config.tagId;
|
|
12
|
+
if (!configuredTagId && config.tag) {
|
|
13
|
+
// Backward compatibility: old nodes use tag.name, need to convert to tagId
|
|
14
|
+
configuredTagId = fuxa.getTagId(config.tag, null);
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
// Initialize previous value for the configured tag
|
|
11
|
-
if (
|
|
12
|
-
var
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
var initialValue = fuxa.getTag(tagId);
|
|
16
|
-
if (initialValue !== undefined) {
|
|
17
|
-
previousValues[tagId] = initialValue;
|
|
18
|
-
}
|
|
18
|
+
if (configuredTagId) {
|
|
19
|
+
var initialValue = fuxa.getTag(configuredTagId);
|
|
20
|
+
if (initialValue !== undefined) {
|
|
21
|
+
previousValues[configuredTagId] = initialValue;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
// Event listener for device value changes (raw events from devices)
|
|
23
26
|
var deviceEventListener = function(deviceEvent) {
|
|
24
27
|
try {
|
|
25
28
|
// deviceEvent format: { id: deviceId, values: { tagId: tagObject, ... } }
|
|
26
29
|
if (deviceEvent && deviceEvent.values) {
|
|
27
|
-
// Check if any of the changed values match our configured tag
|
|
28
|
-
var configuredTagId = fuxa.getTagId(config.tag, null);
|
|
29
|
-
|
|
30
30
|
if (configuredTagId && deviceEvent.values[configuredTagId]) {
|
|
31
31
|
var tagData = deviceEvent.values[configuredTagId];
|
|
32
32
|
|
|
@@ -4,23 +4,72 @@
|
|
|
4
4
|
color: '#a6bbcf',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value:""},
|
|
7
|
-
tag: {value:"",
|
|
7
|
+
tag: {value:""}, // Keep: tag name for display and backward compatibility
|
|
8
|
+
tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
|
|
8
9
|
},
|
|
9
10
|
inputs:1,
|
|
10
11
|
outputs:1,
|
|
11
12
|
icon: "white-globe.png",
|
|
12
13
|
label: function() {
|
|
13
|
-
|
|
14
|
+
if (this.name) {
|
|
15
|
+
return this.name;
|
|
16
|
+
}
|
|
17
|
+
// Display tag name or tagId
|
|
18
|
+
return this.tag || this.tagId || "get tag daq settings";
|
|
14
19
|
},
|
|
15
20
|
oneditprepare: function() {
|
|
21
|
+
var node = this;
|
|
22
|
+
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
|
|
23
|
+
|
|
16
24
|
$.getJSON('/nodered/fuxa/devices', function(data) {
|
|
17
25
|
var datalist = $('#fuxa-tags-daq-settings');
|
|
18
26
|
datalist.empty();
|
|
27
|
+
|
|
19
28
|
data.forEach(function(device) {
|
|
20
29
|
device.tags.forEach(function(tag) {
|
|
21
|
-
|
|
30
|
+
var fullName = device.name + ' - ' + tag.name;
|
|
31
|
+
// datalist option value format: tag.name(tag.id)
|
|
32
|
+
var optionValue = tag.name + '(' + tag.id + ')';
|
|
33
|
+
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
|
|
34
|
+
tagMap[tag.id] = {
|
|
35
|
+
name: tag.name,
|
|
36
|
+
deviceName: device.name,
|
|
37
|
+
fullName: fullName
|
|
38
|
+
};
|
|
22
39
|
});
|
|
23
40
|
});
|
|
41
|
+
|
|
42
|
+
// Listen for tagSelected input changes
|
|
43
|
+
$('#node-input-tagSelected').on('change', function() {
|
|
44
|
+
var selectedValue = $(this).val();
|
|
45
|
+
// Parse format: tag.name(tag.id)
|
|
46
|
+
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
|
|
47
|
+
if (match) {
|
|
48
|
+
var tagName = match[1];
|
|
49
|
+
var tagId = match[2];
|
|
50
|
+
|
|
51
|
+
// Set the actual stored fields
|
|
52
|
+
$('#node-input-tag').val(tagName);
|
|
53
|
+
$('#node-input-tagId').val(tagId);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Initialize display (when editing existing node)
|
|
58
|
+
if (node.tagId && tagMap[node.tagId]) {
|
|
59
|
+
// Has tagId, construct tagSelected value
|
|
60
|
+
var tagName = node.tag || tagMap[node.tagId].name;
|
|
61
|
+
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
|
|
62
|
+
} else if (node.tag && !node.tagId) {
|
|
63
|
+
// Old node: only has tag (tag.name), need to find corresponding tagId
|
|
64
|
+
// Note: if there are duplicate names, only the first match will be found
|
|
65
|
+
for (var tagId in tagMap) {
|
|
66
|
+
if (tagMap[tagId].name === node.tag) {
|
|
67
|
+
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
|
|
68
|
+
$('#node-input-tagId').val(tagId);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
24
73
|
});
|
|
25
74
|
}
|
|
26
75
|
});
|
|
@@ -32,10 +81,12 @@
|
|
|
32
81
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
33
82
|
</div>
|
|
34
83
|
<div class="form-row">
|
|
35
|
-
<label for="node-input-
|
|
36
|
-
<input type="text" id="node-input-
|
|
84
|
+
<label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
|
|
85
|
+
<input type="text" id="node-input-tagSelected" list="fuxa-tags-daq-settings" placeholder="Select Tag">
|
|
37
86
|
<datalist id="fuxa-tags-daq-settings"></datalist>
|
|
38
87
|
</div>
|
|
88
|
+
<input type="hidden" id="node-input-tag">
|
|
89
|
+
<input type="hidden" id="node-input-tagId">
|
|
39
90
|
</script>
|
|
40
91
|
|
|
41
92
|
<script type="text/x-red" data-help-name="get-tag-daq-settings">
|
|
@@ -6,18 +6,22 @@ module.exports = function(RED) {
|
|
|
6
6
|
|
|
7
7
|
this.on('input', async function(msg) {
|
|
8
8
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} else {
|
|
17
|
-
node.error('Tag not found: ' + tagName, msg);
|
|
9
|
+
// Prefer config.tagId, fallback to config.tag or msg.tag for backward compatibility
|
|
10
|
+
var tagId = config.tagId;
|
|
11
|
+
if (!tagId) {
|
|
12
|
+
var tagName = config.tag || msg.tag;
|
|
13
|
+
if (tagName) {
|
|
14
|
+
// Backward compatibility: old nodes use tag.name, need to convert to tagId
|
|
15
|
+
tagId = fuxa.getTagId(tagName, null);
|
|
18
16
|
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (tagId) {
|
|
20
|
+
var settings = await fuxa.getTagDaqSettings(tagId);
|
|
21
|
+
msg.payload = settings;
|
|
22
|
+
node.send(msg);
|
|
19
23
|
} else {
|
|
20
|
-
node.error('Tag
|
|
24
|
+
node.error('Tag not found: ' + (config.tag || msg.tag || config.tagId), msg);
|
|
21
25
|
}
|
|
22
26
|
} catch (err) {
|
|
23
27
|
node.error(err, msg);
|
|
@@ -4,23 +4,72 @@
|
|
|
4
4
|
color: '#a6bbcf',
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value:""},
|
|
7
|
-
tag: {value:"",
|
|
7
|
+
tag: {value:""}, // Keep: tag name for display and backward compatibility
|
|
8
|
+
tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
|
|
8
9
|
},
|
|
9
10
|
inputs:1,
|
|
10
11
|
outputs:1,
|
|
11
12
|
icon: "white-globe.png",
|
|
12
13
|
label: function() {
|
|
13
|
-
|
|
14
|
+
if (this.name) {
|
|
15
|
+
return this.name;
|
|
16
|
+
}
|
|
17
|
+
// Display tag name or tagId
|
|
18
|
+
return this.tag || this.tagId || "get tag";
|
|
14
19
|
},
|
|
15
20
|
oneditprepare: function() {
|
|
21
|
+
var node = this;
|
|
22
|
+
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
|
|
23
|
+
|
|
16
24
|
$.getJSON('/nodered/fuxa/devices', function(data) {
|
|
17
25
|
var datalist = $('#fuxa-tags');
|
|
18
26
|
datalist.empty();
|
|
27
|
+
|
|
19
28
|
data.forEach(function(device) {
|
|
20
29
|
device.tags.forEach(function(tag) {
|
|
21
|
-
|
|
30
|
+
var fullName = device.name + ' - ' + tag.name;
|
|
31
|
+
// datalist option value format: tag.name(tag.id)
|
|
32
|
+
var optionValue = tag.name + '(' + tag.id + ')';
|
|
33
|
+
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
|
|
34
|
+
tagMap[tag.id] = {
|
|
35
|
+
name: tag.name,
|
|
36
|
+
deviceName: device.name,
|
|
37
|
+
fullName: fullName
|
|
38
|
+
};
|
|
22
39
|
});
|
|
23
40
|
});
|
|
41
|
+
|
|
42
|
+
// Listen for tagSelected input changes
|
|
43
|
+
$('#node-input-tagSelected').on('change', function() {
|
|
44
|
+
var selectedValue = $(this).val();
|
|
45
|
+
// Parse format: tag.name(tag.id)
|
|
46
|
+
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
|
|
47
|
+
if (match) {
|
|
48
|
+
var tagName = match[1];
|
|
49
|
+
var tagId = match[2];
|
|
50
|
+
|
|
51
|
+
// Set the actual stored fields
|
|
52
|
+
$('#node-input-tag').val(tagName);
|
|
53
|
+
$('#node-input-tagId').val(tagId);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Initialize display (when editing existing node)
|
|
58
|
+
if (node.tagId && tagMap[node.tagId]) {
|
|
59
|
+
// Has tagId, construct tagSelected value
|
|
60
|
+
var tagName = node.tag || tagMap[node.tagId].name;
|
|
61
|
+
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
|
|
62
|
+
} else if (node.tag && !node.tagId) {
|
|
63
|
+
// Old node: only has tag (tag.name), need to find corresponding tagId
|
|
64
|
+
// Note: if there are duplicate names, only the first match will be found
|
|
65
|
+
for (var tagId in tagMap) {
|
|
66
|
+
if (tagMap[tagId].name === node.tag) {
|
|
67
|
+
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
|
|
68
|
+
$('#node-input-tagId').val(tagId);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
24
73
|
});
|
|
25
74
|
}
|
|
26
75
|
});
|
|
@@ -32,10 +81,12 @@
|
|
|
32
81
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
33
82
|
</div>
|
|
34
83
|
<div class="form-row">
|
|
35
|
-
<label for="node-input-
|
|
36
|
-
<input type="text" id="node-input-
|
|
84
|
+
<label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
|
|
85
|
+
<input type="text" id="node-input-tagSelected" list="fuxa-tags" placeholder="Select Tag">
|
|
37
86
|
<datalist id="fuxa-tags"></datalist>
|
|
38
87
|
</div>
|
|
88
|
+
<input type="hidden" id="node-input-tag">
|
|
89
|
+
<input type="hidden" id="node-input-tagId">
|
|
39
90
|
</script>
|
|
40
91
|
|
|
41
92
|
<script type="text/x-red" data-help-name="get-tag">
|
|
@@ -7,14 +7,20 @@ module.exports = function(RED) {
|
|
|
7
7
|
|
|
8
8
|
this.on('input', function(msg) {
|
|
9
9
|
try {
|
|
10
|
-
|
|
10
|
+
// Prefer config.tagId, fallback to config.tag for backward compatibility
|
|
11
|
+
var tagId = config.tagId;
|
|
12
|
+
if (!tagId && config.tag) {
|
|
13
|
+
// Backward compatibility: old nodes use tag.name, need to convert to tagId
|
|
14
|
+
tagId = fuxa.getTagId(config.tag, null);
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
if (tagId) {
|
|
12
18
|
var value = fuxa.getTag(tagId);
|
|
13
19
|
msg.payload = value;
|
|
14
20
|
msg.topic = config.tag; // Set topic to tag name for join operations
|
|
15
21
|
node.send(msg);
|
|
16
22
|
} else {
|
|
17
|
-
node.error('Tag not found: ' + config.tag, msg);
|
|
23
|
+
node.error('Tag not found: ' + (config.tag || config.tagId), msg);
|
|
18
24
|
}
|
|
19
25
|
} catch (err) {
|
|
20
26
|
node.error(err, msg);
|