@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/bacnet_gateway.js
CHANGED
|
@@ -37,6 +37,7 @@ module.exports = function (RED) {
|
|
|
37
37
|
this.portRangeRegisters = config.portRangeRegisters;
|
|
38
38
|
this.cacheFileEnabled = config.cacheFileEnabled;
|
|
39
39
|
this.sanitise_device_schedule = config.sanitise_device_schedule;
|
|
40
|
+
this.enable_device_discovery = config.enable_device_discovery;
|
|
40
41
|
|
|
41
42
|
//client and config store
|
|
42
43
|
this.bacnetConfig = nodeContext.get("bacnetConfig");
|
|
@@ -65,7 +66,8 @@ module.exports = function (RED) {
|
|
|
65
66
|
node.retries,
|
|
66
67
|
node.cacheFileEnabled,
|
|
67
68
|
node.sanitise_device_schedule,
|
|
68
|
-
node.portRangeRegisters.filter((ele) => ele.enabled === true)
|
|
69
|
+
node.portRangeRegisters.filter((ele) => ele.enabled === true),
|
|
70
|
+
node.enable_device_discovery
|
|
69
71
|
);
|
|
70
72
|
|
|
71
73
|
if (typeof node.bacnetClient !== "undefined") {
|
|
@@ -213,11 +215,14 @@ module.exports = function (RED) {
|
|
|
213
215
|
if (
|
|
214
216
|
node.bacnetServerEnabled == true &&
|
|
215
217
|
node.bacnetClient &&
|
|
216
|
-
node.bacnetServer
|
|
217
|
-
nodeContext.get("serverWritePropEvent") == false
|
|
218
|
+
node.bacnetServer
|
|
218
219
|
) {
|
|
219
220
|
try {
|
|
220
|
-
|
|
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) => {
|
|
221
226
|
let formattedTopic = topic;
|
|
222
227
|
if (
|
|
223
228
|
node.nodeName !== "gateway" &&
|
|
@@ -232,8 +237,9 @@ module.exports = function (RED) {
|
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
node.send({ payload: newValue, topic: formattedTopic });
|
|
235
|
-
}
|
|
236
|
-
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
node.bacnetServer.on("writeProperty", node.writePropertyHandler);
|
|
237
243
|
} catch (e) {
|
|
238
244
|
console.log("Bacnet gateway node server writePoperty error: ", e);
|
|
239
245
|
}
|
|
@@ -263,12 +269,12 @@ module.exports = function (RED) {
|
|
|
263
269
|
} else if (msg.doUpdatePriorityDevices == true && msg.priorityDevices !== null) {
|
|
264
270
|
node.bacnetClient
|
|
265
271
|
.updatePriorityQueue(msg.priorityDevices)
|
|
266
|
-
.then(function (result) {})
|
|
272
|
+
.then(function (result) { })
|
|
267
273
|
.catch(function (error) {
|
|
268
274
|
logOut("Error updating priorityQueue: ", error);
|
|
269
275
|
});
|
|
270
276
|
} else if (msg.testFunc == true) {
|
|
271
|
-
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);
|
|
272
278
|
} else if (msg.applyDisplayNames) {
|
|
273
279
|
node.status({ fill: "blue", shape: "dot", text: "Updating display names" });
|
|
274
280
|
setTimeout(() => {
|
|
@@ -277,7 +283,7 @@ module.exports = function (RED) {
|
|
|
277
283
|
|
|
278
284
|
node.bacnetClient
|
|
279
285
|
.applyDisplayNames(msg.pointsToRead)
|
|
280
|
-
.then(function (result) {})
|
|
286
|
+
.then(function (result) { })
|
|
281
287
|
.catch(function (error) {
|
|
282
288
|
logOut("Error in applyDisplayNames: ", error);
|
|
283
289
|
});
|
|
@@ -298,7 +304,14 @@ module.exports = function (RED) {
|
|
|
298
304
|
});
|
|
299
305
|
|
|
300
306
|
node.on("close", function () {
|
|
301
|
-
//
|
|
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);
|
|
302
315
|
});
|
|
303
316
|
} catch (e) {
|
|
304
317
|
console.log("Bacnet node event handler error: ", e);
|
|
@@ -459,7 +472,7 @@ module.exports = function (RED) {
|
|
|
459
472
|
node.bacnetClient
|
|
460
473
|
.updateDataModel(req)
|
|
461
474
|
.then(function (result) {
|
|
462
|
-
res.send(
|
|
475
|
+
res.send(true);
|
|
463
476
|
})
|
|
464
477
|
.catch(function (error) {
|
|
465
478
|
res.send(error);
|
|
@@ -558,6 +571,24 @@ module.exports = function (RED) {
|
|
|
558
571
|
}
|
|
559
572
|
});
|
|
560
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
|
+
|
|
561
592
|
//route handler for importReadList
|
|
562
593
|
RED.httpAdmin.post("/bitpool-bacnet-data/importReadList", function (req, res) {
|
|
563
594
|
if (!node.bacnetClient) {
|
|
@@ -946,6 +977,10 @@ module.exports = function (RED) {
|
|
|
946
977
|
}
|
|
947
978
|
return pointName;
|
|
948
979
|
}
|
|
980
|
+
|
|
981
|
+
function nodeWarn(message) {
|
|
982
|
+
node.warn(message);
|
|
983
|
+
}
|
|
949
984
|
}
|
|
950
985
|
RED.nodes.registerType("Bacnet-Gateway", BitpoolBacnetGatewayDevice);
|
|
951
986
|
};
|
package/bacnet_inspector.js
CHANGED
|
@@ -61,9 +61,41 @@ module.exports = function (RED) {
|
|
|
61
61
|
site_Name: false,
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
// Function to update node status with combined information
|
|
65
|
+
function updateNodeStatus() {
|
|
66
|
+
// Calculate offline percentage for display
|
|
67
|
+
const totalPolledPoints = cachedData.onlineCount + cachedData.offlineCount;
|
|
68
|
+
const offlinePercentage = totalPolledPoints > 0 ?
|
|
69
|
+
Math.round((cachedData.offlineCount / totalPolledPoints) * 100) : 0;
|
|
70
|
+
|
|
71
|
+
// Build comprehensive status text
|
|
72
|
+
const statusParts = [];
|
|
73
|
+
|
|
74
|
+
// Add online/offline info if we have polled points
|
|
75
|
+
if (totalPolledPoints > 0) {
|
|
76
|
+
statusParts.push(`Online: ${cachedData.onlineCount}/${totalPolledPoints} (${100 - offlinePercentage}%)`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add points to read info if we have read list
|
|
80
|
+
if (cachedData.totalUniqueReadCount > 0) {
|
|
81
|
+
statusParts.push(`Total Points: ${cachedData.totalUniqueReadCount}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fallback status if no data
|
|
85
|
+
if (statusParts.length === 0) {
|
|
86
|
+
statusParts.push("No data");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update node status
|
|
90
|
+
node.status({ text: statusParts.join(" | ") });
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
// Initialize cache from flow context
|
|
65
94
|
initializeCache();
|
|
66
95
|
|
|
96
|
+
// Set initial status
|
|
97
|
+
updateNodeStatus();
|
|
98
|
+
|
|
67
99
|
function initializeCache() {
|
|
68
100
|
let flow = context.flow;
|
|
69
101
|
|
|
@@ -187,21 +219,31 @@ module.exports = function (RED) {
|
|
|
187
219
|
} else if (msg.type === "Read") {
|
|
188
220
|
calculateCombinedReadList(node, msg);
|
|
189
221
|
if (done) done();
|
|
190
|
-
} else if (msg.payload && msg.payload.error !== undefined && msg.payload.error !== "none") {
|
|
191
|
-
// bacnet error msg found
|
|
192
|
-
setErrorTopics(msg);
|
|
193
|
-
if (done) done();
|
|
194
222
|
} else if (msg.payload && msg.topic) {
|
|
195
|
-
//regular bacnet output
|
|
223
|
+
//regular bacnet output (including those with errors)
|
|
196
224
|
// Queue the message for batch processing instead of immediate processing
|
|
197
225
|
messageQueue.push({ msg, send, done });
|
|
198
226
|
if (messageQueue.length >= MAX_BATCH_SIZE) {
|
|
199
227
|
processBatch();
|
|
200
228
|
}
|
|
229
|
+
|
|
230
|
+
// Also handle error tracking for messages with errors or offline status
|
|
231
|
+
if ((msg.payload.error !== undefined && msg.payload.error !== "none") ||
|
|
232
|
+
(msg.payload.status !== undefined && msg.payload.status === "offline")) {
|
|
233
|
+
setErrorTopics(msg);
|
|
234
|
+
}
|
|
201
235
|
} else if (msg.type === "sendMqttStats") {
|
|
202
236
|
// Make sure we have the latest statBlock values before sending stats
|
|
203
237
|
syncStatBlockWithWorkerResults().then(() => {
|
|
204
238
|
let statBlock = node.statBlock;
|
|
239
|
+
let statCounts = node.statCounts || {};
|
|
240
|
+
|
|
241
|
+
// Calculate appropriate totals for percentage calculations
|
|
242
|
+
const readCount = statCounts.readCount || 0;
|
|
243
|
+
const discoveryCount = statCounts.discoveryCount || 0;
|
|
244
|
+
const totalDevices = statCounts.totalDevices || 1; // Fallback to 1 to prevent division by zero
|
|
245
|
+
|
|
246
|
+
// Send raw values
|
|
205
247
|
for (let key in statBlock) {
|
|
206
248
|
let value = statBlock[key];
|
|
207
249
|
let keyText = key.toUpperCase();
|
|
@@ -211,6 +253,35 @@ module.exports = function (RED) {
|
|
|
211
253
|
};
|
|
212
254
|
node.send(newMsg);
|
|
213
255
|
}
|
|
256
|
+
|
|
257
|
+
// Send percentage values
|
|
258
|
+
for (let key in statBlock) {
|
|
259
|
+
let rawValue = statBlock[key];
|
|
260
|
+
let percentage = 0;
|
|
261
|
+
|
|
262
|
+
// Calculate percentage based on appropriate denominator (rounded to 2 decimal places)
|
|
263
|
+
if (key === 'unmapped') {
|
|
264
|
+
// Unmapped points are from discovery list
|
|
265
|
+
percentage = discoveryCount > 0 ? Math.round((rawValue / discoveryCount) * 10000) / 100 : 0;
|
|
266
|
+
} else if (key === 'offlinePercentage') {
|
|
267
|
+
// Already a percentage, just round to 2 decimal places
|
|
268
|
+
percentage = Math.round(rawValue * 100) / 100;
|
|
269
|
+
} else if (key === 'deviceIdConflict') {
|
|
270
|
+
// Device conflicts as percentage of total devices
|
|
271
|
+
percentage = totalDevices > 0 ? Math.round((rawValue / totalDevices) * 10000) / 100 : 0;
|
|
272
|
+
} else {
|
|
273
|
+
// All other stats (ok, error, missing, warnings, moved, deviceIdChange) are based on read list
|
|
274
|
+
percentage = readCount > 0 ? Math.round((rawValue / readCount) * 10000) / 100 : 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let keyText = key.toUpperCase();
|
|
278
|
+
let percentageMsg = {
|
|
279
|
+
topic: `EDGE_DEVICE_${node.siteName}/BACNETSTATS/${keyText}PERCENTAGE`,
|
|
280
|
+
payload: percentage,
|
|
281
|
+
};
|
|
282
|
+
node.send(percentageMsg);
|
|
283
|
+
}
|
|
284
|
+
|
|
214
285
|
if (done) done();
|
|
215
286
|
});
|
|
216
287
|
|
|
@@ -419,6 +490,10 @@ module.exports = function (RED) {
|
|
|
419
490
|
topicData.error = error;
|
|
420
491
|
entryChanged = true;
|
|
421
492
|
}
|
|
493
|
+
if (status !== undefined && topicData.status !== status) {
|
|
494
|
+
topicData.status = status;
|
|
495
|
+
entryChanged = true;
|
|
496
|
+
}
|
|
422
497
|
|
|
423
498
|
if (entryChanged) {
|
|
424
499
|
topicData.key = topicData.deviceID + ":" + topicData.objectType + ":" + topicData.objectInstance;
|
|
@@ -436,8 +511,8 @@ module.exports = function (RED) {
|
|
|
436
511
|
dirtyFlags.offlinePercentage = true;
|
|
437
512
|
}
|
|
438
513
|
|
|
439
|
-
// Update the node status
|
|
440
|
-
|
|
514
|
+
// Update the node status with combined information
|
|
515
|
+
updateNodeStatus();
|
|
441
516
|
|
|
442
517
|
// Periodically call getModelStats to keep model stats updated
|
|
443
518
|
// Use a debounce pattern to avoid calling it too frequently
|
|
@@ -581,6 +656,7 @@ module.exports = function (RED) {
|
|
|
581
656
|
function setErrorTopics(msg) {
|
|
582
657
|
let topic = msg.topic;
|
|
583
658
|
let error = msg.payload.error;
|
|
659
|
+
let status = msg.payload.status;
|
|
584
660
|
|
|
585
661
|
// Extract properties only if they exist
|
|
586
662
|
let deviceID = msg.payload.meta?.device?.deviceId;
|
|
@@ -595,7 +671,8 @@ module.exports = function (RED) {
|
|
|
595
671
|
? msg.payload.meta.device.address.address
|
|
596
672
|
: msg.payload.meta?.device?.address;
|
|
597
673
|
|
|
598
|
-
|
|
674
|
+
// Track entries with explicit errors or offline status
|
|
675
|
+
if ((error !== undefined && error !== "none") || (status !== undefined && status === "offline")) {
|
|
599
676
|
// Use the cache instead of direct flow context access
|
|
600
677
|
cachedData.entriesWithErrors.set(topic, {
|
|
601
678
|
deviceID: deviceID,
|
|
@@ -605,7 +682,7 @@ module.exports = function (RED) {
|
|
|
605
682
|
displayName: displayName,
|
|
606
683
|
deviceName: deviceName,
|
|
607
684
|
ipAddress: ipAddress,
|
|
608
|
-
error: error,
|
|
685
|
+
error: error || (status === "offline" ? "Point offline" : "N/A"),
|
|
609
686
|
});
|
|
610
687
|
|
|
611
688
|
// Mark as dirty so it will be synced to flow context
|
|
@@ -771,12 +848,11 @@ module.exports = function (RED) {
|
|
|
771
848
|
// Force sync with flow context to ensure data is immediately available
|
|
772
849
|
syncWithFlowContext();
|
|
773
850
|
|
|
774
|
-
// Update the node status
|
|
775
|
-
|
|
851
|
+
// Update the node status with combined information
|
|
852
|
+
updateNodeStatus();
|
|
776
853
|
}
|
|
777
854
|
|
|
778
855
|
function getPublishedPointsList() {
|
|
779
|
-
node.warn("Generating Published Points List...");
|
|
780
856
|
let flow = context.flow;
|
|
781
857
|
let now = new Date();
|
|
782
858
|
|
|
@@ -3,10 +3,10 @@ const { Worker } = require("worker_threads");
|
|
|
3
3
|
const path = require("path");
|
|
4
4
|
|
|
5
5
|
function getRelativeTime(timestamp) {
|
|
6
|
-
if (!timestamp) return "N/A";
|
|
6
|
+
if (!timestamp || isNaN(timestamp) || typeof timestamp !== 'number') return "N/A";
|
|
7
7
|
let now = Date.now();
|
|
8
8
|
let timeDiff = now - timestamp;
|
|
9
|
-
if (timeDiff < 0) return "
|
|
9
|
+
if (isNaN(timeDiff) || timeDiff < 0) return "Invalid timestamp";
|
|
10
10
|
|
|
11
11
|
let seconds = Math.floor(timeDiff / 1000);
|
|
12
12
|
let minutes = Math.floor(seconds / 60);
|
|
@@ -149,6 +149,7 @@ function processModelStats(data) {
|
|
|
149
149
|
// Store the data under all variations
|
|
150
150
|
const publishData = {
|
|
151
151
|
error: data.error || "N/A",
|
|
152
|
+
status: data.status || "N/A",
|
|
152
153
|
presentValue: data.presentValue,
|
|
153
154
|
bacnetLastSeen: data.bacnetLastSeen
|
|
154
155
|
};
|
|
@@ -232,15 +233,24 @@ function processModelStats(data) {
|
|
|
232
233
|
|
|
233
234
|
if (DiscoveryKeyMatch) {
|
|
234
235
|
if (PublishPointTopicMatch) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
const publishData = PublishTopicsNormalized.get(matchVariation);
|
|
237
|
+
const error = publishData.error;
|
|
238
|
+
const status = publishData.status;
|
|
239
|
+
|
|
240
|
+
// Check if point has error or is offline
|
|
241
|
+
if ((error !== "none" && error !== "N/A") || status === "offline") {
|
|
242
|
+
if (status === "offline") {
|
|
243
|
+
PointResult.dataModelStatus = `Point Error - Matched in Discovery / Data Model and publishing, however point status is offline`;
|
|
244
|
+
} else {
|
|
245
|
+
PointResult.dataModelStatus = `Point Error - Matched in Discovery / Data Model and publishing, however BACNet Error is present: ${error}`;
|
|
246
|
+
}
|
|
238
247
|
statBlock.error++;
|
|
239
248
|
} else {
|
|
240
249
|
PointResult.dataModelStatus = "Point Ok - Matched in Discovery / Data Model and publishing";
|
|
241
250
|
statBlock.ok++;
|
|
242
251
|
}
|
|
243
252
|
PointResult.error = error;
|
|
253
|
+
PointResult.status = status; // Also store the status for reference
|
|
244
254
|
pointOkCount++;
|
|
245
255
|
} else {
|
|
246
256
|
if (DiscoveryPointMap[ReadPointKey].objectName !== ReadPointObj.pointName) {
|