@bitpoolos/edge-bacnet 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.2] - 07-05-2025
4
+
5
+ Minor feature:
6
+ - Added "Enable device discovery" check box to gateway settings, discovery tab.
7
+ - This check box controls whether on not the auto point discovery and property discovery is enabled.
8
+ - This can be used to turn off unecessary network traffic once you have discovered all of the desired devices and points.
9
+ - 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.
10
+ - Note - This does not turn off the whoIs task schedule.
11
+ - A user can use Right Click -> Update points on desired deviced in the read node tree if you would like to manually discover devices.
12
+
13
+ Minor update:
14
+ - 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.
15
+
16
+ Bug fixes:
17
+ - Inspector:
18
+ - Table resized to avoid scroll bars
19
+ - Percentage rounding to nearest 2 decimal places rather than whole integer for more detailed data.
20
+ - Loading animation added
21
+ - NAN years ago - removed as invalid time differential
22
+ - Last seen for device points adjusted to be more accurate
23
+
24
+
25
+
3
26
  ## [1.6.1] - 14-04-2025
4
27
 
5
28
  Bug fixes:
package/bacnet_client.js CHANGED
@@ -52,6 +52,7 @@ class BacnetClient extends EventEmitter {
52
52
  that.deviceRetryCount = parseInt(config.retries);
53
53
  that.sanitise_device_schedule = config.sanitise_device_schedule;
54
54
  that.buildTreeException = false;
55
+ that.enable_device_discovery = config.enable_device_discovery;
55
56
 
56
57
  that.readPropertyMultipleOptions = {
57
58
  maxSegments: 112,
@@ -80,11 +81,11 @@ class BacnetClient extends EventEmitter {
80
81
 
81
82
  //query device task
82
83
  const queryDevices = new Task("simple task", () => {
83
- if (!that.pollInProgress) {
84
+ if (!that.pollInProgress && that.enable_device_discovery) {
84
85
  that.queryDevices();
85
86
  }
86
87
 
87
- if (!that.buildJsonInProgress) {
88
+ if (!that.buildJsonInProgress && that.enable_device_discovery) {
88
89
  that.buildJsonTree();
89
90
  }
90
91
  });
@@ -106,10 +107,15 @@ class BacnetClient extends EventEmitter {
106
107
  setTimeout(() => {
107
108
  that.globalWhoIs();
108
109
  setTimeout(() => {
109
- that.queryDevices();
110
- that.buildJsonTree();
110
+ if (!that.pollInProgress && that.enable_device_discovery) {
111
+ that.queryDevices();
112
+ }
113
+
114
+ if (!that.buildJsonInProgress && that.enable_device_discovery) {
115
+ that.buildJsonTree();
116
+ }
111
117
  }, "4000");
112
- }, "5000");
118
+ }, "15000");
113
119
 
114
120
  } catch (e) {
115
121
  that.logOut("Issue initializing client: ", e);
@@ -615,6 +621,7 @@ class BacnetClient extends EventEmitter {
615
621
  that.deviceId = config.deviceId;
616
622
  that.broadCastAddr = config.broadCastAddr;
617
623
  that.device_read_schedule = config.device_read_schedule;
624
+ that.enable_device_discovery = config.enable_device_discovery;
618
625
 
619
626
  if (that.scheduler !== null) {
620
627
  that.scheduler.stop();
@@ -640,11 +647,11 @@ class BacnetClient extends EventEmitter {
640
647
 
641
648
  // //query device task
642
649
  const queryDevices = new Task("simple task", () => {
643
- if (!that.pollInProgress) {
650
+ if (!that.pollInProgress && that.enable_device_discovery) {
644
651
  that.queryDevices();
645
652
  }
646
653
 
647
- if (!that.buildJsonInProgress) {
654
+ if (!that.buildJsonInProgress && that.enable_device_discovery) {
648
655
  that.buildJsonTree();
649
656
  }
650
657
  });
@@ -177,6 +177,7 @@
177
177
  sanitise_device_schedule: { value: "60", required: false },
178
178
  sanitise_device_schedule_value: { value: "1", required: false },
179
179
  sanitise_device_schedule_options: { value: "Hours", required: false },
180
+ enable_device_discovery: { value: true, required: true },
180
181
  },
181
182
  networkInterfaces: [],
182
183
  inputs: 1,
@@ -985,6 +986,11 @@
985
986
  </select>
986
987
  </div>
987
988
 
989
+ <div class="form-row bp-checkbox-row">
990
+ <input type="checkbox" id="node-input-enable_device_discovery" class="bp-checkbox" />
991
+ <label for="node-input-enable_device_discovery"> Enable device discovery </label>
992
+ </div>
993
+
988
994
  <div class="form-row bp-checkbox-row">
989
995
  <input type="checkbox" id="node-input-cacheFileEnabled" class="bp-checkbox" />
990
996
  <label for="node-input-cacheFileEnabled"> Enable cache file </label>
@@ -1100,6 +1106,7 @@
1100
1106
  the this bacnet client will enter into manual discovery mode, where it iterates through types and instnace ranges. This
1101
1107
  range can be used to limit this manual scanning
1102
1108
  </li>
1109
+ <li>Enable device discovery - toggles whether automatic device discovery, object scanning, and tree building is enabled.</li>
1103
1110
  <li>Log Device Found - toggles logging of found devices to the node-red debug tab.</li>
1104
1111
  <li>Log BACnet Errors to Console - toggles logging of BACnet related errors to the node-red console</li>
1105
1112
  </ul>
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") {
@@ -263,7 +265,7 @@ module.exports = function (RED) {
263
265
  } else if (msg.doUpdatePriorityDevices == true && msg.priorityDevices !== null) {
264
266
  node.bacnetClient
265
267
  .updatePriorityQueue(msg.priorityDevices)
266
- .then(function (result) {})
268
+ .then(function (result) { })
267
269
  .catch(function (error) {
268
270
  logOut("Error updating priorityQueue: ", error);
269
271
  });
@@ -277,7 +279,7 @@ module.exports = function (RED) {
277
279
 
278
280
  node.bacnetClient
279
281
  .applyDisplayNames(msg.pointsToRead)
280
- .then(function (result) {})
282
+ .then(function (result) { })
281
283
  .catch(function (error) {
282
284
  logOut("Error in applyDisplayNames: ", error);
283
285
  });
@@ -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 "In the future";
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);
package/common.js CHANGED
@@ -65,7 +65,8 @@ class BacnetClientConfig {
65
65
  retries,
66
66
  cacheFileEnabled,
67
67
  sanitise_device_schedule,
68
- portRangeMatrix
68
+ portRangeMatrix,
69
+ enable_device_discovery
69
70
  ) {
70
71
  this.apduTimeout = apduTimeout;
71
72
  this.localIpAdrress = localIpAdrress;
@@ -84,6 +85,7 @@ class BacnetClientConfig {
84
85
  this.cacheFileEnabled = cacheFileEnabled;
85
86
  this.sanitise_device_schedule = sanitise_device_schedule;
86
87
  this.portRangeMatrix = this.generatePortRangeArray(portRangeMatrix);
88
+ this.enable_device_discovery = enable_device_discovery;
87
89
  }
88
90
 
89
91
  generatePortRangeArray(rangeMatrix) {
@@ -255,7 +257,7 @@ async function Store_Config(data) {
255
257
  JSON.parse(tempContent);
256
258
  } catch (verifyError) {
257
259
  console.error("Temporary file validation failed:", verifyError);
258
- await fs2.unlink(tempFile).catch(() => {});
260
+ await fs2.unlink(tempFile).catch(() => { });
259
261
  return false;
260
262
  }
261
263
 
@@ -275,8 +277,8 @@ async function Store_Config(data) {
275
277
 
276
278
  // Cleanup temporary file if it exists
277
279
  try {
278
- await fs2.unlink(tempFile).catch(() => {});
279
- } catch (cleanupError) {}
280
+ await fs2.unlink(tempFile).catch(() => { });
281
+ } catch (cleanupError) { }
280
282
 
281
283
  // If main file is corrupted and backup exists, restore from backup
282
284
  try {
@@ -396,7 +398,7 @@ async function Store_Config_Server(data) {
396
398
  //console.log("Store_Config_Server writeFile error: ", err);
397
399
  }
398
400
  });
399
- } catch (err) {}
401
+ } catch (err) { }
400
402
  }
401
403
 
402
404
  // READ CONFIG SYNC FUNCTION - BACNET SERVER ======================================
package/inspector.html CHANGED
@@ -133,9 +133,9 @@
133
133
  <div class="center">
134
134
  <p class="headertext">{{siteName}} - Point Status Display</p>
135
135
  </div>
136
- <p-datatable :value="displayData" paginator :rows="12" filterDisplay="menu" :filters="filters"
137
- v-model:filters="filters" :sortMode="'multiple'" @filter="onFilter" scrollable scrollHeight="800px"
138
- class="datatable fixed-height-table">
136
+ <p-datatable :value="displayData" paginator scrollable scrollHeight="800px" :rows="12" filterDisplay="menu"
137
+ :filters="filters" v-model:filters="filters" :sortMode="'multiple'" @filter="onFilter"
138
+ class="datatable fixed-height-table" :loading="loading">
139
139
  <template #header>
140
140
  <div class="tableHeaderDiv">
141
141
  <div style="margin-right: 5px">
@@ -177,6 +177,14 @@
177
177
  </div>
178
178
  </div>
179
179
  </template>
180
+ <template #loading>
181
+ <div class="loading-overlay">
182
+ <div class="loading-spinner">
183
+ <i class="pi pi-spin pi-spinner" style="font-size: 2rem"></i>
184
+ <p>Loading data...</p>
185
+ </div>
186
+ </div>
187
+ </template>
180
188
  <p-column v-for="col in visibleColumns" :key="col.field" :field="col.field" :header="col.header" sortable
181
189
  filter></p-column>
182
190
  <template #paginatorstart>
@@ -188,6 +196,7 @@
188
196
  </span>
189
197
  </template>
190
198
  </p-datatable>
199
+
191
200
  </div>
192
201
  </div>
193
202
  </div>
@@ -199,7 +208,7 @@
199
208
  data() {
200
209
  return {
201
210
  tableData: null,
202
- loading: false,
211
+ loading: true,
203
212
  totalRecords: 0,
204
213
  first: 0,
205
214
  lazyParams: {},
@@ -263,6 +272,7 @@
263
272
  this.observeHeaderHeight();
264
273
 
265
274
  this.selectedColumns = JSON.parse(JSON.stringify(this.allColumns));
275
+ this.loading = true;
266
276
 
267
277
  fetch("/getModelStats")
268
278
  .then((response) => {
@@ -288,19 +298,22 @@
288
298
  // Calculate percentages
289
299
  const total = tableData.length || 1; // Avoid division by zero
290
300
  app.statPercentages = {
291
- readCount: Math.round((data.statCounts.readCount / total) * 100),
292
- ok: Math.round((data.statCounts.statBlock.ok / total) * 100),
293
- error: Math.round((data.statCounts.statBlock.error / total) * 100),
294
- missing: Math.round((data.statCounts.statBlock.missing / total) * 100),
295
- warnings: Math.round((data.statCounts.statBlock.warnings / total) * 100),
296
- deviceIdChange: Math.round((data.statCounts.statBlock.deviceIdChange / total) * 100),
297
- deviceIdConflict: Math.round((data.statCounts.statBlock.deviceIdConflict / total) * 100),
298
- unmapped: Math.round((data.statCounts.statBlock.unmapped / total) * 100)
301
+ readCount: Math.round((data.statCounts.readCount / total) * 10000) / 100,
302
+ ok: Math.round((data.statCounts.statBlock.ok / total) * 10000) / 100,
303
+ error: Math.round((data.statCounts.statBlock.error / total) * 10000) / 100,
304
+ missing: Math.round((data.statCounts.statBlock.missing / total) * 10000) / 100,
305
+ warnings: Math.round((data.statCounts.statBlock.warnings / total) * 10000) / 100,
306
+ deviceIdChange: Math.round((data.statCounts.statBlock.deviceIdChange / total) * 10000) / 100,
307
+ deviceIdConflict: Math.round((data.statCounts.statBlock.deviceIdConflict / total) * 10000) / 100,
308
+ unmapped: Math.round((data.statCounts.statBlock.unmapped / total) * 10000) / 100
299
309
  };
300
310
  })
301
311
  .catch((error) => {
302
312
  // Handle any errors
303
313
  console.error("Error:", error);
314
+ })
315
+ .finally(() => {
316
+ app.loading = false;
304
317
  });
305
318
  },
306
319
  computed: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpoolos/edge-bacnet",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "A bacnet gateway for node-red",
5
5
  "dependencies": {
6
6
  "@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
@@ -146,14 +146,14 @@ async function processPrimeVueHtml(appData) {
146
146
  // Calculate percentages
147
147
  const total = this.tableData.length || 1; // Avoid division by zero
148
148
  this.statPercentages = {
149
- readCount: Math.round((this.statCounts.readCount / total) * 100),
150
- ok: Math.round((this.statCounts.statBlock.ok / total) * 100),
151
- error: Math.round((this.statCounts.statBlock.error / total) * 100),
152
- missing: Math.round((this.statCounts.statBlock.missing / total) * 100),
153
- warnings: Math.round((this.statCounts.statBlock.warnings / total) * 100),
154
- deviceIdChange: Math.round((this.statCounts.statBlock.deviceIdChange / total) * 100),
155
- deviceIdConflict: Math.round((this.statCounts.statBlock.deviceIdConflict / total) * 100 || 0),
156
- unmapped: Math.round((this.statCounts.statBlock.unmapped / total) * 100)
149
+ readCount: Math.round((this.statCounts.readCount / total) * 10000) / 100,
150
+ ok: Math.round((this.statCounts.statBlock.ok / total) * 10000) / 100,
151
+ error: Math.round((this.statCounts.statBlock.error / total) * 10000) / 100,
152
+ missing: Math.round((this.statCounts.statBlock.missing / total) * 10000) / 100,
153
+ warnings: Math.round((this.statCounts.statBlock.warnings / total) * 10000) / 100,
154
+ deviceIdChange: Math.round((this.statCounts.statBlock.deviceIdChange / total) * 10000) / 100,
155
+ deviceIdConflict: Math.round((this.statCounts.statBlock.deviceIdConflict / total) * 10000) / 100,
156
+ unmapped: Math.round((this.statCounts.statBlock.unmapped / total) * 10000) / 100
157
157
  };
158
158
  },
159
159
  computed: {
@@ -183,32 +183,15 @@ body {
183
183
  }
184
184
 
185
185
 
186
- /* changes start */
187
-
188
- /* Add these rules to forcefully control table height */
189
- .fixed-height-table {
190
- height: 800px !important; /* Fixed height matching scrollHeight */
191
- }
192
-
193
- .fixed-height-table .p-datatable-wrapper {
194
- height: calc(800px - 92px) !important; /* Subtract header and paginator height */
195
- min-height: calc(800px - 92px) !important;
196
- }
197
-
198
186
  .fixed-height-table .p-datatable-table {
199
- min-height: 800px !important; /* Force table to maintain minimum height */
200
- }
201
-
202
- /* Make sure the table body expands to fill available space */
203
- .fixed-height-table .p-datatable-tbody {
204
- min-height: 760px !important; /* Approximate calculation for body only */
187
+ height: 70vh !important;
205
188
  }
206
189
 
207
- /* Add an empty row to maintain height when filtered results are few */
208
190
  .p-datatable-emptymessage td {
209
191
  height: 760px !important;
210
192
  }
211
193
 
194
+
212
195
  .row-missing {
213
196
  background-color: #00adef;
214
197
  }
@@ -234,7 +217,7 @@ body {
234
217
  width: -webkit-fill-available;
235
218
  margin: 5px;
236
219
  left: 0px;
237
- height: 1000px !important;
220
+ /* height: 1000px !important; */
238
221
  }
239
222
 
240
223
  .datatable-card {
@@ -242,7 +225,7 @@ body {
242
225
  padding-bottom: 1rem !important;
243
226
  padding-left: 2rem;
244
227
  padding-right: 2rem;
245
- height: 1000px !important;
228
+ /* height: 1000px !important; */
246
229
  }
247
230
 
248
231
  .p-multiselect-overlay {
@@ -500,4 +483,33 @@ body {
500
483
  /* Ensure table takes minimum space when empty */
501
484
  .p-datatable-empty-message {
502
485
  height: 576px !important;
486
+ }
487
+ .loading-overlay {
488
+ position: absolute;
489
+ top: 0;
490
+ left: 0;
491
+ width: 100%;
492
+ height: 100%;
493
+ display: flex;
494
+ justify-content: center;
495
+ align-items: center;
496
+ background-color: rgba(255, 255, 255, 0.7);
497
+ z-index: 1000;
498
+ }
499
+
500
+ .loading-spinner {
501
+ display: flex;
502
+ flex-direction: column;
503
+ align-items: center;
504
+ gap: 1rem;
505
+ }
506
+
507
+ .loading-spinner p {
508
+ margin: 0;
509
+ color: #495057;
510
+ font-weight: 500;
511
+ }
512
+
513
+ .p-datatable .p-datatable-loading-overlay {
514
+ position: relative;
503
515
  }
@@ -278,14 +278,14 @@ async function generatePrimeVueAppHtmlStatic(appData, filename = "bacnet-inspect
278
278
  // Calculate percentages
279
279
  const total = this.tableData.length || 1; // Avoid division by zero
280
280
  this.statPercentages = {
281
- readCount: Math.round((this.statCounts.readCount / total) * 100) || 0,
282
- ok: Math.round((this.statCounts.statBlock?.ok / total) * 100) || 0,
283
- error: Math.round((this.statCounts.statBlock?.error / total) * 100) || 0,
284
- missing: Math.round((this.statCounts.statBlock?.missing / total) * 100) || 0,
285
- warnings: Math.round((this.statCounts.statBlock?.warnings / total) * 100) || 0,
286
- deviceIdChange: Math.round((this.statCounts.statBlock?.deviceIdChange / total) * 100) || 0,
287
- deviceIdConflict: Math.round((this.statCounts.statBlock?.deviceIdConflict / total) * 100) || 0,
288
- unmapped: Math.round((this.statCounts.statBlock?.unmapped / total) * 100) || 0
281
+ readCount: Math.round((this.statCounts.readCount / total) * 10000) / 100 || 0,
282
+ ok: Math.round((this.statCounts.statBlock.ok / total) * 10000) / 100 || 0,
283
+ error: Math.round((this.statCounts.statBlock.error / total) * 10000) / 100 || 0,
284
+ missing: Math.round((this.statCounts.statBlock.missing / total) * 10000) / 100 || 0,
285
+ warnings: Math.round((this.statCounts.statBlock.warnings / total) * 10000) / 100 || 0,
286
+ deviceIdChange: Math.round((this.statCounts.statBlock.deviceIdChange / total) * 10000) / 100 || 0,
287
+ deviceIdConflict: Math.round((this.statCounts.statBlock.deviceIdConflict / total) * 10000) / 100 || 0,
288
+ unmapped: Math.round((this.statCounts.statBlock.unmapped / total) * 10000) / 100 || 0
289
289
  };
290
290
  },
291
291
  computed: {