@bitpoolos/edge-bacnet 1.6.1 → 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,54 @@
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
+
29
+ ## [1.6.2] - 07-05-2025
30
+
31
+ Minor feature:
32
+ - Added "Enable device discovery" check box to gateway settings, discovery tab.
33
+ - This check box controls whether on not the auto point discovery and property discovery is enabled.
34
+ - This can be used to turn off unecessary network traffic once you have discovered all of the desired devices and points.
35
+ - IMPORTANT - if you are updating a existing deployment, the new property will be unticked, however new installs / dragging from the pallete will by default have the option checked. So if you want to keep this enabled on an existing deployment, please check the setting.
36
+ - Note - This does not turn off the whoIs task schedule.
37
+ - A user can use Right Click -> Update points on desired deviced in the read node tree if you would like to manually discover devices.
38
+
39
+ Minor update:
40
+ - Adjusted initial whoIs broadcast delay from 5seconds to 15seconds after node-red is started with a deployed gateway. This is to ensure large cached files are completely read and loaded.
41
+
42
+ Bug fixes:
43
+ - Inspector:
44
+ - Table resized to avoid scroll bars
45
+ - Percentage rounding to nearest 2 decimal places rather than whole integer for more detailed data.
46
+ - Loading animation added
47
+ - NAN years ago - removed as invalid time differential
48
+ - Last seen for device points adjusted to be more accurate
49
+
50
+
51
+
3
52
  ## [1.6.1] - 14-04-2025
4
53
 
5
54
  Bug fixes:
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;
@@ -52,6 +54,7 @@ class BacnetClient extends EventEmitter {
52
54
  that.deviceRetryCount = parseInt(config.retries);
53
55
  that.sanitise_device_schedule = config.sanitise_device_schedule;
54
56
  that.buildTreeException = false;
57
+ that.enable_device_discovery = config.enable_device_discovery;
55
58
 
56
59
  that.readPropertyMultipleOptions = {
57
60
  maxSegments: 112,
@@ -80,11 +83,11 @@ class BacnetClient extends EventEmitter {
80
83
 
81
84
  //query device task
82
85
  const queryDevices = new Task("simple task", () => {
83
- if (!that.pollInProgress) {
86
+ if (!that.pollInProgress && that.enable_device_discovery) {
84
87
  that.queryDevices();
85
88
  }
86
89
 
87
- if (!that.buildJsonInProgress) {
90
+ if (!that.buildJsonInProgress && that.enable_device_discovery) {
88
91
  that.buildJsonTree();
89
92
  }
90
93
  });
@@ -106,10 +109,15 @@ class BacnetClient extends EventEmitter {
106
109
  setTimeout(() => {
107
110
  that.globalWhoIs();
108
111
  setTimeout(() => {
109
- that.queryDevices();
110
- that.buildJsonTree();
112
+ if (!that.pollInProgress && that.enable_device_discovery) {
113
+ that.queryDevices();
114
+ }
115
+
116
+ if (!that.buildJsonInProgress && that.enable_device_discovery) {
117
+ that.buildJsonTree();
118
+ }
111
119
  }, "4000");
112
- }, "5000");
120
+ }, "15000");
113
121
 
114
122
  } catch (e) {
115
123
  that.logOut("Issue initializing client: ", e);
@@ -195,7 +203,8 @@ class BacnetClient extends EventEmitter {
195
203
  const cachedData = await Read_Config_Async();
196
204
  const parsedData = JSON.parse(cachedData);
197
205
  if (parsedData && typeof parsedData == "object") {
198
- 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;
199
208
  if (parsedData.deviceList) {
200
209
  parsedData.deviceList.forEach(function (device) {
201
210
  let newBacnetDevice = new BacnetDevice(true, device);
@@ -203,12 +212,13 @@ class BacnetClient extends EventEmitter {
203
212
  });
204
213
  }
205
214
  if (parsedData.pointList) that.networkTree = parsedData.pointList;
206
- 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;
207
217
  }
208
218
  }
209
219
  }
210
220
 
211
- testFunction(address, port, type, instance, property) {
221
+ testFunction(address, port, type, instance, property, nodeWarnCallback) {
212
222
  let that = this;
213
223
  console.log("test function ");
214
224
 
@@ -223,6 +233,11 @@ class BacnetClient extends EventEmitter {
223
233
  console.log("1 - readPropertyMultiple: ");
224
234
 
225
235
  console.log(value);
236
+
237
+ if (nodeWarnCallback) {
238
+ nodeWarnCallback(value);
239
+ }
240
+
226
241
  if (value) {
227
242
  // If the result has value, resolve the promise
228
243
  console.log(value.values[0]);
@@ -615,6 +630,7 @@ class BacnetClient extends EventEmitter {
615
630
  that.deviceId = config.deviceId;
616
631
  that.broadCastAddr = config.broadCastAddr;
617
632
  that.device_read_schedule = config.device_read_schedule;
633
+ that.enable_device_discovery = config.enable_device_discovery;
618
634
 
619
635
  if (that.scheduler !== null) {
620
636
  that.scheduler.stop();
@@ -640,11 +656,11 @@ class BacnetClient extends EventEmitter {
640
656
 
641
657
  // //query device task
642
658
  const queryDevices = new Task("simple task", () => {
643
- if (!that.pollInProgress) {
659
+ if (!that.pollInProgress && that.enable_device_discovery) {
644
660
  that.queryDevices();
645
661
  }
646
662
 
647
- if (!that.buildJsonInProgress) {
663
+ if (!that.buildJsonInProgress && that.enable_device_discovery) {
648
664
  that.buildJsonTree();
649
665
  }
650
666
  });
@@ -713,35 +729,30 @@ class BacnetClient extends EventEmitter {
713
729
  const that = this;
714
730
  const roundDecimal = readConfig.precision;
715
731
  const devicesToRead = Object.keys(readConfig.pointsToRead);
716
- const bacnetResults = {};
717
- let pendingRequests = 0;
732
+ let completedDevices = 0;
718
733
 
719
734
  try {
720
- // Process all devices in sequence
721
- for (let deviceIndex = 0; deviceIndex < devicesToRead.length; deviceIndex++) {
722
- const key = devicesToRead[deviceIndex];
735
+ // Create array of device processing promises
736
+ const devicePromises = devicesToRead.map(async (key, deviceIndex) => {
723
737
  const device = that.findDeviceByKey(key);
724
- if (!device) continue;
738
+ if (!device) return null;
725
739
 
726
740
  const deviceName = that.computeDeviceName(device);
727
741
  const deviceKey = that.createDeviceKey(device);
728
742
  const deviceObject = that.networkTree[deviceKey];
729
743
  const maxObjectCount = that.estimateMaxObjectSize(device.getMaxApdu());
730
744
 
731
- if (!bacnetResults[deviceName]) {
732
- bacnetResults[deviceName] = {};
733
- }
745
+ const bacnetResults = {};
746
+ bacnetResults[deviceName] = {};
734
747
 
735
748
  // Process points for the current device
736
749
  const pointsToRead = readConfig.pointsToRead[key];
737
750
  const pointNames = Object.keys(pointsToRead);
738
751
  let totalPoints = pointNames.length - 1;
739
752
  let requestArray = [];
740
- let processedPoints = 0; // Counter for processed points
741
753
 
742
754
  // Process each point for the device in batches
743
755
  for (let i = 0; i < pointNames.length; i++) {
744
-
745
756
  const pointName = pointNames[i];
746
757
  if (pointName === "deviceName") {
747
758
  continue;
@@ -764,17 +775,7 @@ class BacnetClient extends EventEmitter {
764
775
  }
765
776
 
766
777
  // Process the batch when the request array is full or the last point is reached
767
- if (requestArray.length === maxObjectCount) {
768
- if (device.getProtocolServiceSupport("ReadPropertyMultiple") == true) {
769
- await that.processBatch(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
770
- } else {
771
- await that.processIndividualPoints(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
772
- }
773
-
774
- requestArray = [];
775
- // Increment the processed points counter
776
- processedPoints += maxObjectCount;
777
- } else if (i === pointNames.length - 1) {
778
+ if (requestArray.length === maxObjectCount || i === pointNames.length - 1) {
778
779
  if (device.getProtocolServiceSupport("ReadPropertyMultiple") == true) {
779
780
  await that.processBatch(device, requestArray, deviceName, bacnetResults, that, roundDecimal);
780
781
  } else {
@@ -782,28 +783,43 @@ class BacnetClient extends EventEmitter {
782
783
  }
783
784
 
784
785
  requestArray = [];
785
- // Increment the processed points counter
786
- processedPoints += i;
787
786
  }
787
+ }
788
788
 
789
- // Check if all points for the device have been processed
790
- if (processedPoints >= totalPoints) {
791
- pendingRequests++;
792
-
793
- // Emit the `values` event for the current device
794
- that.emit(
795
- "values",
796
- bacnetResults,
797
- outputType,
798
- objectPropertyType,
799
- readNodeName,
800
- pendingRequests,
801
- devicesToRead.length
802
- );
803
- delete bacnetResults[deviceName];
804
- }
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);
805
820
  }
806
- }
821
+ });
822
+
807
823
  } catch (error) {
808
824
  that.logOut("doRead error: ", error);
809
825
  }
@@ -840,6 +856,7 @@ class BacnetClient extends EventEmitter {
840
856
  if (isNumber(val)) {
841
857
  pointRef.presentValue = roundDecimalPlaces(val, roundDecimal);
842
858
  pointRef.error = "none";
859
+ pointRef.status = "online";
843
860
  if (pointRef.meta.objectId.type == 19 || pointRef.meta.objectId.type == 13 || pointRef.meta.objectId.type == 14) {
844
861
  if (pointRef.stateTextArray && typeof pointRef.stateTextArray[0].value !== "object") {
845
862
  if (val != 0) {
@@ -853,14 +870,18 @@ class BacnetClient extends EventEmitter {
853
870
  if (typeof val !== "object") {
854
871
  pointRef.presentValue = val;
855
872
  pointRef.error = "none";
873
+ pointRef.status = "online";
856
874
  } else if (val.errorClass && val.errorClass) {
857
875
  pointRef.error = getBacnetErrorString(val.errorClass, val.errorClass);
876
+ pointRef.status = "offline";
877
+ } else {
878
+ pointRef.error = "none";
879
+ pointRef.status = "online";
858
880
  }
859
881
  }
860
882
 
861
883
  pointRef.meta["device"] = deviceMetaInfo;
862
884
  pointRef.timestamp = Date.now();
863
- pointRef.status = "online";
864
885
 
865
886
  // Store the point data in results
866
887
  bacnetResults[deviceName][pointNameRef] = pointRef;
@@ -892,6 +913,8 @@ class BacnetClient extends EventEmitter {
892
913
 
893
914
  if (isNumber(val)) {
894
915
  pointRef.presentValue = roundDecimalPlaces(val, roundDecimal);
916
+ pointRef.error = "none";
917
+ pointRef.status = "online";
895
918
 
896
919
  if (pointRef.meta.objectId.type == 19 || pointRef.meta.objectId.type == 13 || pointRef.meta.objectId.type == 14) {
897
920
  if (pointRef.stateTextArray && typeof pointRef.stateTextArray[0].value !== "object") {
@@ -903,13 +926,21 @@ class BacnetClient extends EventEmitter {
903
926
  }
904
927
  }
905
928
  } else {
906
- 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
+ }
907
940
  }
908
941
 
909
942
  pointRef.meta["device"] = deviceMetaInfo;
910
943
  pointRef.timestamp = Date.now();
911
- pointRef.status = "online";
912
- pointRef.error = "none";
913
944
 
914
945
  // Store the point data in results
915
946
  bacnetResults[deviceName][pointName] = pointRef;
@@ -1674,7 +1705,7 @@ class BacnetClient extends EventEmitter {
1674
1705
  if (json.body.renderListCount) {
1675
1706
  that.renderListCount = json.body.renderListCount;
1676
1707
  }
1677
- resolve();
1708
+ resolve(true);
1678
1709
  } catch (e) {
1679
1710
  reject(e);
1680
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",
@@ -177,6 +187,7 @@
177
187
  sanitise_device_schedule: { value: "60", required: false },
178
188
  sanitise_device_schedule_value: { value: "1", required: false },
179
189
  sanitise_device_schedule_options: { value: "Hours", required: false },
190
+ enable_device_discovery: { value: true, required: true },
180
191
  },
181
192
  networkInterfaces: [],
182
193
  inputs: 1,
@@ -395,25 +406,130 @@
395
406
  });
396
407
  });
397
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
+
398
442
  //Import complete Data model
399
443
  $("#file-upload-database").on("change", function (event) {
400
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
+
401
471
  const reader = new FileReader();
402
472
 
403
- reader.onload = function (e) {
404
- const text = e.target.result;
473
+ reader.onloadstart = function () {
474
+ setDataModelUpdateStatus("Reading file...", "info");
475
+ };
405
476
 
406
- 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
+ };
407
483
 
408
- $.ajax({
409
- type: "POST",
410
- url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/updateDataModel",
411
- dataType: "json",
412
- contentType: "application/json",
413
- data: JSON.stringify(jsonPayload),
414
- success: function (result) { },
415
- timeout: 0,
416
- });
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('');
417
533
  };
418
534
 
419
535
  reader.readAsText(input);
@@ -421,16 +537,82 @@
421
537
 
422
538
  // Export complete Data model
423
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
+
424
550
  $.ajax({
425
551
  url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/getDataModel",
426
552
  success: function (deviceList) {
427
- let data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(deviceList));
428
- let aEle = document.getElementById("exportJSON");
429
- aEle.setAttribute("href", "data:" + data);
430
- aEle.setAttribute("download", "edge-bacnet-datastore.json");
431
- 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
+ }
432
591
  },
433
- 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
434
616
  });
435
617
  });
436
618
 
@@ -817,6 +999,39 @@
817
999
  .database-file-label-div {
818
1000
  padding-top: 15px;
819
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
+ }
820
1035
  </style>
821
1036
 
822
1037
  <div class="form-row node-input-read-tabs-row">
@@ -985,6 +1200,11 @@
985
1200
  </select>
986
1201
  </div>
987
1202
 
1203
+ <div class="form-row bp-checkbox-row">
1204
+ <input type="checkbox" id="node-input-enable_device_discovery" class="bp-checkbox" />
1205
+ <label for="node-input-enable_device_discovery"> Enable device discovery </label>
1206
+ </div>
1207
+
988
1208
  <div class="form-row bp-checkbox-row">
989
1209
  <input type="checkbox" id="node-input-cacheFileEnabled" class="bp-checkbox" />
990
1210
  <label for="node-input-cacheFileEnabled"> Enable cache file </label>
@@ -1064,6 +1284,7 @@
1064
1284
  <input id="file-export-database" class="inputStyle" style="width: 258px; display: none;" />
1065
1285
  <a id="exportJSON" style="display: none"></a>
1066
1286
  </div>
1287
+ <div id="data-model-update-status" class="data-model-update-status" style="margin-top: 10px;"></div>
1067
1288
  </div>
1068
1289
  </div>
1069
1290
  </script>
@@ -1100,6 +1321,7 @@
1100
1321
  the this bacnet client will enter into manual discovery mode, where it iterates through types and instnace ranges. This
1101
1322
  range can be used to limit this manual scanning
1102
1323
  </li>
1324
+ <li>Enable device discovery - toggles whether automatic device discovery, object scanning, and tree building is enabled.</li>
1103
1325
  <li>Log Device Found - toggles logging of found devices to the node-red debug tab.</li>
1104
1326
  <li>Log BACnet Errors to Console - toggles logging of BACnet related errors to the node-red console</li>
1105
1327
  </ul>