@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.
Files changed (60) hide show
  1. package/api/auth/index.js +141 -3
  2. package/api/command/index.js +10 -4
  3. package/api/diagnose/index.js +12 -4
  4. package/api/index.js +41 -8
  5. package/api/jwt-helper.js +15 -2
  6. package/api/path-helper.js +41 -0
  7. package/api/projects/index.js +27 -14
  8. package/api/reports/reports.service.ts +12 -2
  9. package/api/resources/index.js +30 -9
  10. package/api/scheduler/index.js +21 -1
  11. package/dist/3rdpartylicenses.txt +139 -7
  12. package/dist/assets/i18n/de.json +10 -0
  13. package/dist/assets/i18n/en.json +17 -3
  14. package/dist/assets/i18n/es.json +12 -0
  15. package/dist/assets/i18n/fr.json +10 -0
  16. package/dist/assets/i18n/ja.json +15 -6
  17. package/dist/assets/i18n/ko.json +12 -0
  18. package/dist/assets/i18n/pt.json +9 -2
  19. package/dist/assets/i18n/ru.json +11 -0
  20. package/dist/assets/i18n/sv.json +10 -1
  21. package/dist/assets/i18n/tr.json +8 -1
  22. package/dist/assets/i18n/ua.json +9 -2
  23. package/dist/assets/i18n/zh-cn.json +10 -0
  24. package/dist/assets/i18n/zh-tw.json +11 -1
  25. package/dist/index.html +2 -2
  26. package/dist/main.bafae830903d548e.js +329 -0
  27. package/dist/polyfills.d7de05f9af2fb559.js +1 -0
  28. package/dist/reports.service.js +11 -1
  29. package/dist/{runtime.8ef63094e52a66ba.js → runtime.9136a61a9b98f987.js} +1 -1
  30. package/dist/{scripts.40b60f02658462e4.js → scripts.d9e6ee984bf6f3b7.js} +1 -1
  31. package/dist/styles.545e37beb3e671ba.css +1 -0
  32. package/integrations/node-red/index.js +91 -24
  33. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.html +56 -5
  34. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.js +8 -2
  35. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.html +56 -5
  36. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.js +12 -12
  37. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.html +56 -5
  38. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.js +14 -10
  39. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.html +56 -5
  40. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.js +8 -2
  41. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.html +56 -5
  42. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.js +24 -20
  43. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.html +56 -5
  44. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.js +8 -2
  45. package/main.js +41 -17
  46. package/package.json +10 -5
  47. package/runtime/devices/adsclient/index.js +1 -1
  48. package/runtime/devices/bacnet/index.js +66 -32
  49. package/runtime/devices/ethernetip/index.js +1 -1
  50. package/runtime/devices/gpio/index.js +1 -1
  51. package/runtime/devices/odbc/index.js +5 -5
  52. package/runtime/devices/template/index.js +14 -14
  53. package/runtime/storage/daqstorage.js +28 -2
  54. package/runtime/storage/influxdb/index.js +1 -1
  55. package/runtime/storage/questdb/index.js +224 -0
  56. package/runtime/utils.js +5 -0
  57. package/settings.default.js +13 -3
  58. package/dist/main.020ca34630a3363a.js +0 -329
  59. package/dist/polyfills.c8e7db9850a3ad8b.js +0 -1
  60. 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:"", required:true},
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
- return this.name||this.tag||"set tag daq settings";
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
- datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
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-tag"><i class="fa fa-tag"></i> Tag</label>
39
- <input type="text" id="node-input-tag" list="fuxa-tags-set-daq-settings" placeholder="Tag Name">
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">&nbsp;</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
- var tagName = config.tag || msg.tag;
10
- if (tagName) {
11
- var tagId = fuxa.getTagId(tagName, null);
12
- if (tagId) {
13
- var settings = {
14
- enabled: config.enabled,
15
- interval: config.interval,
16
- deadband: config.deadband
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
- // Override with message properties if provided
20
- if (msg.enabled !== undefined) settings.enabled = msg.enabled;
21
- if (msg.interval !== undefined) settings.interval = msg.interval;
22
- if (msg.deadband !== undefined) settings.deadband = msg.deadband;
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
- var result = await fuxa.setTagDaqSettings(tagId, settings);
25
- msg.payload = result;
26
- node.send(msg);
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 name not specified', msg);
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:"", required:true}
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
- return this.name||this.tag||"set tag";
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
- datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
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-tag"><i class="fa fa-tag"></i> Tag</label>
36
- <input type="text" id="node-input-tag" list="fuxa-tags-set" placeholder="Tag Name">
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
- var tagId = fuxa.getTagId(config.tag, null);
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
- app.use(morgan('combined', {
346
- stream: accessLogStream,
347
- skip: function (req, res) { return res.statusCode < 400 }
348
- }));
349
-
350
- app.use(morgan('dev', {
351
- skip: function (req, res) {
352
- return res.statusCode < 400
353
- }, stream: process.stderr
354
- }));
355
-
356
- app.use(morgan('dev', {
357
- skip: function (req, res) {
358
- return res.statusCode >= 400
359
- }, stream: process.stdout
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.2.10",
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": "0.30.2",
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.0",
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.7",
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,7 +26,7 @@ function ADSclient(_data, _logger, _events, _runtime) {
26
26
  * initialize the device type
27
27
  */
28
28
  this.init = function (_type) {
29
- console.error('Not supported!');
29
+ console.error('Not supported! (adsclient.init)');
30
30
  }
31
31
 
32
32
  /**
@@ -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
- logger.error(`'${data.name}' readPropertyMultiple error! ${err}`);
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 readfnc = [];
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
- readfnc.push(_readProperty(_getDeviceAddress(devices[instance]), { type: object.type, instance: object.instance}, bacnet.enum.PropertyIdentifier.OBJECT_NAME));
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
- Promise.all(readfnc).then(results => {
475
- if (results) {
476
- for (var index in results) {
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
- resolve(objects);
488
- }, reason => {
489
- if (reason) {
490
- if (reason.stack) {
491
- logger.error(`'${data.name}' _readObjectList error! ${reason.stack}`);
492
- } else if (reason.message) {
493
- logger.error(`'${data.name}' _readObjectList error! ${reason.message}`);
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
- reject(reason);
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
- return device.address || device.sender.address;
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
+ }
@@ -28,7 +28,7 @@ function EthernetIPclient(_data, _logger, _events, _runtime) {
28
28
  * initialize the device type
29
29
  */
30
30
  this.init = function (_type) {
31
- console.error('Not supported!');
31
+ console.error('Not supported! (ethernetip.init)');
32
32
  }
33
33
 
34
34
  /**
@@ -198,7 +198,7 @@ function GpioClient(_data, _logger, _events, _runtime) {
198
198
  */
199
199
  this.lastReadTimestamp = () => {
200
200
  return lastTimestampValue;
201
- console.error('Not supported!');
201
+ console.error('Not supported! (gpio.lastReadTimestamp)');
202
202
  }
203
203
 
204
204
  /**