@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 +26 -0
- package/bacnet_client.js +73 -49
- package/bacnet_gateway.html +233 -18
- package/bacnet_gateway.js +41 -8
- package/bacnet_inspector.js +88 -12
- package/bacnet_inspector_worker.js +13 -3
- package/bacnet_read.html +229 -8
- package/bacnet_read.js +13 -1
- package/inspector.html +35 -1
- package/package.json +1 -1
- package/resources/style.css +28 -1
- package/treeBuilder.js +80 -5
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
724
|
-
let pendingRequests = 0;
|
|
732
|
+
let completedDevices = 0;
|
|
725
733
|
|
|
726
734
|
try {
|
|
727
|
-
//
|
|
728
|
-
|
|
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)
|
|
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
|
-
|
|
739
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
}
|
package/bacnet_gateway.html
CHANGED
|
@@ -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.
|
|
405
|
-
|
|
473
|
+
reader.onloadstart = function () {
|
|
474
|
+
setDataModelUpdateStatus("Reading file...", "info");
|
|
475
|
+
};
|
|
406
476
|
|
|
407
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
};
|