@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
|
@@ -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
|
enabled: {value:true},
|
|
9
10
|
interval: {value:1000},
|
|
10
11
|
deadband: {value:0}
|
|
@@ -13,17 +14,65 @@
|
|
|
13
14
|
outputs:1,
|
|
14
15
|
icon: "white-globe.png",
|
|
15
16
|
label: function() {
|
|
16
|
-
|
|
17
|
+
if (this.name) {
|
|
18
|
+
return this.name;
|
|
19
|
+
}
|
|
20
|
+
// Display tag name or tagId
|
|
21
|
+
return this.tag || this.tagId || "set tag daq settings";
|
|
17
22
|
},
|
|
18
23
|
oneditprepare: function() {
|
|
24
|
+
var node = this;
|
|
25
|
+
var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
|
|
26
|
+
|
|
19
27
|
$.getJSON('/nodered/fuxa/devices', function(data) {
|
|
20
28
|
var datalist = $('#fuxa-tags-set-daq-settings');
|
|
21
29
|
datalist.empty();
|
|
30
|
+
|
|
22
31
|
data.forEach(function(device) {
|
|
23
32
|
device.tags.forEach(function(tag) {
|
|
24
|
-
|
|
33
|
+
var fullName = device.name + ' - ' + tag.name;
|
|
34
|
+
// datalist option value format: tag.name(tag.id)
|
|
35
|
+
var optionValue = tag.name + '(' + tag.id + ')';
|
|
36
|
+
datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
|
|
37
|
+
tagMap[tag.id] = {
|
|
38
|
+
name: tag.name,
|
|
39
|
+
deviceName: device.name,
|
|
40
|
+
fullName: fullName
|
|
41
|
+
};
|
|
25
42
|
});
|
|
26
43
|
});
|
|
44
|
+
|
|
45
|
+
// Listen for tagSelected input changes
|
|
46
|
+
$('#node-input-tagSelected').on('change', function() {
|
|
47
|
+
var selectedValue = $(this).val();
|
|
48
|
+
// Parse format: tag.name(tag.id)
|
|
49
|
+
var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
|
|
50
|
+
if (match) {
|
|
51
|
+
var tagName = match[1];
|
|
52
|
+
var tagId = match[2];
|
|
53
|
+
|
|
54
|
+
// Set the actual stored fields
|
|
55
|
+
$('#node-input-tag').val(tagName);
|
|
56
|
+
$('#node-input-tagId').val(tagId);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Initialize display (when editing existing node)
|
|
61
|
+
if (node.tagId && tagMap[node.tagId]) {
|
|
62
|
+
// Has tagId, construct tagSelected value
|
|
63
|
+
var tagName = node.tag || tagMap[node.tagId].name;
|
|
64
|
+
$('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
|
|
65
|
+
} else if (node.tag && !node.tagId) {
|
|
66
|
+
// Old node: only has tag (tag.name), need to find corresponding tagId
|
|
67
|
+
// Note: if there are duplicate names, only the first match will be found
|
|
68
|
+
for (var tagId in tagMap) {
|
|
69
|
+
if (tagMap[tagId].name === node.tag) {
|
|
70
|
+
$('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
|
|
71
|
+
$('#node-input-tagId').val(tagId);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
27
76
|
});
|
|
28
77
|
}
|
|
29
78
|
});
|
|
@@ -35,10 +84,12 @@
|
|
|
35
84
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
36
85
|
</div>
|
|
37
86
|
<div class="form-row">
|
|
38
|
-
<label for="node-input-
|
|
39
|
-
<input type="text" id="node-input-
|
|
87
|
+
<label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
|
|
88
|
+
<input type="text" id="node-input-tagSelected" list="fuxa-tags-set-daq-settings" placeholder="Select Tag">
|
|
40
89
|
<datalist id="fuxa-tags-set-daq-settings"></datalist>
|
|
41
90
|
</div>
|
|
91
|
+
<input type="hidden" id="node-input-tag">
|
|
92
|
+
<input type="hidden" id="node-input-tagId">
|
|
42
93
|
<div class="form-row">
|
|
43
94
|
<label for="node-input-enabled"> </label>
|
|
44
95
|
<input type="checkbox" id="node-input-enabled" style="display:inline-block; width:15px; vertical-align:baseline;">
|
|
@@ -6,29 +6,33 @@ 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
|
-
|
|
17
|
-
|
|
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);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (tagId) {
|
|
20
|
+
var settings = {
|
|
21
|
+
enabled: config.enabled,
|
|
22
|
+
interval: config.interval,
|
|
23
|
+
deadband: config.deadband
|
|
24
|
+
};
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
// Override with message properties if provided
|
|
27
|
+
if (msg.enabled !== undefined) settings.enabled = msg.enabled;
|
|
28
|
+
if (msg.interval !== undefined) settings.interval = msg.interval;
|
|
29
|
+
if (msg.deadband !== undefined) settings.deadband = msg.deadband;
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} else {
|
|
28
|
-
node.error('Tag not found: ' + tagName, msg);
|
|
29
|
-
}
|
|
31
|
+
var result = await fuxa.setTagDaqSettings(tagId, settings);
|
|
32
|
+
msg.payload = result;
|
|
33
|
+
node.send(msg);
|
|
30
34
|
} else {
|
|
31
|
-
node.error('Tag
|
|
35
|
+
node.error('Tag not found: ' + (config.tag || msg.tag || config.tagId), msg);
|
|
32
36
|
}
|
|
33
37
|
} catch (err) {
|
|
34
38
|
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 || "set 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-set');
|
|
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-set" placeholder="Select Tag">
|
|
37
86
|
<datalist id="fuxa-tags-set"></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="set-tag">
|
|
@@ -7,12 +7,18 @@ module.exports = function(RED) {
|
|
|
7
7
|
|
|
8
8
|
this.on('input', async 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
|
await fuxa.setTag(tagId, msg.payload);
|
|
13
19
|
node.send(msg);
|
|
14
20
|
} else {
|
|
15
|
-
node.error('Tag not found: ' + config.tag, msg);
|
|
21
|
+
node.error('Tag not found: ' + (config.tag || config.tagId), msg);
|
|
16
22
|
}
|
|
17
23
|
} catch (err) {
|
|
18
24
|
node.error(err, msg);
|
package/main.js
CHANGED
|
@@ -148,6 +148,12 @@ try {
|
|
|
148
148
|
if (mysettings.tokenExpiresIn) {
|
|
149
149
|
settings.tokenExpiresIn = mysettings.tokenExpiresIn;
|
|
150
150
|
}
|
|
151
|
+
if (!utils.isNullOrUndefined(mysettings.enableRefreshCookieAuth)) {
|
|
152
|
+
settings.enableRefreshCookieAuth = mysettings.enableRefreshCookieAuth;
|
|
153
|
+
}
|
|
154
|
+
if (mysettings.refreshTokenExpiresIn) {
|
|
155
|
+
settings.refreshTokenExpiresIn = mysettings.refreshTokenExpiresIn;
|
|
156
|
+
}
|
|
151
157
|
if (mysettings.secretCode) {
|
|
152
158
|
settings.secretCode = mysettings.secretCode;
|
|
153
159
|
}
|
|
@@ -175,14 +181,26 @@ try {
|
|
|
175
181
|
if (!utils.isNullOrUndefined(mysettings.nodeRedEnabled)) {
|
|
176
182
|
settings.nodeRedEnabled = mysettings.nodeRedEnabled;
|
|
177
183
|
}
|
|
184
|
+
if (!utils.isNullOrUndefined(mysettings.nodeRedAuthMode)) {
|
|
185
|
+
settings.nodeRedAuthMode = mysettings.nodeRedAuthMode;
|
|
186
|
+
}
|
|
178
187
|
if (!utils.isNullOrUndefined(mysettings.swaggerEnabled)) {
|
|
179
188
|
settings.swaggerEnabled = mysettings.swaggerEnabled;
|
|
180
189
|
}
|
|
190
|
+
if (mysettings.nodeRedEnabled === true && utils.isNullOrUndefined(mysettings.nodeRedAuthMode)) {
|
|
191
|
+
settings.nodeRedAuthMode = 'legacy-open';
|
|
192
|
+
}
|
|
181
193
|
}
|
|
182
194
|
} catch (err) {
|
|
183
195
|
logger.error('Error loading user settings file: ' + userSettingsFile)
|
|
184
196
|
}
|
|
185
197
|
|
|
198
|
+
// Ensure secure mode never runs with an empty/static-known JWT secret.
|
|
199
|
+
if (settings.secureEnabled && !settings.secretCode) {
|
|
200
|
+
settings.secretCode = utils.generateSecretCode();
|
|
201
|
+
logger.warn('Generated a random JWT secret in memory because secureEnabled=true and secretCode was missing. Persist it in settings for stable sessions across restarts.');
|
|
202
|
+
}
|
|
203
|
+
|
|
186
204
|
// Check logger
|
|
187
205
|
if (!settings.logDir) {
|
|
188
206
|
settings.logDir = path.resolve(rootDir, '_logs');
|
|
@@ -254,6 +272,7 @@ var www = path.resolve(__dirname, '../client/dist');
|
|
|
254
272
|
if (!fs.existsSync(www)) { // compatibility with docker/npm/electron
|
|
255
273
|
www = path.resolve(__dirname, './dist');
|
|
256
274
|
}
|
|
275
|
+
|
|
257
276
|
settings.httpStatic = settings.httpStatic || www;
|
|
258
277
|
|
|
259
278
|
if (parsedArgs.port !== undefined) {
|
|
@@ -315,10 +334,13 @@ const allowCrossDomain = function (req, res, next) {
|
|
|
315
334
|
|
|
316
335
|
if (isOriginAllowed(origin)) {
|
|
317
336
|
res.header('Access-Control-Allow-Origin', origin || '*');
|
|
337
|
+
if (settings.enableRefreshCookieAuth) {
|
|
338
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
339
|
+
}
|
|
318
340
|
}
|
|
319
341
|
|
|
320
342
|
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
|
|
321
|
-
res.header('Access-Control-Allow-Headers', 'x-access-token, x-auth-user, Origin, Content-Type, Accept');
|
|
343
|
+
res.header('Access-Control-Allow-Headers', 'x-access-token, x-auth-user, Origin, Content-Type, Accept, Skip-Auth, Skip-Error');
|
|
322
344
|
|
|
323
345
|
|
|
324
346
|
if (req.method === 'OPTIONS') {
|
|
@@ -342,22 +364,24 @@ app.use('/_widgets', express.static(settings.widgetsFileDir));
|
|
|
342
364
|
app.use('/snapshots', express.static(settings.webcamSnapShotsDir))
|
|
343
365
|
|
|
344
366
|
var accessLogStream = fs.createWriteStream(settings.logDir + '/api.log', { flags: 'a' });
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
367
|
+
if (runtime.settings.logApiLevel !== 'none') {
|
|
368
|
+
app.use(morgan('combined', {
|
|
369
|
+
stream: accessLogStream,
|
|
370
|
+
skip: function (req, res) { return res.statusCode < 400 }
|
|
371
|
+
}));
|
|
372
|
+
|
|
373
|
+
app.use(morgan('dev', {
|
|
374
|
+
skip: function (req, res) {
|
|
375
|
+
return res.statusCode < 400
|
|
376
|
+
}, stream: process.stderr
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
app.use(morgan('dev', {
|
|
380
|
+
skip: function (req, res) {
|
|
381
|
+
return res.statusCode >= 400
|
|
382
|
+
}, stream: process.stdout
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
361
385
|
|
|
362
386
|
function mountSwaggerIfEnabled() {
|
|
363
387
|
const swaggerEnabled = settings.swagger || settings.swaggerEnabled;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frangoteam/fuxa-min",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Web-based Process Visualization (SCADA/HMI/Dashboard) software",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"scripts": {
|
|
@@ -36,12 +36,13 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@influxdata/influxdb-client": "1.25.0",
|
|
39
|
+
"@questdb/nodejs-client": "3.0.0",
|
|
39
40
|
"@tdengine/rest": "3.0.2",
|
|
40
41
|
"ads-client": "2.1.0",
|
|
41
42
|
"app-root-path": "2.2.1",
|
|
42
43
|
"async": "3.2.3",
|
|
43
44
|
"async-mutex": "0.5.0",
|
|
44
|
-
"axios": "
|
|
45
|
+
"axios": "1.13.5",
|
|
45
46
|
"bcryptjs": "2.4.3",
|
|
46
47
|
"bluebird": "3.7.2",
|
|
47
48
|
"body-parser": "1.20.3",
|
|
@@ -52,19 +53,20 @@
|
|
|
52
53
|
"fontkit": "^2.0.2",
|
|
53
54
|
"fs-extra": "7.0.1",
|
|
54
55
|
"influx": "5.9.3",
|
|
55
|
-
"ip": "2.0.1",
|
|
56
|
+
"ip": "~2.0.1",
|
|
56
57
|
"jsonwebtoken": "9.0.0",
|
|
57
58
|
"live-plugin-manager": "0.15.1",
|
|
58
59
|
"modbus-serial": "8.0.19",
|
|
59
|
-
"morgan": "1.10.
|
|
60
|
+
"morgan": "1.10.1",
|
|
60
61
|
"mqtt": "4.3.7",
|
|
61
62
|
"node-bacnet": "0.2.4",
|
|
62
63
|
"node-opcua": "2.149.0",
|
|
63
64
|
"node-red": "^4.1.0",
|
|
64
65
|
"node-schedule": "2.1.1",
|
|
65
|
-
"nodemailer": "7.0.
|
|
66
|
+
"nodemailer": "7.0.13",
|
|
66
67
|
"nopt": "5.0.0",
|
|
67
68
|
"pdfmake": "0.2.5",
|
|
69
|
+
"pg": "8.18.0",
|
|
68
70
|
"socket.io": "4.8.1",
|
|
69
71
|
"sqlite3": "5.1.5",
|
|
70
72
|
"swagger-ui-express": "^5.0.1",
|
|
@@ -72,6 +74,9 @@
|
|
|
72
74
|
"ws": "8.18.0",
|
|
73
75
|
"yamljs": "^0.3.0"
|
|
74
76
|
},
|
|
77
|
+
"overrides": {
|
|
78
|
+
"tar": "^7.4.3"
|
|
79
|
+
},
|
|
75
80
|
"devDependencies": {
|
|
76
81
|
"@mapbox/node-pre-gyp": "^1.0.0",
|
|
77
82
|
"@types/express": "5.0.0",
|
|
@@ -26,6 +26,7 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
26
26
|
var overloading = 0; // Overloading counter to mange the break connection
|
|
27
27
|
|
|
28
28
|
var devices = {}; // Devices found { id, maxApdu, segmentation, vendorId }
|
|
29
|
+
var rpmErrorByDevice = {}; // Track RPM error state per device to reduce log noise
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Connect the client to BACnet device
|
|
@@ -141,7 +142,6 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
141
142
|
return new Promise((resolve, reject) => {
|
|
142
143
|
client.readPropertyMultiple(deviceAddress, requestArray, async (err, value) => {
|
|
143
144
|
if (err) {
|
|
144
|
-
logger.error(`'${data.name}' readPropertyMultiple error! ${err}`);
|
|
145
145
|
reject(err);
|
|
146
146
|
}
|
|
147
147
|
resolve(value);
|
|
@@ -160,6 +160,10 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
160
160
|
const deviceAddress = _getDeviceAddress(devices[deviceId]);
|
|
161
161
|
//wrap the client.readPropertyMultiple in a promise so the callback can be awaited
|
|
162
162
|
const value = await this.apiFunctionWrapper(deviceAddress, objectsMapToRead[deviceId]);
|
|
163
|
+
if (rpmErrorByDevice[deviceId]) {
|
|
164
|
+
logger.info(`'${data.name}' readPropertyMultiple recovered`, true);
|
|
165
|
+
rpmErrorByDevice[deviceId] = false;
|
|
166
|
+
}
|
|
163
167
|
|
|
164
168
|
if (!(value && value.values && value.values[0] && value.values[0].values)) {
|
|
165
169
|
logger.error(`'${data.name}' readPropertyMultiple error! unknow`);
|
|
@@ -194,7 +198,10 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
194
198
|
}
|
|
195
199
|
} catch (err) {
|
|
196
200
|
if (err) {
|
|
197
|
-
|
|
201
|
+
if (!rpmErrorByDevice[deviceId]) {
|
|
202
|
+
logger.error(`'${data.name}' readPropertyMultiple error! ${err}`);
|
|
203
|
+
rpmErrorByDevice[deviceId] = true;
|
|
204
|
+
}
|
|
198
205
|
}
|
|
199
206
|
}
|
|
200
207
|
}
|
|
@@ -389,7 +396,6 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
389
396
|
client.whoIs({deviceIPAddress: settings['broadcastAddress']});
|
|
390
397
|
}, 2000);
|
|
391
398
|
}
|
|
392
|
-
|
|
393
399
|
} catch (err) {
|
|
394
400
|
reject(err);
|
|
395
401
|
if (tdelay) {
|
|
@@ -453,50 +459,73 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
453
459
|
*/
|
|
454
460
|
var _readObjectList = function(instance) {
|
|
455
461
|
return new Promise(function (resolve, reject) {
|
|
456
|
-
client.readProperty(_getDeviceAddress(devices[instance]), {type: bacnet.enum.ObjectType.DEVICE, instance: instance}, bacnet.enum.PropertyIdentifier.OBJECT_LIST, (err, value) => {
|
|
462
|
+
client.readProperty(_getDeviceAddress(devices[instance]), {type: bacnet.enum.ObjectType.DEVICE, instance: instance}, bacnet.enum.PropertyIdentifier.OBJECT_LIST, async (err, value) => {
|
|
457
463
|
if (err) {
|
|
458
464
|
logger.error(`'${data.name}' _readObjectList error! ${err}`);
|
|
465
|
+
resolve([]);
|
|
459
466
|
} else if (value && value.values && value.values.length) {
|
|
460
467
|
var objects = [];
|
|
461
|
-
var
|
|
468
|
+
var results = [];
|
|
469
|
+
|
|
470
|
+
// Filtrer les objets à lire
|
|
462
471
|
for (var index in value.values) {
|
|
463
472
|
var object = value.values[index].value;
|
|
464
473
|
object.parent = instance;
|
|
465
474
|
if (_isObjectToShow(object.type)) {
|
|
466
475
|
objects.push(object);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
logger.info(`'${data.name}' reading ${objects.length} objects...`);
|
|
480
|
+
|
|
481
|
+
// Lire les objets par batch de 10 avec délai
|
|
482
|
+
const BATCH_SIZE = 10;
|
|
483
|
+
const DELAY_MS = 100;
|
|
484
|
+
|
|
485
|
+
for (let i = 0; i < objects.length; i += BATCH_SIZE) {
|
|
486
|
+
const batch = objects.slice(i, i + BATCH_SIZE);
|
|
487
|
+
const batchPromises = batch.map(obj => {
|
|
467
488
|
try {
|
|
468
|
-
|
|
489
|
+
return _readProperty(_getDeviceAddress(devices[instance]), { type: obj.type, instance: obj.instance}, bacnet.enum.PropertyIdentifier.OBJECT_NAME);
|
|
469
490
|
} catch (error) {
|
|
470
491
|
logger.error(`'${data.name}' _readObjectList error! ${error}`);
|
|
492
|
+
return Promise.resolve(null);
|
|
471
493
|
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const batchResults = await Promise.all(batchPromises);
|
|
498
|
+
results.push(...batchResults);
|
|
499
|
+
logger.info(`'${data.name}' read ${Math.min(i + BATCH_SIZE, objects.length)}/${objects.length} objects`);
|
|
500
|
+
} catch (batchErr) {
|
|
501
|
+
logger.error(`'${data.name}' batch error: ${batchErr}`);
|
|
472
502
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
if (results[index]) {
|
|
478
|
-
var object = _getObject(objects, results[index].type, results[index].instance);
|
|
479
|
-
if (object) {
|
|
480
|
-
object.id = _formatId(object.type, object.instance);
|
|
481
|
-
object.name = results[index].value;
|
|
482
|
-
object.class = _getObjectClass(object.type);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
503
|
+
|
|
504
|
+
// Délai entre les batches
|
|
505
|
+
if (i + BATCH_SIZE < objects.length) {
|
|
506
|
+
await new Promise(r => setTimeout(r, DELAY_MS));
|
|
486
507
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Associer les noms aux objets
|
|
511
|
+
let resolvedCount = 0;
|
|
512
|
+
for (var idx in results) {
|
|
513
|
+
if (results[idx]) {
|
|
514
|
+
resolvedCount++;
|
|
515
|
+
var object = _getObject(objects, results[idx].type, results[idx].instance);
|
|
516
|
+
if (object) {
|
|
517
|
+
object.id = _formatId(object.type, object.instance);
|
|
518
|
+
object.name = results[idx].value;
|
|
519
|
+
object.class = _getObjectClass(object.type);
|
|
494
520
|
}
|
|
495
|
-
} else {
|
|
496
|
-
logger.error(`'${data.name}' _readObjectList error! ${reason}`);
|
|
497
521
|
}
|
|
498
|
-
|
|
499
|
-
|
|
522
|
+
}
|
|
523
|
+
if (resolvedCount !== objects.length) {
|
|
524
|
+
logger.warn(`'${data.name}' _readObjectList partial: ${resolvedCount}/${objects.length} objects named`);
|
|
525
|
+
}
|
|
526
|
+
resolve(objects);
|
|
527
|
+
} else {
|
|
528
|
+
resolve([]);
|
|
500
529
|
}
|
|
501
530
|
});
|
|
502
531
|
});
|
|
@@ -511,6 +540,7 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
511
540
|
return new Promise(function (resolve, reject) {
|
|
512
541
|
client.readProperty(address, bacobj, property, (err, value) => {
|
|
513
542
|
if (err) {
|
|
543
|
+
logger.error(`'${data.name}' _readProperty error! ${err}`);
|
|
514
544
|
resolve();
|
|
515
545
|
} else if (value && value.values && value.values[0] && value.values[0].value) {
|
|
516
546
|
resolve({ type: bacobj.type, instance: bacobj.instance, value: value.values[0].value });
|
|
@@ -726,7 +756,11 @@ function BACNETclient(_data, _logger, _events, _runtime) {
|
|
|
726
756
|
}
|
|
727
757
|
|
|
728
758
|
var _getDeviceAddress = function (device) {
|
|
729
|
-
|
|
759
|
+
if (!device) {
|
|
760
|
+
logger.error(`'${data.name}' _getDeviceAddress error! device is undefined`);
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
return device.address || (device.sender ? device.sender.address : null);
|
|
730
764
|
}
|
|
731
765
|
}
|
|
732
766
|
|
|
@@ -740,4 +774,4 @@ module.exports = {
|
|
|
740
774
|
if (!bacnet) return null;
|
|
741
775
|
return new BACNETclient(data, logger, events, runtime);
|
|
742
776
|
}
|
|
743
|
-
}
|
|
777
|
+
}
|