@bitpoolos/edge-bacnet 1.5.2 → 1.6.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/bacnet_read.html CHANGED
@@ -545,6 +545,12 @@
545
545
  menu.style.setProperty("--mouse-x", event.clientX + "px");
546
546
  menu.style.setProperty("--mouse-y", event.clientY + "px");
547
547
  menu.style.display = "block";
548
+
549
+ if (app.rightClickedDevice.node.showAdded == true) {
550
+ menu.classList.add("deviceAddedToRead");
551
+ } else if (app.rightClickedDevice.node.showAdded == false) {
552
+ menu.classList.remove("deviceAddedToRead");
553
+ }
548
554
  },
549
555
  onPointRightClick(slotProps, event) {
550
556
  let app = this;
@@ -553,6 +559,12 @@
553
559
  pointMenu.style.setProperty("--mouse-x", event.clientX + "px");
554
560
  pointMenu.style.setProperty("--mouse-y", event.clientY + "px");
555
561
  pointMenu.style.display = "block";
562
+
563
+ if (app.rightClickedPoint.node.showAdded == true) {
564
+ pointMenu.classList.add("pointAddedToRead");
565
+ } else if (app.rightClickedPoint.node.showAdded == false) {
566
+ pointMenu.classList.remove("pointAddedToRead");
567
+ }
556
568
  },
557
569
  handleContextMenuClick(type) {
558
570
  let app = this;
@@ -593,55 +605,34 @@
593
605
  },
594
606
  purgeDevice(slotProps) {
595
607
  let app = this;
596
- let device = app.deviceList.find((ele) => {
597
- if (ele.address.address) {
598
- return ele.address.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
599
- } else {
600
- return ele.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
601
- }
602
- });
608
+ let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
603
609
  if (device) {
604
- app.nodeService.purgeDevice(device).then(function (result) { });
610
+ app.nodeService.purgeDevice(device).then(function (result) {});
605
611
  }
606
612
  },
607
613
  updatePointsForDevice(slotProps) {
608
614
  let app = this;
609
- let device = app.deviceList.find((ele) => {
610
- if (ele.address.address) {
611
- return ele.address.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
612
- } else {
613
- return ele.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
614
- }
615
- });
615
+ let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
616
616
  if (device) {
617
- app.nodeService.updatePointsForDevice(device).then(function (result) { });
617
+ app.nodeService.updatePointsForDevice(device).then(function (result) {});
618
618
  }
619
619
  },
620
620
  updatePoint(slotProps) {
621
621
  let app = this;
622
- let device = app.deviceList.find((ele) => {
623
- if (ele.address.address) {
624
- return ele.address.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
625
- } else {
626
- return ele.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
627
- }
622
+ const pointKey = `${slotProps.node.bacnetType}:${slotProps.node.bacnetInstance}`;
623
+ const device = app.deviceList.find((ele) => {
624
+ return ele.displayName == slotProps.node.parentDevice;
628
625
  });
626
+ const deviceKey = `${app.getDeviceAddress(device.address)}-${device.deviceId}`;
629
627
  if (device) {
630
- app.nodeService.updatePoint(device).then(function (result) { });
628
+ app.nodeService.updatePoint(deviceKey, pointKey).then(function (result) {});
631
629
  }
632
630
  },
633
631
  setDeviceName() {
634
632
  let app = this;
635
633
  const slotProps = app.rightClickedDevice;
636
634
  const displayName = app.deviceDisplayNameValue;
637
- let device = app.deviceList.find((ele) => {
638
- if (ele.address.address) {
639
- return ele.address.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
640
- } else {
641
- return ele.address == slotProps.node.ipAddr && ele.deviceId == slotProps.node.deviceId;
642
- }
643
- });
644
-
635
+ let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
645
636
  if (device) {
646
637
  app.nodeService.setDeviceDisplayName(device, displayName).then(function (result) {
647
638
  if (result) {
@@ -660,7 +651,7 @@
660
651
  const pointName = slotProps.node.pointName;
661
652
 
662
653
  let device = app.deviceList.find((ele) => {
663
- return ele.deviceName == slotProps.node.parentDevice;
654
+ return ele.displayName == slotProps.node.parentDevice;
664
655
  });
665
656
 
666
657
  if (device) {
@@ -676,6 +667,18 @@
676
667
 
677
668
  app.showPointNameDialog = false;
678
669
  },
670
+ getDeviceFromDeviceList(ip, id) {
671
+ let app = this;
672
+ let device = app.deviceList.find((ele) => {
673
+ if (ele.address.address) {
674
+ return ele.address.address == ip && ele.deviceId == id;
675
+ } else {
676
+ return ele.address == ip && ele.deviceId == id;
677
+ }
678
+ });
679
+
680
+ return device;
681
+ },
679
682
  settingDeviceName(slotProps) {
680
683
  let app = this;
681
684
  if (slotProps.node.settingDisplayName) {
@@ -709,11 +712,9 @@
709
712
  (ele) => ele.ipAddr == key.split("-")[0] && ele.deviceId == key.split("-")[1]
710
713
  );
711
714
  let deviceName;
712
-
713
715
  if (readDevice) {
714
716
  deviceName = readDevice.label;
715
717
  }
716
-
717
718
  exportJson[key] = {};
718
719
  if (deviceName) {
719
720
  exportJson[key]["deviceName"] = deviceName;
@@ -725,9 +726,8 @@
725
726
  let pointName = devicePoints[pointIndex];
726
727
  let pointObject = device[pointName];
727
728
 
728
- //formatting json payload, still in progress
729
-
730
- if (pointObject) {
729
+ //formatting json payload
730
+ if (pointObject && pointName !== "deviceName") {
731
731
  exportJson[key][pointName] = {
732
732
  meta: pointObject.meta,
733
733
  objectName: pointObject.objectName,
@@ -765,6 +765,7 @@
765
765
  let ip = key.split("-")[0];
766
766
  let id = key.split("-")[1];
767
767
  const importedDevice = pointsToRead[key];
768
+ //match only by IP, to handle case of mstp device
768
769
  let foundIndex = app.devices.findIndex((ele) => ele.ipAddr == ip);
769
770
 
770
771
  if (foundIndex !== -1) {
@@ -772,17 +773,11 @@
772
773
  if (app.devices[foundIndex].deviceId == id) {
773
774
  //found device
774
775
  let treeDevice = app.devices[foundIndex];
775
- let device = app.deviceList.find((ele) => {
776
- if (ele.address.address) {
777
- return ele.address.address == ip && ele.deviceId == id;
778
- } else {
779
- return ele.address == ip && ele.deviceId == id;
780
- }
781
- });
776
+ let device = app.getDeviceFromDeviceList(ip, id);
782
777
 
783
778
  for (let pointName in importedDevice) {
784
779
  let point = importedDevice[pointName];
785
- if (pointName == "deviceName") {
780
+ if (pointName == "deviceName" && typeof point == "string") {
786
781
  app.nodeService.setDeviceDisplayName(device, point);
787
782
  treeDevice.label = point;
788
783
  } else {
@@ -849,7 +844,7 @@
849
844
 
850
845
  for (let pointName in importedDevice) {
851
846
  let point = importedDevice[pointName];
852
- if (pointName == "deviceName") {
847
+ if (pointName == "deviceName" && typeof point == "string") {
853
848
  app.nodeService.setDeviceDisplayName(device, point);
854
849
  mstpDevice.label = point;
855
850
  } else {
@@ -878,7 +873,9 @@
878
873
  } else {
879
874
  // read device found, add point to existing
880
875
  let pointIndex = app.readDevices[isDeviceInReadList].children[0].children.findIndex(
881
- (ele) => ele.data == point.objectName
876
+ (ele) =>
877
+ parseInt(ele.bacnetInstance) == parseInt(point.meta.objectId.instance) &&
878
+ parseInt(ele.bacnetType) == parseInt(point.meta.objectId.type)
882
879
  );
883
880
  if (pointIndex == -1) {
884
881
  app.readDevices[isDeviceInReadList].children[0].children.push(pointInTree);
@@ -1088,7 +1085,7 @@
1088
1085
  </a>
1089
1086
  </li>
1090
1087
  <li class="red-ui-menu-divider"></li>
1091
- <li class="context-menu-item" @click="handleContextMenuClick('setDeviceName')">
1088
+ <li class="context-menu-item set-device-name-option" @click="handleContextMenuClick('setDeviceName')">
1092
1089
  <a class="red-ui-menu-label">
1093
1090
  <i class="pi pi-pencil context-menu-icon"></i>
1094
1091
  <span class="context-menu-item-text">Set Device Name</span>
@@ -1100,7 +1097,7 @@
1100
1097
  <!-- Start Point Context Menu -->
1101
1098
  <ul
1102
1099
  class="red-ui-menu red-ui-menu-dropdown red-ui-menu-dropdown-direction-right red-ui-menu-dropdown-noicons red-ui-menu-dropdown-submenus point-context-menu">
1103
- <li class="context-menu-item" @click="handlePointContextMenuClick('setPointName')">
1100
+ <li class="context-menu-item set-point-name-option" @click="handlePointContextMenuClick('setPointName')">
1104
1101
  <a class="red-ui-menu-label">
1105
1102
  <i class="pi pi-pencil context-menu-icon"></i>
1106
1103
  <span class="context-menu-item-text">Set Point Name</span>
@@ -1495,4 +1492,4 @@
1495
1492
  <li><a href="https://wiki.bitpool.com/">wiki.bitpool.com</a> - find more documentation.</li>
1496
1493
  <li><a href="https://bacnet.org/">BACnet</a> - find more about the protocol.</li>
1497
1494
  </ul>
1498
- </script>
1495
+ </script>
package/bacnet_read.js CHANGED
@@ -1,9 +1,6 @@
1
1
  /*
2
2
  MIT License Copyright 2021, 2022 - Bitpool Pty Ltd
3
3
  */
4
-
5
-
6
-
7
4
  module.exports = function (RED) {
8
5
  const { ReadCommandConfig } = require('./common');
9
6
  const baEnum = require('./resources/node-bacstack-ts/dist/index.js').enum;
package/common.js CHANGED
@@ -1,13 +1,12 @@
1
1
  /*
2
- MIT License Copyright 2021, 2024 - Bitpool Pty Ltd
2
+ MIT License Copyright 2021, 2025 - Bitpool Pty Ltd
3
3
  */
4
4
 
5
- const { createLogger, format, transports } = require("winston");
6
5
  const { randomUUID } = require("crypto");
7
6
  const os = require("os");
8
- const { exec } = require("child_process");
9
7
  const baEnum = require("./resources/node-bacstack-ts/dist/index.js").enum;
10
8
  const fs = require("fs");
9
+ const fs2 = require("fs").promises;
11
10
 
12
11
  class BacnetConfig {
13
12
  constructor(
@@ -102,8 +101,6 @@ class BacnetClientConfig {
102
101
  }
103
102
  }
104
103
 
105
-
106
-
107
104
  class ReadCommandConfig {
108
105
  constructor(pointsToRead, objectProperties, decimalPrecision) {
109
106
  this.pointsToRead = pointsToRead;
@@ -190,30 +187,12 @@ const roundDecimalPlaces = function (value, decimals) {
190
187
  return value;
191
188
  };
192
189
 
193
- const doNodeRedRestart = function () {
194
- return new Promise(function (resolve, reject) {
195
- try {
196
- exec("restart", (error, stdout, stderr) => {
197
- if (error) {
198
- console.log(`Node-Red restart error: ${error.message}`);
199
- reject(error.message);
200
- }
201
- if (stderr) {
202
- console.log(`Node-Red restart stderr: ${stderr}`);
203
- reject(stderr);
204
- }
205
- resolve(stdout);
206
- });
207
- } catch (e) {
208
- console.log(`Node-Red restart error: ${e}`);
209
- reject(e);
210
- }
211
- });
212
- };
213
-
214
190
  // STORE CONFIG FUNCTION ==========================================================
215
191
  //
216
192
  // ================================================================================
193
+
194
+ /*
195
+
217
196
  async function Store_Config(data) {
218
197
  try {
219
198
  await fs.writeFile("edge-bacnet-datastore.cfg", data, { encoding: "utf8", flag: "w" }, (err) => {
@@ -226,18 +205,185 @@ async function Store_Config(data) {
226
205
  }
227
206
  }
228
207
 
208
+ */
209
+
210
+ // refactor:
211
+
212
+ let storeQueue = [];
213
+ let isStoreProcessing = false;
214
+
215
+ async function queueConfigStore(data) {
216
+ storeQueue.push(data);
217
+
218
+ if (!isStoreProcessing) {
219
+ isStoreProcessing = true;
220
+
221
+ while (storeQueue.length > 0) {
222
+ const nextData = storeQueue.pop(); // Get most recent data
223
+ storeQueue.length = 0; // Clear any accumulated data
224
+
225
+ await Store_Config(nextData);
226
+
227
+ // Add small delay between attempts
228
+ await new Promise((resolve) => setTimeout(resolve, 100));
229
+ }
230
+
231
+ isStoreProcessing = false;
232
+ }
233
+ }
234
+
235
+ async function Store_Config(data) {
236
+ const mainFile = "edge-bacnet-datastore.cfg";
237
+ const tempFile = "edge-bacnet-datastore.cfg.tmp";
238
+ const backupFile = "edge-bacnet-datastore.cfg.bak";
239
+
240
+ try {
241
+ // First validate the JSON to ensure it's valid before writing
242
+ try {
243
+ JSON.parse(JSON.stringify(data));
244
+ } catch (jsonError) {
245
+ console.error("Invalid JSON data detected:", jsonError);
246
+ return false;
247
+ }
248
+
249
+ // Write to temporary file first
250
+ await fs2.writeFile(tempFile, JSON.stringify(data, null, 2), { encoding: "utf8" });
251
+
252
+ // Verify the temporary file is valid JSON
253
+ try {
254
+ const tempContent = await fs2.readFile(tempFile, "utf8");
255
+ JSON.parse(tempContent);
256
+ } catch (verifyError) {
257
+ console.error("Temporary file validation failed:", verifyError);
258
+ await fs2.unlink(tempFile).catch(() => {});
259
+ return false;
260
+ }
261
+
262
+ // Create backup of current file if it exists
263
+ try {
264
+ await fs2.access(mainFile);
265
+ await fs2.copyFile(mainFile, backupFile);
266
+ } catch (backupError) {
267
+ // If main file doesn't exist, no backup needed
268
+ }
269
+
270
+ // Atomic rename of temporary file to main file
271
+ await fs2.rename(tempFile, mainFile);
272
+ return true;
273
+ } catch (error) {
274
+ console.error("Store_Config error:", error);
275
+
276
+ // Cleanup temporary file if it exists
277
+ try {
278
+ await fs2.unlink(tempFile).catch(() => {});
279
+ } catch (cleanupError) {}
280
+
281
+ // If main file is corrupted and backup exists, restore from backup
282
+ try {
283
+ const backupExists = await fs2.access(backupFile).catch(() => false);
284
+ if (backupExists) {
285
+ await fs2.copyFile(backupFile, mainFile);
286
+ console.log("Restored from backup file");
287
+ }
288
+ } catch (restoreError) {
289
+ console.error("Failed to restore from backup:", restoreError);
290
+ }
291
+
292
+ return false;
293
+ }
294
+ }
295
+
229
296
  // READ CONFIG SYNC FUNCTION ======================================================
230
297
  //
231
298
  // ================================================================================
299
+
232
300
  function Read_Config_Sync() {
233
- var data = "{}";
301
+ const mainFile = "edge-bacnet-datastore.cfg";
302
+ const backupFile = "edge-bacnet-datastore.cfg.bak";
303
+ const defaultData = "{}";
304
+
234
305
  try {
235
- data = fs.readFileSync("edge-bacnet-datastore.cfg", { encoding: "utf8", flag: "r" });
236
- } catch (err) {
237
- data = "{}";
238
- Store_Config(data);
306
+ // Try to read the main file
307
+ let data = fsSync.readFileSync(mainFile, { encoding: "utf8" });
308
+
309
+ // Validate JSON
310
+ try {
311
+ JSON.parse(data);
312
+ return data;
313
+ } catch (jsonError) {
314
+ console.error("Main file contains invalid JSON, attempting backup recovery");
315
+
316
+ // Try to read backup file
317
+ try {
318
+ const backupData = fsSync.readFileSync(backupFile, { encoding: "utf8" });
319
+ JSON.parse(backupData); // Validate backup JSON
320
+
321
+ // Restore from backup
322
+ fsSync.copyFileSync(backupFile, mainFile);
323
+ console.log("Successfully restored from backup file");
324
+ return backupData;
325
+ } catch (backupError) {
326
+ console.error("Backup recovery failed, creating new file");
327
+ fsSync.writeFileSync(mainFile, defaultData, { encoding: "utf8" });
328
+ return defaultData;
329
+ }
330
+ }
331
+ } catch (error) {
332
+ console.error("Error reading config:", error);
333
+ fsSync.writeFileSync(mainFile, defaultData, { encoding: "utf8" });
334
+ return defaultData;
335
+ }
336
+ }
337
+
338
+ // refactor:
339
+
340
+ async function Read_Config_Async() {
341
+ // todo rename function, not using sync
342
+ const mainFile = "edge-bacnet-datastore.cfg";
343
+ const backupFile = "edge-bacnet-datastore.cfg.bak";
344
+ const defaultData = "{}";
345
+
346
+ try {
347
+ // Try to read the main file
348
+ const data = await fs2.readFile(mainFile, { encoding: "utf8" });
349
+
350
+ // Validate JSON
351
+ try {
352
+ JSON.parse(data);
353
+
354
+ return data;
355
+ } catch (jsonError) {
356
+ console.error("Main file contains invalid JSON, attempting backup recovery");
357
+
358
+ // Try to read backup file
359
+ try {
360
+ const backupData = await fs2.readFile(backupFile, { encoding: "utf8" });
361
+ JSON.parse(backupData); // Validate backup JSON
362
+
363
+ // Restore from backup
364
+ await fs.copyFile(backupFile, mainFile);
365
+ console.log("Successfully restored from backup file");
366
+
367
+ console.log("log2");
368
+
369
+ return backupData;
370
+ } catch (backupError) {
371
+ console.error("Backup recovery failed, creating new file");
372
+ await Store_Config(defaultData);
373
+
374
+ console.log("log3");
375
+
376
+ return defaultData;
377
+ }
378
+ }
379
+ } catch (error) {
380
+ console.error("Error reading config:", error);
381
+ await Store_Config(defaultData);
382
+
383
+ console.log("log4");
384
+
385
+ return defaultData;
239
386
  }
240
- return data;
241
387
  }
242
388
 
243
389
  // STORE CONFIG FUNCTION - BACNET SERVER ==========================================
@@ -250,7 +396,7 @@ async function Store_Config_Server(data) {
250
396
  //console.log("Store_Config_Server writeFile error: ", err);
251
397
  }
252
398
  });
253
- } catch (err) { }
399
+ } catch (err) {}
254
400
  }
255
401
 
256
402
  // READ CONFIG SYNC FUNCTION - BACNET SERVER ======================================
@@ -288,12 +434,12 @@ function decodeBitArray(size, bits) {
288
434
  if (i == bits.length - 1) {
289
435
  return array;
290
436
  }
291
- };
437
+ }
292
438
  }
293
439
 
294
440
  function getBacnetErrorString(classInt, codeInt) {
295
- const classString = Object.keys(baEnum.ErrorClass).find(key => baEnum.ErrorClass[key] === classInt);
296
- const codeString = Object.keys(baEnum.ErrorCode).find(key => baEnum.ErrorCode[key] === codeInt);
441
+ const classString = Object.keys(baEnum.ErrorClass).find((key) => baEnum.ErrorClass[key] === classInt);
442
+ const codeString = Object.keys(baEnum.ErrorCode).find((key) => baEnum.ErrorCode[key] === codeInt);
297
443
  return `BacnetError - Class:${classString} - Code:${codeString}`;
298
444
  }
299
445
 
@@ -309,7 +455,22 @@ function parseBacnetError(error) {
309
455
  }
310
456
 
311
457
  return err;
312
- };
458
+ }
459
+ function debounce(func, wait) {
460
+ let timeout;
461
+
462
+ return function (...args) {
463
+ const context = this;
464
+
465
+ // Clear the previous timeout
466
+ clearTimeout(timeout);
467
+
468
+ // Set a new timeout
469
+ timeout = setTimeout(() => {
470
+ func.apply(context, args);
471
+ }, wait);
472
+ };
473
+ }
313
474
 
314
475
  module.exports = {
315
476
  BacnetConfig,
@@ -320,13 +481,15 @@ module.exports = {
320
481
  generateId,
321
482
  getIpAddress,
322
483
  roundDecimalPlaces,
323
- doNodeRedRestart,
484
+ queueConfigStore,
324
485
  Store_Config,
325
486
  Read_Config_Sync,
487
+ Read_Config_Async,
326
488
  Store_Config_Server,
327
489
  Read_Config_Sync_Server,
328
490
  isNumber,
329
491
  decodeBitArray,
330
492
  parseBacnetError,
331
493
  getBacnetErrorString,
494
+ debounce,
332
495
  };