@bitpoolos/edge-bacnet 1.6.2 → 1.6.4

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.
@@ -136,6 +136,16 @@
136
136
  body: JSON.stringify({ k: deviceKey, p: pointName, n: pointDisplayName }),
137
137
  }).then((res) => res.json());
138
138
  }
139
+ applyDisplayNames(pointsToRead) {
140
+ return fetch(RED.settings.httpNodeRoot + "bitpool-bacnet-data/applyDisplayNames", {
141
+ method: "POST",
142
+ headers: {
143
+ Accept: "application/json",
144
+ "Content-Type": "application/json",
145
+ },
146
+ body: JSON.stringify({ pointsToRead: pointsToRead }),
147
+ }).then((res) => res.json());
148
+ }
139
149
  importReadList(payload) {
140
150
  return fetch(RED.settings.httpNodeRoot + "bitpool-bacnet-data/importReadList", {
141
151
  method: "POST",
@@ -396,25 +406,130 @@
396
406
  });
397
407
  });
398
408
 
409
+ function setDataModelUpdateStatus(status, type, autoClear) {
410
+ const statusElement = $("#data-model-update-status");
411
+ statusElement.text(status);
412
+
413
+ // Remove existing status classes
414
+ statusElement.removeClass("status-success status-error status-warning status-info");
415
+
416
+ // Add appropriate styling based on type
417
+ switch (type) {
418
+ case "success":
419
+ statusElement.addClass("status-success");
420
+ break;
421
+ case "error":
422
+ statusElement.addClass("status-error");
423
+ break;
424
+ case "warning":
425
+ statusElement.addClass("status-warning");
426
+ break;
427
+ case "info":
428
+ default:
429
+ statusElement.addClass("status-info");
430
+ break;
431
+ }
432
+
433
+ // Auto-clear success messages after 5 seconds
434
+ if (autoClear !== false && type === "success") {
435
+ setTimeout(() => {
436
+ statusElement.text("");
437
+ statusElement.removeClass("status-success");
438
+ }, 5000);
439
+ }
440
+ }
441
+
399
442
  //Import complete Data model
400
443
  $("#file-upload-database").on("change", function (event) {
401
444
  const input = event.target.files[0];
445
+
446
+ if (!input) {
447
+ setDataModelUpdateStatus("No file selected", "warning");
448
+ return;
449
+ }
450
+
451
+ // Validate file type
452
+ if (!input.name.toLowerCase().endsWith('.json')) {
453
+ setDataModelUpdateStatus("Please select a valid JSON file", "error");
454
+ return;
455
+ }
456
+
457
+ // Check file size (warn if larger than 10MB)
458
+ const fileSizeMB = (input.size / (1024 * 1024)).toFixed(2);
459
+ if (input.size > 10 * 1024 * 1024) {
460
+ setDataModelUpdateStatus(`Large file detected (${fileSizeMB}MB). Import may take several minutes...`, "warning");
461
+ }
462
+
463
+ // Confirmation dialog for potentially destructive operation
464
+ if (!confirm(`Are you sure you want to import this data model?\n\nFile: ${input.name}\nSize: ${fileSizeMB}MB\n\nThis will replace the existing data model and cannot be undone.`)) {
465
+ // Reset the file input
466
+ $(this).val('');
467
+ setDataModelUpdateStatus("Import cancelled", "info");
468
+ return;
469
+ }
470
+
402
471
  const reader = new FileReader();
403
472
 
404
- reader.onload = function (e) {
405
- const text = e.target.result;
473
+ reader.onloadstart = function () {
474
+ setDataModelUpdateStatus("Reading file...", "info");
475
+ };
406
476
 
407
- let jsonPayload = JSON.parse(text);
477
+ reader.onprogress = function (e) {
478
+ if (e.lengthComputable) {
479
+ const percentLoaded = Math.round((e.loaded / e.total) * 100);
480
+ setDataModelUpdateStatus(`Reading file... ${percentLoaded}%`, "info");
481
+ }
482
+ };
408
483
 
409
- $.ajax({
410
- type: "POST",
411
- url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/updateDataModel",
412
- dataType: "json",
413
- contentType: "application/json",
414
- data: JSON.stringify(jsonPayload),
415
- success: function (result) { },
416
- timeout: 0,
417
- });
484
+ reader.onload = function (e) {
485
+ setDataModelUpdateStatus("Parsing JSON data...", "info");
486
+
487
+ try {
488
+ const text = e.target.result;
489
+ let jsonPayload = JSON.parse(text);
490
+
491
+ setDataModelUpdateStatus("Uploading data model to server...", "info");
492
+
493
+ $.ajax({
494
+ type: "POST",
495
+ url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/updateDataModel",
496
+ dataType: "json",
497
+ contentType: "application/json",
498
+ data: JSON.stringify(jsonPayload),
499
+ success: function (data, status, xhr) {
500
+ setDataModelUpdateStatus(`Data model imported successfully! (${fileSizeMB}MB processed)`, "success");
501
+ // Clear the file input
502
+ $("#file-upload-database").val('');
503
+ },
504
+ error: function (xhr, status, error) {
505
+ let errorMsg = "Failed to import data model";
506
+ if (xhr.responseText) {
507
+ try {
508
+ const errorResponse = JSON.parse(xhr.responseText);
509
+ errorMsg += ": " + (errorResponse.message || errorResponse.error || xhr.responseText);
510
+ } catch (e) {
511
+ errorMsg += ": " + xhr.responseText;
512
+ }
513
+ } else {
514
+ errorMsg += ": " + (error || status || "Unknown error");
515
+ }
516
+ setDataModelUpdateStatus(errorMsg, "error");
517
+ // Clear the file input
518
+ $("#file-upload-database").val('');
519
+ },
520
+ timeout: 0,
521
+ });
522
+ } catch (parseError) {
523
+ setDataModelUpdateStatus("Invalid JSON file: " + parseError.message, "error");
524
+ // Clear the file input
525
+ $("#file-upload-database").val('');
526
+ }
527
+ };
528
+
529
+ reader.onerror = function () {
530
+ setDataModelUpdateStatus("Error reading file", "error");
531
+ // Clear the file input
532
+ $("#file-upload-database").val('');
418
533
  };
419
534
 
420
535
  reader.readAsText(input);
@@ -422,16 +537,82 @@
422
537
 
423
538
  // Export complete Data model
424
539
  $("#file-export-database").click(function (params) {
540
+ // Confirmation dialog
541
+ if (!confirm("Are you sure you want to export the complete data model?\n\nThis may take some time for large databases.")) {
542
+ setDataModelUpdateStatus("Export cancelled", "info");
543
+ return;
544
+ }
545
+
546
+ setDataModelUpdateStatus("Retrieving data model from server...", "info");
547
+
548
+ const startTime = Date.now();
549
+
425
550
  $.ajax({
426
551
  url: RED.settings.httpNodeRoot + "bitpool-bacnet-data/getDataModel",
427
552
  success: function (deviceList) {
428
- let data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(deviceList));
429
- let aEle = document.getElementById("exportJSON");
430
- aEle.setAttribute("href", "data:" + data);
431
- aEle.setAttribute("download", "edge-bacnet-datastore.json");
432
- aEle.click();
553
+ try {
554
+ setDataModelUpdateStatus("Preparing download...", "info");
555
+
556
+ // Calculate data size
557
+ const jsonString = JSON.stringify(deviceList);
558
+ const dataSize = new Blob([jsonString]).size;
559
+ const dataSizeMB = (dataSize / (1024 * 1024)).toFixed(2);
560
+
561
+ // Create a Blob with the JSON data
562
+ const blob = new Blob([jsonString], { type: 'application/json' });
563
+
564
+ // Create an object URL from the Blob
565
+ const url = URL.createObjectURL(blob);
566
+
567
+ // Generate filename with timestamp
568
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
569
+ const filename = `edge-bacnet-datastore-${timestamp}.json`;
570
+
571
+ // Set the URL to the link and trigger download
572
+ let aEle = document.getElementById("exportJSON");
573
+ aEle.setAttribute("href", url);
574
+ aEle.setAttribute("download", filename);
575
+ aEle.click();
576
+
577
+ // Calculate duration
578
+ const duration = ((Date.now() - startTime) / 1000).toFixed(1);
579
+
580
+ setDataModelUpdateStatus(`Export completed successfully! (${dataSizeMB}MB, ${duration}s)`, "success");
581
+
582
+ // Release the object URL when done
583
+ setTimeout(() => {
584
+ URL.revokeObjectURL(url);
585
+ }, 2000);
586
+
587
+ } catch (error) {
588
+ console.error("Error exporting data model:", error);
589
+ setDataModelUpdateStatus("Failed to create download file: " + error.message, "error");
590
+ }
433
591
  },
434
- timeout: 0,
592
+ error: function (xhr, status, error) {
593
+ console.error("AJAX error retrieving data model:", status, error);
594
+ let errorMsg = "Failed to retrieve data model from server";
595
+
596
+ if (xhr.status === 0) {
597
+ errorMsg += " (Connection timeout or server unavailable)";
598
+ } else if (xhr.status === 404) {
599
+ errorMsg += " (Endpoint not found)";
600
+ } else if (xhr.status === 500) {
601
+ errorMsg += " (Internal server error)";
602
+ } else if (xhr.responseText) {
603
+ try {
604
+ const errorResponse = JSON.parse(xhr.responseText);
605
+ errorMsg += ": " + (errorResponse.message || errorResponse.error);
606
+ } catch (e) {
607
+ errorMsg += ": " + xhr.responseText;
608
+ }
609
+ } else {
610
+ errorMsg += ": " + (error || status || "Unknown error");
611
+ }
612
+
613
+ setDataModelUpdateStatus(errorMsg, "error");
614
+ },
615
+ timeout: 60000, // 60 second timeout for large exports
435
616
  });
436
617
  });
437
618
 
@@ -818,6 +999,39 @@
818
999
  .database-file-label-div {
819
1000
  padding-top: 15px;
820
1001
  }
1002
+ /* Status indicator styles */
1003
+ .status-success {
1004
+ color: #28a745 !important;
1005
+ font-weight: 500;
1006
+ background-color: #d4edda;
1007
+ border: 1px solid #c3e6cb;
1008
+ border-radius: 4px;
1009
+ padding: 8px 12px;
1010
+ }
1011
+ .status-error {
1012
+ color: #dc3545 !important;
1013
+ font-weight: 500;
1014
+ background-color: #f8d7da;
1015
+ border: 1px solid #f5c6cb;
1016
+ border-radius: 4px;
1017
+ padding: 8px 12px;
1018
+ }
1019
+ .status-warning {
1020
+ color: #856404 !important;
1021
+ font-weight: 500;
1022
+ background-color: #fff3cd;
1023
+ border: 1px solid #ffeaa7;
1024
+ border-radius: 4px;
1025
+ padding: 8px 12px;
1026
+ }
1027
+ .status-info {
1028
+ color: #0c5460 !important;
1029
+ font-weight: 500;
1030
+ background-color: #d1ecf1;
1031
+ border: 1px solid #bee5eb;
1032
+ border-radius: 4px;
1033
+ padding: 8px 12px;
1034
+ }
821
1035
  </style>
822
1036
 
823
1037
  <div class="form-row node-input-read-tabs-row">
@@ -1070,6 +1284,7 @@
1070
1284
  <input id="file-export-database" class="inputStyle" style="width: 258px; display: none;" />
1071
1285
  <a id="exportJSON" style="display: none"></a>
1072
1286
  </div>
1287
+ <div id="data-model-update-status" class="data-model-update-status" style="margin-top: 10px;"></div>
1073
1288
  </div>
1074
1289
  </div>
1075
1290
  </script>
package/bacnet_gateway.js CHANGED
@@ -215,11 +215,14 @@ module.exports = function (RED) {
215
215
  if (
216
216
  node.bacnetServerEnabled == true &&
217
217
  node.bacnetClient &&
218
- node.bacnetServer &&
219
- nodeContext.get("serverWritePropEvent") == false
218
+ node.bacnetServer
220
219
  ) {
221
220
  try {
222
- node.bacnetServer.on("writeProperty", (topic, newValue) => {
221
+ // Clean up any existing listeners to prevent stale references
222
+ node.bacnetServer.removeAllListeners('writeProperty');
223
+
224
+ // Store the event handler function so we can clean it up later
225
+ node.writePropertyHandler = (topic, newValue) => {
223
226
  let formattedTopic = topic;
224
227
  if (
225
228
  node.nodeName !== "gateway" &&
@@ -234,8 +237,9 @@ module.exports = function (RED) {
234
237
  }
235
238
 
236
239
  node.send({ payload: newValue, topic: formattedTopic });
237
- });
238
- nodeContext.set("serverWritePropEvent", true);
240
+ };
241
+
242
+ node.bacnetServer.on("writeProperty", node.writePropertyHandler);
239
243
  } catch (e) {
240
244
  console.log("Bacnet gateway node server writePoperty error: ", e);
241
245
  }
@@ -270,7 +274,7 @@ module.exports = function (RED) {
270
274
  logOut("Error updating priorityQueue: ", error);
271
275
  });
272
276
  } else if (msg.testFunc == true) {
273
- node.bacnetClient.testFunction(msg.address, msg.port, msg.type, msg.instance, msg.property);
277
+ node.bacnetClient.testFunction(msg.address, msg.port, msg.type, msg.instance, msg.property, nodeWarn);
274
278
  } else if (msg.applyDisplayNames) {
275
279
  node.status({ fill: "blue", shape: "dot", text: "Updating display names" });
276
280
  setTimeout(() => {
@@ -300,7 +304,14 @@ module.exports = function (RED) {
300
304
  });
301
305
 
302
306
  node.on("close", function () {
303
- //do nothing
307
+ // Clean up the writeProperty event listener
308
+ if (node.bacnetServer && node.writePropertyHandler) {
309
+ node.bacnetServer.removeListener('writeProperty', node.writePropertyHandler);
310
+ node.writePropertyHandler = null;
311
+ }
312
+
313
+ // Reset the serverWritePropEvent flag so it can be re-registered
314
+ nodeContext.set("serverWritePropEvent", false);
304
315
  });
305
316
  } catch (e) {
306
317
  console.log("Bacnet node event handler error: ", e);
@@ -461,7 +472,7 @@ module.exports = function (RED) {
461
472
  node.bacnetClient
462
473
  .updateDataModel(req)
463
474
  .then(function (result) {
464
- res.send(result);
475
+ res.send(true);
465
476
  })
466
477
  .catch(function (error) {
467
478
  res.send(error);
@@ -560,6 +571,24 @@ module.exports = function (RED) {
560
571
  }
561
572
  });
562
573
 
574
+ //route handler for applyDisplayNames
575
+ RED.httpAdmin.post("/bitpool-bacnet-data/applyDisplayNames", function (req, res) {
576
+ if (!node.bacnetClient) {
577
+ logOut("Issue with the bacnetClient while applying display names: ", node.bacnetClient);
578
+ res.send(false);
579
+ } else {
580
+ node.bacnetClient
581
+ .applyDisplayNames(req.body.pointsToRead)
582
+ .then(function (result) {
583
+ res.send(result);
584
+ })
585
+ .catch(function (error) {
586
+ res.send(error);
587
+ logOut("Error applying display names: ", error);
588
+ });
589
+ }
590
+ });
591
+
563
592
  //route handler for importReadList
564
593
  RED.httpAdmin.post("/bitpool-bacnet-data/importReadList", function (req, res) {
565
594
  if (!node.bacnetClient) {
@@ -948,6 +977,10 @@ module.exports = function (RED) {
948
977
  }
949
978
  return pointName;
950
979
  }
980
+
981
+ function nodeWarn(message) {
982
+ node.warn(message);
983
+ }
951
984
  }
952
985
  RED.nodes.registerType("Bacnet-Gateway", BitpoolBacnetGatewayDevice);
953
986
  };
@@ -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
- node.status({ text: "Points Online: " + cachedData.onlineCount + "/" + cachedData.totalUniquePolledCount });
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
- if (error !== undefined && error !== "none") {
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
- node.status({ text: "Points To Read: " + cachedData.totalUniqueReadCount });
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
 
@@ -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 error = PublishTopicsNormalized.get(matchVariation).error;
236
- if (error !== "none" && error !== "N/A") {
237
- PointResult.dataModelStatus = `Point Error - Matched in Discovery / Data Model and publishing, however BACNet Error is present: ${error}`;
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) {