@bitpoolos/edge-bacnet 1.6.2 → 1.6.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.3] - 00-06-2025
4
+
5
+ Minor feature:
6
+ - Device List - new right click option to MSTP NET folders - Update All Devices. Specifically updates the mstp device listed in that selected network
7
+ - Inspector - Added statistic percentages as MQTT output in the Inspector node
8
+ - Inspector - Added online / offline stats and Total points to read as status on node
9
+ - Test Functions - Outputs results to both node-red console and node-red debug window now
10
+
11
+ Minor update / refactor:
12
+ - Datamodel - Importing and Exporting datamodel displays process status and related stats (file size etc) while importing or exporting, informing the user of current state
13
+ - Device List - UI tree no longer stored in datamodel, as it is generated dynamically on a schedule. This significantly reduces datamodel file size, read / write time, system start up processes.
14
+ - Datamodel - writes to file system for persistance and backup was being executed more often than needed. Process schedule is now on a larger interval, and write algorithm refactored and optimized.
15
+ - Device List - right click -> Set Point Name feature refactored, due to many scenarios where it wasnt executing as expected.
16
+ - BACnet read output - error and status field setting optimized
17
+ - Inspector - updated ObjectType column values to show object type enum string instead of integer
18
+
19
+ Bug fixes:
20
+ - Inspector - incorrect percentages, statistics and values in the statistics bar. Fixed and tested to represent site status more accurately
21
+ - Inpsector - not flagging offline points in error statistic and table filter. Now correctly identifies "offline" points as an Error type
22
+ - BACnet read output - was not updating error and status correctly for full object payloads.
23
+ - BACnet Server - was not outputting sucessfull write update MQTT msg after node-red deploy.
24
+
25
+ Updating:
26
+ - There shouldnt be a need for users to remove nodes or do any specific action when updating to this version, however backing up a datamodel export is advised.
27
+
28
+
3
29
  ## [1.6.2] - 07-05-2025
4
30
 
5
31
  Minor feature:
package/bacnet_client.js CHANGED
@@ -20,6 +20,7 @@ const { BacnetDevice } = require("./bacnet_device");
20
20
  const { Mutex } = require("async-mutex");
21
21
  const { treeBuilder } = require("./treeBuilder.js");
22
22
 
23
+
23
24
  class BacnetClient extends EventEmitter {
24
25
  //client constructor
25
26
  constructor(config) {
@@ -28,6 +29,7 @@ class BacnetClient extends EventEmitter {
28
29
  that.config = config;
29
30
  that.deviceList = [];
30
31
  that.networkTree = {};
32
+ that.renderList = [];
31
33
  that.lastWhoIs = null;
32
34
  that.client = null;
33
35
  that.lastNetworkPoll = null;
@@ -201,7 +203,8 @@ class BacnetClient extends EventEmitter {
201
203
  const cachedData = await Read_Config_Async();
202
204
  const parsedData = JSON.parse(cachedData);
203
205
  if (parsedData && typeof parsedData == "object") {
204
- if (parsedData.renderList) that.renderList = parsedData.renderList;
206
+ // renderList is no longer cached - will be rebuilt by tree builder
207
+ // if (parsedData.renderList) that.renderList = parsedData.renderList;
205
208
  if (parsedData.deviceList) {
206
209
  parsedData.deviceList.forEach(function (device) {
207
210
  let newBacnetDevice = new BacnetDevice(true, device);
@@ -209,12 +212,13 @@ class BacnetClient extends EventEmitter {
209
212
  });
210
213
  }
211
214
  if (parsedData.pointList) that.networkTree = parsedData.pointList;
212
- if (parsedData.renderListCount) that.renderListCount = parsedData.renderListCount;
215
+ // renderListCount is no longer cached - will be recalculated by tree builder
216
+ // if (parsedData.renderListCount) that.renderListCount = parsedData.renderListCount;
213
217
  }
214
218
  }
215
219
  }
216
220
 
217
- testFunction(address, port, type, instance, property) {
221
+ testFunction(address, port, type, instance, property, nodeWarnCallback) {
218
222
  let that = this;
219
223
  console.log("test function ");
220
224
 
@@ -229,6 +233,11 @@ class BacnetClient extends EventEmitter {
229
233
  console.log("1 - readPropertyMultiple: ");
230
234
 
231
235
  console.log(value);
236
+
237
+ if (nodeWarnCallback) {
238
+ nodeWarnCallback(value);
239
+ }
240
+
232
241
  if (value) {
233
242
  // If the result has value, resolve the promise
234
243
  console.log(value.values[0]);
@@ -720,35 +729,30 @@ class BacnetClient extends EventEmitter {
720
729
  const that = this;
721
730
  const roundDecimal = readConfig.precision;
722
731
  const devicesToRead = Object.keys(readConfig.pointsToRead);
723
- const bacnetResults = {};
724
- let pendingRequests = 0;
732
+ let completedDevices = 0;
725
733
 
726
734
  try {
727
- // Process all devices in sequence
728
- for (let deviceIndex = 0; deviceIndex < devicesToRead.length; deviceIndex++) {
729
- const key = devicesToRead[deviceIndex];
735
+ // Create array of device processing promises
736
+ const devicePromises = devicesToRead.map(async (key, deviceIndex) => {
730
737
  const device = that.findDeviceByKey(key);
731
- if (!device) continue;
738
+ if (!device) return null;
732
739
 
733
740
  const deviceName = that.computeDeviceName(device);
734
741
  const deviceKey = that.createDeviceKey(device);
735
742
  const deviceObject = that.networkTree[deviceKey];
736
743
  const maxObjectCount = that.estimateMaxObjectSize(device.getMaxApdu());
737
744
 
738
- if (!bacnetResults[deviceName]) {
739
- bacnetResults[deviceName] = {};
740
- }
745
+ const bacnetResults = {};
746
+ bacnetResults[deviceName] = {};
741
747
 
742
748
  // Process points for the current device
743
749
  const pointsToRead = readConfig.pointsToRead[key];
744
750
  const pointNames = Object.keys(pointsToRead);
745
751
  let totalPoints = pointNames.length - 1;
746
752
  let requestArray = [];
747
- let processedPoints = 0; // Counter for processed points
748
753
 
749
754
  // Process each point for the device in batches
750
755
  for (let i = 0; i < pointNames.length; i++) {
751
-
752
756
  const pointName = pointNames[i];
753
757
  if (pointName === "deviceName") {
754
758
  continue;
@@ -771,7 +775,7 @@ class BacnetClient extends EventEmitter {
771
775
  }
772
776
 
773
777
  // Process the batch when the request array is full or the last point is reached
774
- if (requestArray.length === maxObjectCount) {
778
+ if (requestArray.length === maxObjectCount || i === pointNames.length - 1) {
775
779
  if (device.getProtocolServiceSupport("ReadPropertyMultiple") == true) {
776
780
  await that.processBatch(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
777
781
  } else {
@@ -779,38 +783,43 @@ class BacnetClient extends EventEmitter {
779
783
  }
780
784
 
781
785
  requestArray = [];
782
- // Increment the processed points counter
783
- processedPoints += maxObjectCount;
784
- } else if (i === pointNames.length - 1) {
785
- if (device.getProtocolServiceSupport("ReadPropertyMultiple") == true) {
786
- await that.processBatch(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
787
- } else {
788
- await that.processIndividualPoints(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
789
- }
790
-
791
- requestArray = [];
792
- // Increment the processed points counter
793
- processedPoints += i;
794
786
  }
787
+ }
795
788
 
796
- // Check if all points for the device have been processed
797
- if (processedPoints >= totalPoints) {
798
- pendingRequests++;
799
-
800
- // Emit the `values` event for the current device
801
- that.emit(
802
- "values",
803
- bacnetResults,
804
- outputType,
805
- objectPropertyType,
806
- readNodeName,
807
- pendingRequests,
808
- devicesToRead.length
809
- );
810
- delete bacnetResults[deviceName];
811
- }
789
+ // Return results for this device
790
+ return {
791
+ deviceName,
792
+ results: bacnetResults,
793
+ deviceIndex: deviceIndex + 1,
794
+ totalDevices: devicesToRead.length
795
+ };
796
+ });
797
+
798
+ // Process all devices in parallel and emit results as they complete
799
+ const results = await Promise.allSettled(devicePromises);
800
+
801
+ results.forEach((result, index) => {
802
+ if (result.status === 'fulfilled' && result.value) {
803
+ completedDevices++;
804
+ const { deviceName, results: bacnetResults, deviceIndex, totalDevices } = result.value;
805
+
806
+ // Emit the `values` event for this device immediately
807
+ that.emit(
808
+ "values",
809
+ bacnetResults,
810
+ outputType,
811
+ objectPropertyType,
812
+ readNodeName,
813
+ completedDevices,
814
+ totalDevices
815
+ );
816
+ } else {
817
+ // Handle failed device (offline/error)
818
+ completedDevices++;
819
+ that.logOut(`Device ${devicesToRead[index]} failed:`, result.reason);
812
820
  }
813
- }
821
+ });
822
+
814
823
  } catch (error) {
815
824
  that.logOut("doRead error: ", error);
816
825
  }
@@ -847,6 +856,7 @@ class BacnetClient extends EventEmitter {
847
856
  if (isNumber(val)) {
848
857
  pointRef.presentValue = roundDecimalPlaces(val, roundDecimal);
849
858
  pointRef.error = "none";
859
+ pointRef.status = "online";
850
860
  if (pointRef.meta.objectId.type == 19 || pointRef.meta.objectId.type == 13 || pointRef.meta.objectId.type == 14) {
851
861
  if (pointRef.stateTextArray && typeof pointRef.stateTextArray[0].value !== "object") {
852
862
  if (val != 0) {
@@ -860,14 +870,18 @@ class BacnetClient extends EventEmitter {
860
870
  if (typeof val !== "object") {
861
871
  pointRef.presentValue = val;
862
872
  pointRef.error = "none";
873
+ pointRef.status = "online";
863
874
  } else if (val.errorClass && val.errorClass) {
864
875
  pointRef.error = getBacnetErrorString(val.errorClass, val.errorClass);
876
+ pointRef.status = "offline";
877
+ } else {
878
+ pointRef.error = "none";
879
+ pointRef.status = "online";
865
880
  }
866
881
  }
867
882
 
868
883
  pointRef.meta["device"] = deviceMetaInfo;
869
884
  pointRef.timestamp = Date.now();
870
- pointRef.status = "online";
871
885
 
872
886
  // Store the point data in results
873
887
  bacnetResults[deviceName][pointNameRef] = pointRef;
@@ -899,6 +913,8 @@ class BacnetClient extends EventEmitter {
899
913
 
900
914
  if (isNumber(val)) {
901
915
  pointRef.presentValue = roundDecimalPlaces(val, roundDecimal);
916
+ pointRef.error = "none";
917
+ pointRef.status = "online";
902
918
 
903
919
  if (pointRef.meta.objectId.type == 19 || pointRef.meta.objectId.type == 13 || pointRef.meta.objectId.type == 14) {
904
920
  if (pointRef.stateTextArray && typeof pointRef.stateTextArray[0].value !== "object") {
@@ -910,13 +926,21 @@ class BacnetClient extends EventEmitter {
910
926
  }
911
927
  }
912
928
  } else {
913
- pointRef.presentValue = val;
929
+ if (typeof val !== "object") {
930
+ pointRef.presentValue = val;
931
+ pointRef.error = "none";
932
+ pointRef.status = "online";
933
+ } else if (val.errorClass && val.errorClass) {
934
+ pointRef.error = getBacnetErrorString(val.errorClass, val.errorClass);
935
+ pointRef.status = "offline";
936
+ } else {
937
+ pointRef.error = "none";
938
+ pointRef.status = "online";
939
+ }
914
940
  }
915
941
 
916
942
  pointRef.meta["device"] = deviceMetaInfo;
917
943
  pointRef.timestamp = Date.now();
918
- pointRef.status = "online";
919
- pointRef.error = "none";
920
944
 
921
945
  // Store the point data in results
922
946
  bacnetResults[deviceName][pointName] = pointRef;
@@ -1681,7 +1705,7 @@ class BacnetClient extends EventEmitter {
1681
1705
  if (json.body.renderListCount) {
1682
1706
  that.renderListCount = json.body.renderListCount;
1683
1707
  }
1684
- resolve();
1708
+ resolve(true);
1685
1709
  } catch (e) {
1686
1710
  reject(e);
1687
1711
  }
@@ -136,6 +136,16 @@
136
136
  body: JSON.stringify({ k: deviceKey, p: pointName, n: pointDisplayName }),
137
137
  }).then((res) => res.json());
138
138
  }
139
+ applyDisplayNames(pointsToRead) {
140
+ return fetch(RED.settings.httpNodeRoot + "bitpool-bacnet-data/applyDisplayNames", {
141
+ method: "POST",
142
+ headers: {
143
+ Accept: "application/json",
144
+ "Content-Type": "application/json",
145
+ },
146
+ body: JSON.stringify({ pointsToRead: pointsToRead }),
147
+ }).then((res) => res.json());
148
+ }
139
149
  importReadList(payload) {
140
150
  return fetch(RED.settings.httpNodeRoot + "bitpool-bacnet-data/importReadList", {
141
151
  method: "POST",
@@ -396,25 +406,130 @@
396
406
  });
397
407
  });
398
408
 
409
+ function setDataModelUpdateStatus(status, type, autoClear) {
410
+ const statusElement = $("#data-model-update-status");
411
+ statusElement.text(status);
412
+
413
+ // Remove existing status classes
414
+ statusElement.removeClass("status-success status-error status-warning status-info");
415
+
416
+ // Add appropriate styling based on type
417
+ switch (type) {
418
+ case "success":
419
+ statusElement.addClass("status-success");
420
+ break;
421
+ case "error":
422
+ statusElement.addClass("status-error");
423
+ break;
424
+ case "warning":
425
+ statusElement.addClass("status-warning");
426
+ break;
427
+ case "info":
428
+ default:
429
+ statusElement.addClass("status-info");
430
+ break;
431
+ }
432
+
433
+ // Auto-clear success messages after 5 seconds
434
+ if (autoClear !== false && type === "success") {
435
+ setTimeout(() => {
436
+ statusElement.text("");
437
+ statusElement.removeClass("status-success");
438
+ }, 5000);
439
+ }
440
+ }
441
+
399
442
  //Import complete Data model
400
443
  $("#file-upload-database").on("change", function (event) {
401
444
  const input = event.target.files[0];
445
+
446
+ if (!input) {
447
+ setDataModelUpdateStatus("No file selected", "warning");
448
+ return;
449
+ }
450
+
451
+ // Validate file type
452
+ if (!input.name.toLowerCase().endsWith('.json')) {
453
+ setDataModelUpdateStatus("Please select a valid JSON file", "error");
454
+ return;
455
+ }
456
+
457
+ // Check file size (warn if larger than 10MB)
458
+ const fileSizeMB = (input.size / (1024 * 1024)).toFixed(2);
459
+ if (input.size > 10 * 1024 * 1024) {
460
+ setDataModelUpdateStatus(`Large file detected (${fileSizeMB}MB). Import may take several minutes...`, "warning");
461
+ }
462
+
463
+ // Confirmation dialog for potentially destructive operation
464
+ if (!confirm(`Are you sure you want to import this data model?\n\nFile: ${input.name}\nSize: ${fileSizeMB}MB\n\nThis will replace the existing data model and cannot be undone.`)) {
465
+ // Reset the file input
466
+ $(this).val('');
467
+ setDataModelUpdateStatus("Import cancelled", "info");
468
+ return;
469
+ }
470
+
402
471
  const reader = new FileReader();
403
472
 
404
- reader.onload = function (e) {
405
- const text = e.target.result;
473
+ reader.onloadstart = function () {
474
+ setDataModelUpdateStatus("Reading file...", "info");
475
+ };
406
476
 
407
- let jsonPayload = JSON.parse(text);
477
+ reader.onprogress = function (e) {
478
+ if (e.lengthComputable) {
479
+ const percentLoaded = Math.round((e.loaded / e.total) * 100);
480
+ setDataModelUpdateStatus(`Reading file... ${percentLoaded}%`, "info");
481
+ }
482
+ };
408
483
 
409
- $.ajax({
410
- type: "POST",
411
- url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/updateDataModel",
412
- dataType: "json",
413
- contentType: "application/json",
414
- data: JSON.stringify(jsonPayload),
415
- success: function (result) { },
416
- timeout: 0,
417
- });
484
+ reader.onload = function (e) {
485
+ setDataModelUpdateStatus("Parsing JSON data...", "info");
486
+
487
+ try {
488
+ const text = e.target.result;
489
+ let jsonPayload = JSON.parse(text);
490
+
491
+ setDataModelUpdateStatus("Uploading data model to server...", "info");
492
+
493
+ $.ajax({
494
+ type: "POST",
495
+ url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/updateDataModel",
496
+ dataType: "json",
497
+ contentType: "application/json",
498
+ data: JSON.stringify(jsonPayload),
499
+ success: function (data, status, xhr) {
500
+ setDataModelUpdateStatus(`Data model imported successfully! (${fileSizeMB}MB processed)`, "success");
501
+ // Clear the file input
502
+ $("#file-upload-database").val('');
503
+ },
504
+ error: function (xhr, status, error) {
505
+ let errorMsg = "Failed to import data model";
506
+ if (xhr.responseText) {
507
+ try {
508
+ const errorResponse = JSON.parse(xhr.responseText);
509
+ errorMsg += ": " + (errorResponse.message || errorResponse.error || xhr.responseText);
510
+ } catch (e) {
511
+ errorMsg += ": " + xhr.responseText;
512
+ }
513
+ } else {
514
+ errorMsg += ": " + (error || status || "Unknown error");
515
+ }
516
+ setDataModelUpdateStatus(errorMsg, "error");
517
+ // Clear the file input
518
+ $("#file-upload-database").val('');
519
+ },
520
+ timeout: 0,
521
+ });
522
+ } catch (parseError) {
523
+ setDataModelUpdateStatus("Invalid JSON file: " + parseError.message, "error");
524
+ // Clear the file input
525
+ $("#file-upload-database").val('');
526
+ }
527
+ };
528
+
529
+ reader.onerror = function () {
530
+ setDataModelUpdateStatus("Error reading file", "error");
531
+ // Clear the file input
532
+ $("#file-upload-database").val('');
418
533
  };
419
534
 
420
535
  reader.readAsText(input);
@@ -422,16 +537,82 @@
422
537
 
423
538
  // Export complete Data model
424
539
  $("#file-export-database").click(function (params) {
540
+ // Confirmation dialog
541
+ if (!confirm("Are you sure you want to export the complete data model?\n\nThis may take some time for large databases.")) {
542
+ setDataModelUpdateStatus("Export cancelled", "info");
543
+ return;
544
+ }
545
+
546
+ setDataModelUpdateStatus("Retrieving data model from server...", "info");
547
+
548
+ const startTime = Date.now();
549
+
425
550
  $.ajax({
426
551
  url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/getDataModel",
427
552
  success: function (deviceList) {
428
- let data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(deviceList));
429
- let aEle = document.getElementById("exportJSON");
430
- aEle.setAttribute("href", "data:" + data);
431
- aEle.setAttribute("download", "edge-bacnet-datastore.json");
432
- aEle.click();
553
+ try {
554
+ setDataModelUpdateStatus("Preparing download...", "info");
555
+
556
+ // Calculate data size
557
+ const jsonString = JSON.stringify(deviceList);
558
+ const dataSize = new Blob([jsonString]).size;
559
+ const dataSizeMB = (dataSize / (1024 * 1024)).toFixed(2);
560
+
561
+ // Create a Blob with the JSON data
562
+ const blob = new Blob([jsonString], { type: 'application/json' });
563
+
564
+ // Create an object URL from the Blob
565
+ const url = URL.createObjectURL(blob);
566
+
567
+ // Generate filename with timestamp
568
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
569
+ const filename = `edge-bacnet-datastore-${timestamp}.json`;
570
+
571
+ // Set the URL to the link and trigger download
572
+ let aEle = document.getElementById("exportJSON");
573
+ aEle.setAttribute("href", url);
574
+ aEle.setAttribute("download", filename);
575
+ aEle.click();
576
+
577
+ // Calculate duration
578
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
579
+
580
+ setDataModelUpdateStatus(`Export completed successfully! (${dataSizeMB}MB, ${duration}s)`, "success");
581
+
582
+ // Release the object URL when done
583
+ setTimeout(() => {
584
+ URL.revokeObjectURL(url);
585
+ }, 2000);
586
+
587
+ } catch (error) {
588
+ console.error("Error exporting data model:", error);
589
+ setDataModelUpdateStatus("Failed to create download file: " + error.message, "error");
590
+ }
433
591
  },
434
- timeout: 0,
592
+ error: function (xhr, status, error) {
593
+ console.error("AJAX error retrieving data model:", status, error);
594
+ let errorMsg = "Failed to retrieve data model from server";
595
+
596
+ if (xhr.status === 0) {
597
+ errorMsg += " (Connection timeout or server unavailable)";
598
+ } else if (xhr.status === 404) {
599
+ errorMsg += " (Endpoint not found)";
600
+ } else if (xhr.status === 500) {
601
+ errorMsg += " (Internal server error)";
602
+ } else if (xhr.responseText) {
603
+ try {
604
+ const errorResponse = JSON.parse(xhr.responseText);
605
+ errorMsg += ": " + (errorResponse.message || errorResponse.error);
606
+ } catch (e) {
607
+ errorMsg += ": " + xhr.responseText;
608
+ }
609
+ } else {
610
+ errorMsg += ": " + (error || status || "Unknown error");
611
+ }
612
+
613
+ setDataModelUpdateStatus(errorMsg, "error");
614
+ },
615
+ timeout: 60000, // 60 second timeout for large exports
435
616
  });
436
617
  });
437
618
 
@@ -818,6 +999,39 @@
818
999
  .database-file-label-div {
819
1000
  padding-top: 15px;
820
1001
  }
1002
+ /* Status indicator styles */
1003
+ .status-success {
1004
+ color: #28a745 !important;
1005
+ font-weight: 500;
1006
+ background-color: #d4edda;
1007
+ border: 1px solid #c3e6cb;
1008
+ border-radius: 4px;
1009
+ padding: 8px 12px;
1010
+ }
1011
+ .status-error {
1012
+ color: #dc3545 !important;
1013
+ font-weight: 500;
1014
+ background-color: #f8d7da;
1015
+ border: 1px solid #f5c6cb;
1016
+ border-radius: 4px;
1017
+ padding: 8px 12px;
1018
+ }
1019
+ .status-warning {
1020
+ color: #856404 !important;
1021
+ font-weight: 500;
1022
+ background-color: #fff3cd;
1023
+ border: 1px solid #ffeaa7;
1024
+ border-radius: 4px;
1025
+ padding: 8px 12px;
1026
+ }
1027
+ .status-info {
1028
+ color: #0c5460 !important;
1029
+ font-weight: 500;
1030
+ background-color: #d1ecf1;
1031
+ border: 1px solid #bee5eb;
1032
+ border-radius: 4px;
1033
+ padding: 8px 12px;
1034
+ }
821
1035
  </style>
822
1036
 
823
1037
  <div class="form-row node-input-read-tabs-row">
@@ -1070,6 +1284,7 @@
1070
1284
  <input id="file-export-database" class="inputStyle" style="width: 258px; display: none;" />
1071
1285
  <a id="exportJSON" style="display: none"></a>
1072
1286
  </div>
1287
+ <div id="data-model-update-status" class="data-model-update-status" style="margin-top: 10px;"></div>
1073
1288
  </div>
1074
1289
  </div>
1075
1290
  </script>
package/bacnet_gateway.js CHANGED
@@ -215,11 +215,14 @@ module.exports = function (RED) {
215
215
  if (
216
216
  node.bacnetServerEnabled == true &&
217
217
  node.bacnetClient &&
218
- node.bacnetServer &&
219
- nodeContext.get("serverWritePropEvent") == false
218
+ node.bacnetServer
220
219
  ) {
221
220
  try {
222
- node.bacnetServer.on("writeProperty", (topic, newValue) => {
221
+ // Clean up any existing listeners to prevent stale references
222
+ node.bacnetServer.removeAllListeners('writeProperty');
223
+
224
+ // Store the event handler function so we can clean it up later
225
+ node.writePropertyHandler = (topic, newValue) => {
223
226
  let formattedTopic = topic;
224
227
  if (
225
228
  node.nodeName !== "gateway" &&
@@ -234,8 +237,9 @@ module.exports = function (RED) {
234
237
  }
235
238
 
236
239
  node.send({ payload: newValue, topic: formattedTopic });
237
- });
238
- nodeContext.set("serverWritePropEvent", true);
240
+ };
241
+
242
+ node.bacnetServer.on("writeProperty", node.writePropertyHandler);
239
243
  } catch (e) {
240
244
  console.log("Bacnet gateway node server writePoperty error: ", e);
241
245
  }
@@ -270,7 +274,7 @@ module.exports = function (RED) {
270
274
  logOut("Error updating priorityQueue: ", error);
271
275
  });
272
276
  } else if (msg.testFunc == true) {
273
- node.bacnetClient.testFunction(msg.address, msg.port, msg.type, msg.instance, msg.property);
277
+ node.bacnetClient.testFunction(msg.address, msg.port, msg.type, msg.instance, msg.property, nodeWarn);
274
278
  } else if (msg.applyDisplayNames) {
275
279
  node.status({ fill: "blue", shape: "dot", text: "Updating display names" });
276
280
  setTimeout(() => {
@@ -300,7 +304,14 @@ module.exports = function (RED) {
300
304
  });
301
305
 
302
306
  node.on("close", function () {
303
- //do nothing
307
+ // Clean up the writeProperty event listener
308
+ if (node.bacnetServer && node.writePropertyHandler) {
309
+ node.bacnetServer.removeListener('writeProperty', node.writePropertyHandler);
310
+ node.writePropertyHandler = null;
311
+ }
312
+
313
+ // Reset the serverWritePropEvent flag so it can be re-registered
314
+ nodeContext.set("serverWritePropEvent", false);
304
315
  });
305
316
  } catch (e) {
306
317
  console.log("Bacnet node event handler error: ", e);
@@ -461,7 +472,7 @@ module.exports = function (RED) {
461
472
  node.bacnetClient
462
473
  .updateDataModel(req)
463
474
  .then(function (result) {
464
- res.send(result);
475
+ res.send(true);
465
476
  })
466
477
  .catch(function (error) {
467
478
  res.send(error);
@@ -560,6 +571,24 @@ module.exports = function (RED) {
560
571
  }
561
572
  });
562
573
 
574
+ //route handler for applyDisplayNames
575
+ RED.httpAdmin.post("/bitpool-bacnet-data/applyDisplayNames", function (req, res) {
576
+ if (!node.bacnetClient) {
577
+ logOut("Issue with the bacnetClient while applying display names: ", node.bacnetClient);
578
+ res.send(false);
579
+ } else {
580
+ node.bacnetClient
581
+ .applyDisplayNames(req.body.pointsToRead)
582
+ .then(function (result) {
583
+ res.send(result);
584
+ })
585
+ .catch(function (error) {
586
+ res.send(error);
587
+ logOut("Error applying display names: ", error);
588
+ });
589
+ }
590
+ });
591
+
563
592
  //route handler for importReadList
564
593
  RED.httpAdmin.post("/bitpool-bacnet-data/importReadList", function (req, res) {
565
594
  if (!node.bacnetClient) {
@@ -948,6 +977,10 @@ module.exports = function (RED) {
948
977
  }
949
978
  return pointName;
950
979
  }
980
+
981
+ function nodeWarn(message) {
982
+ node.warn(message);
983
+ }
951
984
  }
952
985
  RED.nodes.registerType("Bacnet-Gateway", BitpoolBacnetGatewayDevice);
953
986
  };