@bitpoolos/edge-bacnet 1.5.2 → 1.5.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.3] - 23-01-2025
4
+
5
+ New feature:
6
+ - import / export buttons added to new tab in gateway node, used to manage the complete data model for backup or restore
7
+ - associated API end points for programatic backing up or restoring - /bitpool-bacnet-data/getDataModel; /bitpool-bacnet-data/updateDataModel;
8
+
9
+ Further async / await refactoring
10
+
11
+ Bug fixes:
12
+ - incorrect device name in read list export
13
+ - read command indexing unhandled scenario
14
+ - duplicating points in read list after pressing refresh tree button
15
+ - Multi State Values and other state text based points not being handled correctly in large volume scenarios
16
+
17
+ NOTE:
18
+ New importing and exporting feature handles a .json file instead of the .cfg file used in the back end. This is due to browsers flagging .cfg files as malicious. The contents of the file are unchanged.
19
+
3
20
  ## [1.5.2] - 10-01-2025
4
21
 
5
22
  Mismatched network request hot fix
package/bacnet_client.js CHANGED
@@ -534,7 +534,6 @@ class BacnetClient extends EventEmitter {
534
534
  }
535
535
  }
536
536
  try {
537
-
538
537
  await that.updateDeviceName(device);
539
538
 
540
539
  if (device.getSegmentation() !== 3) {
@@ -579,23 +578,35 @@ class BacnetClient extends EventEmitter {
579
578
  }
580
579
  }
581
580
 
582
- updateDeviceName(device) {
583
- let that = this;
584
- return new Promise((resolve, reject) => {
585
- that
586
- ._getDeviceName(device)
587
- .then(function (deviceObject) {
588
- if (typeof deviceObject.name == "string") {
589
- device.setDeviceName(deviceObject.name + " " + device.getDeviceId());
590
- device.setPointsList(deviceObject.devicePointEntry);
591
- }
592
- resolve();
593
- })
594
- .catch(function (e) {
595
- that.logOut("updateDeviceName error: ", e);
596
- resolve();
597
- });
598
- });
581
+ // updateDeviceName(device) {
582
+ // let that = this;
583
+ // return new Promise((resolve, reject) => {
584
+ // that
585
+ // ._getDeviceName(device)
586
+ // .then(function (deviceObject) {
587
+ // if (typeof deviceObject.name == "string") {
588
+ // device.setDeviceName(deviceObject.name + " " + device.getDeviceId());
589
+ // device.setPointsList(deviceObject.devicePointEntry);
590
+ // }
591
+ // resolve();
592
+ // })
593
+ // .catch(function (e) {
594
+ // that.logOut("updateDeviceName error: ", e);
595
+ // resolve();
596
+ // });
597
+ // });
598
+ // }
599
+
600
+ async updateDeviceName(device) {
601
+ try {
602
+ const deviceObject = await this._getDeviceName(device);
603
+ if (typeof deviceObject?.name === "string") {
604
+ device.setDeviceName(deviceObject.name + " " + device.getDeviceId());
605
+ device.setPointsList(deviceObject.devicePointEntry);
606
+ }
607
+ } catch (e) {
608
+ this.logOut("updateDeviceName error: ", e);
609
+ }
599
610
  }
600
611
 
601
612
  reinitializeClient(config) {
@@ -729,15 +740,15 @@ class BacnetClient extends EventEmitter {
729
740
  // Process points for the current device
730
741
  const pointsToRead = readConfig.pointsToRead[key];
731
742
  const pointNames = Object.keys(pointsToRead);
732
- let totalPoints = pointNames.length;
743
+ let totalPoints = pointNames.length - 1;
733
744
  let requestArray = [];
734
745
  let processedPoints = 0; // Counter for processed points
735
746
 
736
747
  // Process each point for the device in batches
737
748
  for (let i = 0; i < pointNames.length; i++) {
749
+
738
750
  const pointName = pointNames[i];
739
751
  if (pointName === "deviceName") {
740
- totalPoints = totalPoints - 1;
741
752
  continue;
742
753
  }
743
754
 
@@ -758,8 +769,7 @@ class BacnetClient extends EventEmitter {
758
769
  }
759
770
 
760
771
  // Process the batch when the request array is full or the last point is reached
761
- if (requestArray.length === maxObjectCount || i === pointNames.length - 1) {
762
-
772
+ if (requestArray.length === maxObjectCount) {
763
773
  if (device.getProtocolServiceSupport("ReadPropertyMultiple") == true) {
764
774
  await that.processBatch(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
765
775
  } else {
@@ -769,6 +779,16 @@ class BacnetClient extends EventEmitter {
769
779
  requestArray = [];
770
780
  // Increment the processed points counter
771
781
  processedPoints += maxObjectCount;
782
+ } else if (i === pointNames.length - 1) {
783
+ if (device.getProtocolServiceSupport("ReadPropertyMultiple") == true) {
784
+ await that.processBatch(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
785
+ } else {
786
+ await that.processIndividualPoints(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
787
+ }
788
+
789
+ requestArray = [];
790
+ // Increment the processed points counter
791
+ processedPoints += i;
772
792
  }
773
793
 
774
794
  // Check if all points for the device have been processed
@@ -870,17 +890,23 @@ class BacnetClient extends EventEmitter {
870
890
  const { objectId, pointRef, pointName } = request;
871
891
  try {
872
892
 
873
-
874
893
  const result = await that.updatePoint(device, pointRef);
875
894
 
876
- //const result = await that.updatePointWithRetry(device, pointRef, 1);
877
-
878
-
879
895
  if (result.objectId.type == objectId.type && result.objectId.instance == objectId.instance) {
880
896
  const val = result.values[0].value;
881
897
 
882
898
  if (isNumber(val)) {
883
899
  pointRef.presentValue = roundDecimalPlaces(val, roundDecimal);
900
+
901
+ if (pointRef.meta.objectId.type == 19 || pointRef.meta.objectId.type == 13 || pointRef.meta.objectId.type == 14) {
902
+ if (pointRef.stateTextArray && typeof pointRef.stateTextArray[0].value !== "object") {
903
+ if (val != 0) {
904
+ pointRef.presentValue = pointRef.stateTextArray[val - 1].value;
905
+ } else {
906
+ pointRef.presentValue = pointRef.stateTextArray[val].value;
907
+ }
908
+ }
909
+ }
884
910
  } else {
885
911
  pointRef.presentValue = val;
886
912
  }
@@ -905,24 +931,37 @@ class BacnetClient extends EventEmitter {
905
931
  }
906
932
  }
907
933
 
908
- updateManyPoints(device, points) {
909
- let that = this;
910
- return new Promise((resolve, reject) => {
911
-
934
+ // updateManyPoints(device, points) {
935
+ // let that = this;
936
+ // return new Promise((resolve, reject) => {
937
+ // // let readOptions = {
938
+ // // maxSegments: device.getMaxSe,
939
+ // // maxApdu: that.readPropertyMultipleOptions.maxApdu,
940
+ // // };
941
+
942
+ // that
943
+ // ._readObjectWithRequestArray(device, points, that.readPropertyMultipleOptions)
944
+ // .then(function (results) {
945
+ // resolve(results);
946
+ // })
947
+ // .catch(function (err) {
948
+ // reject(err);
949
+ // });
950
+ // });
951
+ // }
952
+
953
+ async updateManyPoints(device, points) {
954
+ try {
912
955
  // let readOptions = {
913
956
  // maxSegments: device.getMaxSe,
914
957
  // maxApdu: that.readPropertyMultipleOptions.maxApdu,
915
958
  // };
916
959
 
917
- that
918
- ._readObjectWithRequestArray(device, points, that.readPropertyMultipleOptions)
919
- .then(function (results) {
920
- resolve(results);
921
- })
922
- .catch(function (err) {
923
- reject(err);
924
- });
925
- });
960
+ const results = await this._readObjectWithRequestArray(device, points, this.readPropertyMultipleOptions);
961
+ return results;
962
+ } catch (error) {
963
+ throw error;
964
+ }
926
965
  }
927
966
 
928
967
  updatePointWithRetry(device, point, retryCount = 1) {
@@ -1552,6 +1591,22 @@ class BacnetClient extends EventEmitter {
1552
1591
  });
1553
1592
  }
1554
1593
 
1594
+ getDataModel() {
1595
+ let that = this;
1596
+ return new Promise(async function (resolve, reject) {
1597
+ try {
1598
+ resolve({
1599
+ renderList: that.renderList,
1600
+ deviceList: that.deviceList,
1601
+ pointList: that.networkTree,
1602
+ renderListCount: that.renderListCount,
1603
+ });
1604
+ } catch (e) {
1605
+ reject(e);
1606
+ }
1607
+ });
1608
+ }
1609
+
1555
1610
  updatePointsList(json) {
1556
1611
  let that = this;
1557
1612
  json.deviceList.forEach(function (updatedDevice) {
@@ -1587,6 +1642,29 @@ class BacnetClient extends EventEmitter {
1587
1642
  });
1588
1643
  }
1589
1644
 
1645
+ updateDataModel(json) {
1646
+ let that = this;
1647
+ return new Promise(async function (resolve, reject) {
1648
+ try {
1649
+ if (json.body.renderList) {
1650
+ that.renderList = json.body.renderList;
1651
+ }
1652
+ if (json.body.deviceList) {
1653
+ await that.updateDeviceList(json);
1654
+ }
1655
+ if (json.body.pointList) {
1656
+ that.networkTree = json.body.pointList;
1657
+ }
1658
+ if (json.body.renderListCount) {
1659
+ that.renderListCount = json.body.renderListCount;
1660
+ }
1661
+ resolve();
1662
+ } catch (e) {
1663
+ reject(e);
1664
+ }
1665
+ });
1666
+ }
1667
+
1590
1668
  sortDevices(a, b) {
1591
1669
  if (a.deviceId < b.deviceId) {
1592
1670
  return -1;
package/bacnet_device.js CHANGED
@@ -88,7 +88,9 @@ class BacnetDevice {
88
88
  }
89
89
 
90
90
  setDisplayName(displayName) {
91
- this.displayName = displayName;
91
+ if (typeof displayName == "string") {
92
+ this.displayName = displayName;
93
+ }
92
94
  }
93
95
 
94
96
  getDisplayName() {
@@ -203,6 +203,11 @@
203
203
  label: "Server",
204
204
  });
205
205
 
206
+ tabs.addTab({
207
+ id: "read-backup-tab",
208
+ label: "Backup & Restore",
209
+ });
210
+
206
211
  if (node.networkInterfaces && node.networkInterfaces.length > 0) {
207
212
  let nicSelector = document.getElementById("node-input-local_device_address");
208
213
  node.networkInterfaces.forEach(function (option) {
@@ -333,6 +338,8 @@
333
338
  vueapp.use(primevue.confirmationservice);
334
339
  node.vm1 = vueapp.mount("#serverParent");
335
340
 
341
+
342
+ // Import Device List
336
343
  $("#file-upload").on("change", function (event) {
337
344
  const input = event.target.files[0];
338
345
  const reader = new FileReader();
@@ -356,6 +363,7 @@
356
363
  reader.readAsText(input);
357
364
  });
358
365
 
366
+ //Export Device List
359
367
  $("#file-export").click(function (params) {
360
368
  $.ajax({
361
369
  url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/getDeviceList",
@@ -370,6 +378,45 @@
370
378
  });
371
379
  });
372
380
 
381
+ //Import complete Data model
382
+ $("#file-upload-database").on("change", function (event) {
383
+ const input = event.target.files[0];
384
+ const reader = new FileReader();
385
+
386
+ reader.onload = function (e) {
387
+ const text = e.target.result;
388
+
389
+ let jsonPayload = JSON.parse(text);
390
+
391
+ $.ajax({
392
+ type: "POST",
393
+ url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/updateDataModel",
394
+ dataType: "json",
395
+ contentType: "application/json",
396
+ data: JSON.stringify(jsonPayload),
397
+ success: function (result) { },
398
+ timeout: 15000,
399
+ });
400
+ };
401
+
402
+ reader.readAsText(input);
403
+ });
404
+
405
+ // Export complete Data model
406
+ $("#file-export-database").click(function (params) {
407
+ $.ajax({
408
+ url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/getDataModel",
409
+ success: function (deviceList) {
410
+ let data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(deviceList));
411
+ let aEle = document.getElementById("exportJSON");
412
+ aEle.setAttribute("href", "data:" + data);
413
+ aEle.setAttribute("download", "edge-bacnet-datastore.json");
414
+ aEle.click();
415
+ },
416
+ timeout: 10000,
417
+ });
418
+ });
419
+
373
420
  //start device scan range matrix
374
421
 
375
422
  $("#node-input-deviceIdRangeMatrix-container")
@@ -739,6 +786,21 @@
739
786
  .point_name {
740
787
  font-weight: bold;
741
788
  }
789
+ .database-backup {
790
+ border-top: 1px solid grey;
791
+ padding-top: 25px;
792
+ margin-top: 5px !important;
793
+ }
794
+ .database-file-label {
795
+ color: black;
796
+ font-weight: bold;
797
+ font-size: 16px;
798
+ width: auto !important;
799
+
800
+ }
801
+ .database-file-label-div {
802
+ padding-top: 15px;
803
+ }
742
804
  </style>
743
805
 
744
806
  <div class="form-row node-input-read-tabs-row">
@@ -967,6 +1029,26 @@
967
1029
  </div>
968
1030
  </div>
969
1031
  </div>
1032
+ <div id="read-backup-tab" style="display:none">
1033
+ <div class="database-file-label-div">
1034
+ <span class="database-file-label">Database File</span>
1035
+ </div>
1036
+
1037
+ <div class="form-row bp-import-buttons database-backup" id="importDeviceList">
1038
+ <!-- <label> Device List: </label> -->
1039
+ <label for="file-upload-database" class="custom-file-upload">
1040
+ <i class="fa fa-arrow-circle-up" id="fileLabel"></i>
1041
+ <a id="fileLabelText" style="padding-left: 10px;">Import database</a>
1042
+ </label>
1043
+ <input type="file" id="file-upload-database" accept="application/JSON" class="inputStyle" style="width: 258px;" />
1044
+ <label for="file-export-database" class="custom-file-upload" style="margin-left: 5px;">
1045
+ <i class="fa fa-arrow-circle-down" id="fileLabel"></i>
1046
+ <a id="fileLabelText" style="padding-left: 10px;">Export database</a>
1047
+ </label>
1048
+ <input id="file-export-database" class="inputStyle" style="width: 258px; display: none;" />
1049
+ <a id="exportJSON" style="display: none"></a>
1050
+ </div>
1051
+ </div>
970
1052
  </div>
971
1053
  </script>
972
1054
  <script type="text/html" data-help-name="Bacnet-Gateway">
package/bacnet_gateway.js CHANGED
@@ -371,6 +371,42 @@ module.exports = function (RED) {
371
371
  }
372
372
  });
373
373
 
374
+ //route handler for getting data model
375
+ RED.httpAdmin.get("/bitpool-bacnet-data/getDataModel", function (req, res) {
376
+ if (!node.bacnetClient) {
377
+ logOut("Issue with the bacnetClient while getting data model: ", node.bacnetClient);
378
+ res.send(false);
379
+ } else {
380
+ node.bacnetClient
381
+ .getDataModel()
382
+ .then(function (result) {
383
+ res.send(result);
384
+ })
385
+ .catch(function (error) {
386
+ res.send(error);
387
+ logOut("Error getting data model: ", error);
388
+ });
389
+ }
390
+ });
391
+
392
+ //route handler for updating data model
393
+ RED.httpAdmin.post("/bitpool-bacnet-data/updateDataModel", function (req, res) {
394
+ if (!node.bacnetClient) {
395
+ logOut("Issue with the bacnetClient while getting data model: ", node.bacnetClient);
396
+ res.send(false);
397
+ } else {
398
+ node.bacnetClient
399
+ .updateDataModel(req)
400
+ .then(function (result) {
401
+ res.send(result);
402
+ })
403
+ .catch(function (error) {
404
+ res.send(error);
405
+ logOut("Error getting data model: ", error);
406
+ });
407
+ }
408
+ });
409
+
374
410
  //route handler for purge device
375
411
  RED.httpAdmin.post("/bitpool-bacnet-data/purgeDevice", function (req, res) {
376
412
  if (!node.bacnetClient) {
package/bacnet_read.html CHANGED
@@ -634,14 +634,7 @@
634
634
  let app = this;
635
635
  const slotProps = app.rightClickedDevice;
636
636
  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
-
637
+ let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
645
638
  if (device) {
646
639
  app.nodeService.setDeviceDisplayName(device, displayName).then(function (result) {
647
640
  if (result) {
@@ -660,7 +653,7 @@
660
653
  const pointName = slotProps.node.pointName;
661
654
 
662
655
  let device = app.deviceList.find((ele) => {
663
- return ele.deviceName == slotProps.node.parentDevice;
656
+ return ele.displayName == slotProps.node.parentDevice;
664
657
  });
665
658
 
666
659
  if (device) {
@@ -676,6 +669,18 @@
676
669
 
677
670
  app.showPointNameDialog = false;
678
671
  },
672
+ getDeviceFromDeviceList(ip, id) {
673
+ let app = this;
674
+ let device = app.deviceList.find((ele) => {
675
+ if (ele.address.address) {
676
+ return ele.address.address == ip && ele.deviceId == id;
677
+ } else {
678
+ return ele.address == ip && ele.deviceId == id;
679
+ }
680
+ });
681
+
682
+ return device;
683
+ },
679
684
  settingDeviceName(slotProps) {
680
685
  let app = this;
681
686
  if (slotProps.node.settingDisplayName) {
@@ -709,11 +714,9 @@
709
714
  (ele) => ele.ipAddr == key.split("-")[0] && ele.deviceId == key.split("-")[1]
710
715
  );
711
716
  let deviceName;
712
-
713
717
  if (readDevice) {
714
718
  deviceName = readDevice.label;
715
719
  }
716
-
717
720
  exportJson[key] = {};
718
721
  if (deviceName) {
719
722
  exportJson[key]["deviceName"] = deviceName;
@@ -725,9 +728,8 @@
725
728
  let pointName = devicePoints[pointIndex];
726
729
  let pointObject = device[pointName];
727
730
 
728
- //formatting json payload, still in progress
729
-
730
- if (pointObject) {
731
+ //formatting json payload
732
+ if (pointObject && pointName !== "deviceName") {
731
733
  exportJson[key][pointName] = {
732
734
  meta: pointObject.meta,
733
735
  objectName: pointObject.objectName,
@@ -765,6 +767,7 @@
765
767
  let ip = key.split("-")[0];
766
768
  let id = key.split("-")[1];
767
769
  const importedDevice = pointsToRead[key];
770
+ //match only by IP, to handle case of mstp device
768
771
  let foundIndex = app.devices.findIndex((ele) => ele.ipAddr == ip);
769
772
 
770
773
  if (foundIndex !== -1) {
@@ -772,17 +775,11 @@
772
775
  if (app.devices[foundIndex].deviceId == id) {
773
776
  //found device
774
777
  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
- });
778
+ let device = app.getDeviceFromDeviceList(ip, id);
782
779
 
783
780
  for (let pointName in importedDevice) {
784
781
  let point = importedDevice[pointName];
785
- if (pointName == "deviceName") {
782
+ if (pointName == "deviceName" && typeof point == "string") {
786
783
  app.nodeService.setDeviceDisplayName(device, point);
787
784
  treeDevice.label = point;
788
785
  } else {
@@ -849,7 +846,7 @@
849
846
 
850
847
  for (let pointName in importedDevice) {
851
848
  let point = importedDevice[pointName];
852
- if (pointName == "deviceName") {
849
+ if (pointName == "deviceName" && typeof point == "string") {
853
850
  app.nodeService.setDeviceDisplayName(device, point);
854
851
  mstpDevice.label = point;
855
852
  } else {
@@ -878,7 +875,7 @@
878
875
  } else {
879
876
  // read device found, add point to existing
880
877
  let pointIndex = app.readDevices[isDeviceInReadList].children[0].children.findIndex(
881
- (ele) => ele.data == point.objectName
878
+ (ele) => parseInt(ele.bacnetInstance) == parseInt(point.meta.objectId.instance) && 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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpoolos/edge-bacnet",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "A bacnet gateway for node-red",
5
5
  "dependencies": {
6
6
  "@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
@@ -237,9 +237,13 @@ class Client extends events_1.EventEmitter {
237
237
  if (!result) return debug('Received invalid deleteObject message');
238
238
  this.emit('deleteObject', { address: address, invokeId: invokeId, request: result, srcAddress: srcAddress });
239
239
  } else if (service === baEnum.ConfirmedServiceChoice.ACKNOWLEDGE_ALARM) {
240
- result = baServices.alarmAcknowledge.decode(buffer, offset, length);
241
- if (!result) return debug('Received invalid alarmAcknowledge message');
242
- this.emit('alarmAcknowledge', { address: address, invokeId: invokeId, request: result, srcAddress: srcAddress });
240
+ try {
241
+ result = baServices.alarmAcknowledge.decode(buffer, offset, length);
242
+ if (!result) return debug('Received invalid alarmAcknowledge message');
243
+ this.emit('alarmAcknowledge', { address: address, invokeId: invokeId, request: result, srcAddress: srcAddress });
244
+ } catch (e) {
245
+ //console.log("Error in alarmAcknowledge: ", e);
246
+ }
243
247
  } else if (service === baEnum.ConfirmedServiceChoice.GET_ALARM_SUMMARY) {
244
248
  this.emit('getAlarmSummary', { address: address, invokeId: invokeId });
245
249
  } else if (service === baEnum.ConfirmedServiceChoice.GET_ENROLLMENT_SUMMARY) {