@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 +49 -0
- package/bacnet_client.js +87 -56
- package/bacnet_gateway.html +240 -18
- package/bacnet_gateway.js +46 -11
- package/bacnet_inspector.js +88 -12
- package/bacnet_inspector_worker.js +15 -5
- package/bacnet_read.html +229 -8
- package/bacnet_read.js +13 -1
- package/common.js +7 -5
- package/inspector.html +60 -13
- package/package.json +1 -1
- package/resources/downloadAsHtml.js +8 -8
- package/resources/inspectorStyles.css +33 -21
- package/resources/style.css +28 -1
- package/ssrHtmlExporter.js +8 -8
- package/treeBuilder.js +80 -5
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.
|
|
110
|
-
|
|
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
|
-
}, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
717
|
-
let pendingRequests = 0;
|
|
732
|
+
let completedDevices = 0;
|
|
718
733
|
|
|
719
734
|
try {
|
|
720
|
-
//
|
|
721
|
-
|
|
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)
|
|
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
|
-
|
|
732
|
-
|
|
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
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
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
|
}
|
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",
|
|
@@ -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.
|
|
404
|
-
|
|
473
|
+
reader.onloadstart = function () {
|
|
474
|
+
setDataModelUpdateStatus("Reading file...", "info");
|
|
475
|
+
};
|
|
405
476
|
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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>
|