@bitpoolos/edge-bacnet 1.5.3 → 1.6.0

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.
@@ -0,0 +1,535 @@
1
+ /**
2
+ * Exports PrimeVue apps as standalone HTML files
3
+ * Generates static HTML that can be viewed without a server
4
+ */
5
+
6
+ /**
7
+ * Generate a standalone HTML file from a PrimeVue app using SSR
8
+ * @param {Object} appData - The application data to render
9
+ * @param {string} filename - The output filename
10
+ * @param {Object} options - Additional options for rendering
11
+ * @returns {Promise<string>} - The generated HTML content
12
+ */
13
+ async function generatePrimeVueAppHtmlStatic(appData, filename = "bacnet-inspector-snapshot.html", options = {}) {
14
+ const {
15
+ title = "Bitpool BACnet Inspector",
16
+ logoBase64 = null,
17
+ customStyles = "",
18
+ } = options;
19
+
20
+ // Get embedded dependencies
21
+ const embeddedDependencies = await getEmbeddedDependencies();
22
+ const iconBase64Map = await getIconsAsBase64();
23
+
24
+ const fullHtml = `<!DOCTYPE html>
25
+ <html>
26
+ <head>
27
+ <meta charset="UTF-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
+ <title>${title}</title>
30
+
31
+ <!-- Embedded styles -->
32
+ <style>
33
+ ${embeddedDependencies.styles.theme || ''}
34
+ ${embeddedDependencies.styles.primevue || ''}
35
+ ${embeddedDependencies.styles.primeflex || ''}
36
+ ${embeddedDependencies.styles.primeicons || ''}
37
+ ${embeddedDependencies.styles.inspector || ''}
38
+ ${customStyles}
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <div id="app">
43
+ <div class="card header-content">
44
+ <div class="header">
45
+ <div class="dividerRight">
46
+ <img :src="'data:image/svg+xml;base64,${logoBase64 || ""}'" class="logo" alt="Bitpool" />
47
+ </div>
48
+ <div class="status">
49
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
50
+ <span class="status-icon-wrapper" style="background-color: #10B981;">
51
+ <img src="data:image/svg+xml;base64,${iconBase64Map.ok}" class="status-icon ok-icon" alt="Points OK" />
52
+ </span>
53
+ <div class="status-text">
54
+ <span class="statBlockValue">
55
+ {{statCounts?.readCount}}
56
+ <span class="stat-percentage">{{statPercentages.readCount}}%</span>
57
+ </span>
58
+ <span class="statBlockKey">Points OK</span>
59
+ </div>
60
+ </span>
61
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
62
+ <span class="status-icon-wrapper" style="background-color: #F1707B;">
63
+ <img src="data:image/svg+xml;base64,${iconBase64Map.error}" class="status-icon error-icon" alt="Points Error" />
64
+ </span>
65
+ <div class="status-text">
66
+ <span class="statBlockValue">
67
+ {{statCounts?.statBlock.error}}
68
+ <span class="stat-percentage">{{statPercentages.error}}%</span>
69
+ </span>
70
+ <span class="statBlockKey">Points Error</span>
71
+ </div>
72
+ </span>
73
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
74
+ <span class="status-icon-wrapper" style="background-color: #133547;">
75
+ <img src="data:image/svg+xml;base64,${iconBase64Map.missing}" class="status-icon missing-icon" alt="Points Missing" />
76
+ </span>
77
+ <div class="status-text">
78
+ <span class="statBlockValue">
79
+ {{statCounts?.statBlock.missing}}
80
+ <span class="stat-percentage">{{statPercentages.missing}}%</span>
81
+ </span>
82
+ <span class="statBlockKey">Points Missing</span>
83
+ </div>
84
+ </span>
85
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
86
+ <span class="status-icon-wrapper" style="background-color: #F59E0B;">
87
+ <img src="data:image/svg+xml;base64,${iconBase64Map.warning}" class="status-icon warning-icon" alt="Points Warning" />
88
+ </span>
89
+ <div class="status-text">
90
+ <span class="statBlockValue">
91
+ {{statCounts?.statBlock.warnings}}
92
+ <span class="stat-percentage">{{statPercentages.warnings}}%</span>
93
+ </span>
94
+ <span class="statBlockKey">Points Warnings</span>
95
+ </div>
96
+ </span>
97
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
98
+ <span class="status-icon-wrapper" style="background-color: #00ADEF;">
99
+ <img src="data:image/svg+xml;base64,${iconBase64Map.unmapped}" class="status-icon unmapped-icon" alt="Points Unmapped" />
100
+ </span>
101
+ <div class="status-text">
102
+ <span class="statBlockValue">
103
+ {{statCounts?.statBlock.unmapped}}
104
+ <span class="stat-percentage">{{statPercentages.unmapped}}%</span>
105
+ </span>
106
+ <span class="statBlockKey">Points Unmapped</span>
107
+ </div>
108
+ </span>
109
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
110
+ <span class="status-icon-wrapper" style="background-color: #0689BC;">
111
+ <img src="data:image/svg+xml;base64,${iconBase64Map.deviceIdChange}" class="status-icon device-id-change-icon" alt="Device ID Change" />
112
+ </span>
113
+ <div class="status-text">
114
+ <span class="statBlockValue">
115
+ {{statCounts?.statBlock.deviceIdChange}}
116
+ <span class="stat-percentage">{{statPercentages.deviceIdChange}}%</span>
117
+ </span>
118
+ <span class="statBlockKey">Changed Device ID's</span>
119
+ </div>
120
+ </span>
121
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
122
+ <span class="status-icon-wrapper" style="background-color: #406C7D;">
123
+ <img src="data:image/svg+xml;base64,${iconBase64Map.deviceIdConflict || ''}" class="status-icon device-id-conflict-icon" alt="Device ID Conflict" />
124
+ </span>
125
+ <div class="status-text">
126
+ <span class="statBlockValue">
127
+ {{statCounts?.statBlock.deviceIdConflict}}
128
+ <span class="stat-percentage">{{statPercentages.deviceIdConflict}}%</span>
129
+ </span>
130
+ <span class="statBlockKey">Conflicting Device ID's</span>
131
+ </div>
132
+ </span>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ <div class="content-wrapper">
137
+ <div class="card datatable-card">
138
+ <div class="center">
139
+ <p class="headertext">{{siteName}} - Point Status Display</p>
140
+ </div>
141
+ <p-datatable :value="tableData" paginator :rows="12" filterDisplay="menu" :filters="filters"
142
+ v-model:filters="filters" :sortMode="'multiple'" @filter="onFilter" scrollable scrollHeight="800px"
143
+ class="datatable fixed-height-table">
144
+ <template #header>
145
+ <div class="tableHeaderDiv">
146
+ <div style="margin-right: 5px">
147
+ <p-multiselect v-model="selectedColumns" :options="allColumns" optionLabel="header" dataKey="field"
148
+ placeholder="Select Columns" :maxSelectedLabels="3" class="columnSelector w-full md:w-20rem"
149
+ display="chip">
150
+
151
+ <template #option="slotProps">
152
+ <div style="display: flex; align-items: center;">
153
+ <div class="custom-checkbox">
154
+ <div :class="['custom-checkbox-box', {'selected': isSelected(slotProps.option)}]">
155
+ <i v-if="isSelected(slotProps.option)" class="pi pi-check custom-checkbox-icon"></i>
156
+ </div>
157
+ </div>
158
+ <span>{{ slotProps.option.header }}</span>
159
+ </div>
160
+ </template>
161
+
162
+ <template #value="slotProps">
163
+ <template v-if="slotProps.value && slotProps.value.length > 0">
164
+ <template v-if="slotProps.value.length <= 3">
165
+ <div class="p-multiselect-token" v-for="(item, index) in slotProps.value" :key="item.field">
166
+ <span class="p-multiselect-token-label">{{ item.header }}</span>
167
+ <span v-if="index < slotProps.value.length - 1">, </span>
168
+ </div>
169
+ </template>
170
+ <template v-else>
171
+ <div class="p-multiselect-token-label">{{ slotProps.value.length }} items selected</div>
172
+ </template>
173
+ </template>
174
+ <span v-else class="p-multiselect-placeholder">{{ 'Select Columns' }}</span>
175
+ </template>
176
+ </p-multiselect>
177
+ </div>
178
+
179
+ <div style="width: 100%">
180
+ <i class="pi pi-search searchBarContainer"></i>
181
+ <p-input-text class="searchBar" type="text" pInputText v-model="filters['global'].value"
182
+ placeholder="Search"></p-input-text>
183
+ </div>
184
+ </div>
185
+
186
+ </template>
187
+ <p-column v-for="col in visibleColumns" :key="col.field" :field="col.field" :header="col.header" sortable
188
+ filter></p-column>
189
+ <template #paginatorstart>
190
+ </template>
191
+ <template #paginatorend="slotProps">
192
+ <span class="">
193
+ <span class="statBlockValue">{{displayedRowsCount}} results</span>
194
+ </span>
195
+ </template>
196
+ </p-datatable>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Embedded scripts -->
202
+ <script>${embeddedDependencies.scripts.vue || ''}</script>
203
+ <script>${embeddedDependencies.scripts.primevue || ''}</script>
204
+
205
+ <script>
206
+ window.appData = ${JSON.stringify(appData)};
207
+ document.addEventListener('DOMContentLoaded', function() {
208
+
209
+ const { createApp } = Vue;
210
+ const app = createApp({
211
+ data() {
212
+ return {
213
+ tableData: window.appData.tableData || [],
214
+ filteredData: window.appData.tableData || [],
215
+ loading: false,
216
+ totalRecords: window.appData.tableData?.length || 0,
217
+ first: 0,
218
+ rows: 12,
219
+ lazyParams: {},
220
+ siteName: window.appData.siteName || "BACnet Inspector",
221
+ statCounts: window.appData.statCounts || {},
222
+ displayedRowsCount: window.appData.tableData?.length || 0,
223
+ filters: {
224
+ global: { value: null, matchMode: "contains" },
225
+ deviceID: { value: null, matchMode: "contains" },
226
+ objectType: { value: null, matchMode: "contains" },
227
+ objectInstance: { value: null, matchMode: "contains" },
228
+ presentValue: { value: null, matchMode: "contains" },
229
+ dataModelStatus: { value: null, matchMode: "contains" },
230
+ pointName: { value: null, matchMode: "contains" },
231
+ discoveredBACnetPointName: { value: null, matchMode: "contains" },
232
+ displayName: { value: null, matchMode: "contains" },
233
+ deviceName: { value: null, matchMode: "contains" },
234
+ ipAddress: { value: null, matchMode: "contains" },
235
+ area: { value: null, matchMode: "contains" },
236
+ key: { value: null, matchMode: "contains" },
237
+ topic: { value: null, matchMode: "contains" },
238
+ lastSeen: { value: null, matchMode: "contains" },
239
+ error: { value: null, matchMode: "contains" },
240
+ },
241
+ allColumns: [
242
+ { field: "deviceID", header: "Device ID" },
243
+ { field: "objectType", header: "Object Type" },
244
+ { field: "objectInstance", header: "Object Instance" },
245
+ { field: "presentValue", header: "Present Value" },
246
+ { field: "dataModelStatus", header: "Data Model Status" },
247
+ { field: "pointName", header: "Mapped Point Name" },
248
+ { field: "discoveredBACnetPointName", header: "Discovered Point Name" },
249
+ { field: "displayName", header: "Display Name" },
250
+ { field: "deviceName", header: "Device Name" },
251
+ { field: "ipAddress", header: "IP Address" },
252
+ { field: "area", header: "Area" },
253
+ { field: "key", header: "Key" },
254
+ { field: "topic", header: "Topic" },
255
+ { field: "lastSeen", header: "Last Seen" },
256
+ { field: "error", header: "Error" },
257
+ ],
258
+ selectedColumns: [],
259
+ statPercentages: {
260
+ readCount: 0,
261
+ error: 0,
262
+ missing: 0,
263
+ warnings: 0,
264
+ deviceIdChange: 0,
265
+ deviceIdConflict: 0,
266
+ unmapped: 0
267
+ },
268
+ activeFilter: null,
269
+ };
270
+ },
271
+ mounted() {
272
+ this.selectedColumns = JSON.parse(JSON.stringify(this.allColumns));
273
+ if(window.appData.tableData.length > 0) {
274
+ this.tableData = window.appData.tableData;
275
+ }
276
+
277
+ // Calculate percentages
278
+ const total = this.tableData.length || 1; // Avoid division by zero
279
+ this.statPercentages = {
280
+ readCount: Math.round((this.statCounts.readCount / total) * 100) || 0,
281
+ error: Math.round((this.statCounts.statBlock?.error / total) * 100) || 0,
282
+ missing: Math.round((this.statCounts.statBlock?.missing / total) * 100) || 0,
283
+ warnings: Math.round((this.statCounts.statBlock?.warnings / total) * 100) || 0,
284
+ deviceIdChange: Math.round((this.statCounts.statBlock?.deviceIdChange / total) * 100) || 0,
285
+ deviceIdConflict: Math.round((this.statCounts.statBlock?.deviceIdConflict / total) * 100) || 0,
286
+ unmapped: Math.round((this.statCounts.statBlock?.unmapped / total) * 100) || 0
287
+ };
288
+ },
289
+ computed: {
290
+ visibleColumns() {
291
+ return this.selectedColumns || this.allColumns;
292
+ },
293
+ displayData() {
294
+ if (!this.tableData || this.tableData.length === 0) {
295
+ // Create empty rows to maintain height
296
+ return Array(20).fill({}).map(() => ({}));
297
+ }
298
+ return this.tableData;
299
+ }
300
+ },
301
+ methods: {
302
+ statusItemClicked(e) {
303
+ // Get the filter category from the clicked item's text content
304
+ const clickedItem = e.currentTarget;
305
+ const statusType = clickedItem.querySelector('.statBlockKey').textContent.trim();
306
+
307
+ // Clear previous active status styling
308
+ document.querySelectorAll('.statusItem').forEach(item => {
309
+ item.classList.remove('active-filter');
310
+ });
311
+
312
+ // If clicking the already active filter, clear it
313
+ if (this.activeFilter === statusType) {
314
+ this.activeFilter = null;
315
+ this.filters['dataModelStatus'].value = null;
316
+ return;
317
+ }
318
+
319
+ // Set this item as active
320
+ clickedItem.classList.add('active-filter');
321
+ this.activeFilter = statusType;
322
+
323
+ // Apply the appropriate filter based on which status item was clicked
324
+ let filterValue = '';
325
+ switch (statusType) {
326
+ case 'Points OK':
327
+ filterValue = 'Point Ok';
328
+ break;
329
+ case 'Points Error':
330
+ filterValue = 'Point Error';
331
+ break;
332
+ case 'Points Missing':
333
+ filterValue = 'Point Missing';
334
+ break;
335
+ case 'Points Warnings':
336
+ filterValue = 'Point Warning';
337
+ break;
338
+ case 'Points Unmapped':
339
+ filterValue = 'Point Unmapped';
340
+ break;
341
+ case "Changed Device ID's":
342
+ filterValue = 'Device ID Changed';
343
+ break;
344
+ case "Conflicting Device ID's":
345
+ filterValue = 'Device ID Conflict';
346
+ break;
347
+ default:
348
+ filterValue = '';
349
+ }
350
+
351
+ // Apply the filter
352
+ this.filters['dataModelStatus'].value = filterValue;
353
+ },
354
+ isSelected(option) {
355
+ if (!this.selectedColumns) return false;
356
+ return this.selectedColumns.some((item) => item.field === option.field);
357
+ },
358
+ getRowClass(rowData) {
359
+ if (!rowData || !rowData.dataModelStatus) return "";
360
+ if (rowData.dataModelStatus.includes("Point OK")) return "row-ok";
361
+ if (rowData.dataModelStatus.includes("Point Error")) return "row-error";
362
+ if (rowData.dataModelStatus.includes("Point Warning")) return "row-warning";
363
+ if (rowData.dataModelStatus.includes("Point Missing")) return "row-missing";
364
+ return "";
365
+ },
366
+ onGlobalFilterChange(event) {
367
+ const value = event.target.value;
368
+ this.filters.global.value = value;
369
+ if (this.$refs.dt) {
370
+ this.$refs.dt.filter(value, "global", "contains");
371
+ }
372
+ },
373
+ onFilter(e) {
374
+ this.displayedRowsCount = e.filteredValue ? e.filteredValue.length : 0;
375
+ },
376
+ onPage(event) {
377
+ this.first = event.first;
378
+ this.rows = event.rows;
379
+ },
380
+ },
381
+ components: {
382
+ "p-button": PrimeVue.Button,
383
+ "p-card": PrimeVue.Card,
384
+ "p-input-text": PrimeVue.InputText,
385
+ "p-datatable": PrimeVue.DataTable,
386
+ "p-column": PrimeVue.Column,
387
+ "p-icon-field": PrimeVue.IconField,
388
+ "p-input-icon": PrimeVue.InputIcon,
389
+ "p-multiselect": PrimeVue.MultiSelect
390
+ },
391
+ });
392
+
393
+ app.use(PrimeVue.Config);
394
+ app.mount('#app');
395
+ });
396
+ </script>
397
+ </body>
398
+ </html>`;
399
+
400
+ return fullHtml;
401
+ }
402
+
403
+ /**
404
+ * In a Node.js environment, fetch the logo and convert to base64
405
+ * @param {string} logoPath - Path to the logo file
406
+ * @returns {Promise<string>} - Base64 encoded logo
407
+ */
408
+ async function getLogoAsBase64(logoPath) {
409
+ // In Node.js environment
410
+ if (typeof window === "undefined" && logoPath) {
411
+ try {
412
+ const fs = require("fs");
413
+ const logoData = fs.readFileSync(logoPath);
414
+ return Buffer.from(logoData).toString("base64");
415
+ } catch (error) {
416
+ console.error("Error reading logo file:", error);
417
+ return "";
418
+ }
419
+ }
420
+
421
+ // In browser environment (if somehow used there)
422
+ if (typeof window !== "undefined" && logoPath) {
423
+ try {
424
+ const response = await fetch(logoPath);
425
+ const blob = await response.blob();
426
+ return new Promise((resolve) => {
427
+ const reader = new FileReader();
428
+ reader.onloadend = () => {
429
+ const base64 = reader.result.split(",")[1];
430
+ resolve(base64);
431
+ };
432
+ reader.readAsDataURL(blob);
433
+ });
434
+ } catch (error) {
435
+ console.error("Error fetching logo:", error);
436
+ return "";
437
+ }
438
+ }
439
+
440
+ return "";
441
+ }
442
+
443
+ async function getIconsAsBase64() {
444
+ const fs = require('fs');
445
+ const path = require('path');
446
+
447
+ const icons = {
448
+ ok: 'points-ok-icon.svg',
449
+ error: 'points-error-icon.svg',
450
+ missing: 'points-missing-icon.svg',
451
+ warning: 'points-warning-icon.svg',
452
+ deviceIdChange: 'device-id-change-icon.svg',
453
+ unmapped: 'points-unmapped-icon.svg',
454
+ deviceIdConflict: 'device-id-conflict-icon.svg'
455
+ };
456
+
457
+ const iconBase64Map = {};
458
+
459
+ for (const [key, filename] of Object.entries(icons)) {
460
+ try {
461
+ // Construct the full path to the icon file
462
+ const iconPath = path.join(__dirname, 'resources/icons', filename);
463
+
464
+ // Read the file synchronously
465
+ const fileContent = fs.readFileSync(iconPath, 'utf8');
466
+
467
+ // Convert SVG content to base64
468
+ const base64 = Buffer.from(fileContent).toString('base64');
469
+
470
+ iconBase64Map[key] = base64;
471
+ } catch (error) {
472
+ console.error(`Error reading ${filename}:`, error);
473
+ iconBase64Map[key] = '';
474
+ }
475
+ }
476
+
477
+ return iconBase64Map;
478
+ }
479
+
480
+ /**
481
+ * Get embedded dependencies content for the PrimeVue app
482
+ * @returns {Object} - Object with embedded scripts and styles
483
+ */
484
+ async function getEmbeddedDependencies() {
485
+ const fs = require('fs');
486
+ const path = require('path');
487
+
488
+ // Define files to embed
489
+ const dependencyFiles = {
490
+ scripts: [
491
+ { name: 'vue', path: path.join(__dirname, "resources", "vue3513.global.prod.js") },
492
+ { name: 'primevue', path: path.join(__dirname, 'resources', 'primevue.min.js') }
493
+ ],
494
+ styles: [
495
+ { name: 'theme', path: path.join(__dirname, 'resources', 'primevue-saga-blue-theme.css') },
496
+ { name: 'primevue', path: path.join(__dirname, 'resources', 'primevue.min.css') },
497
+ { name: 'primeflex', path: path.join(__dirname, 'resources', 'primeflex.min.css') },
498
+ { name: 'primeicons', path: path.join(__dirname, 'resources', 'primeicons.css') },
499
+ { name: 'inspector', path: path.join(__dirname, 'resources', 'inspectorStyles.css') }
500
+ ]
501
+ };
502
+
503
+ // Read and collect dependencies
504
+ const embeddedContent = {
505
+ scripts: {},
506
+ styles: {}
507
+ };
508
+
509
+ // Read script files
510
+ for (const script of dependencyFiles.scripts) {
511
+ try {
512
+ embeddedContent.scripts[script.name] = fs.readFileSync(script.path, 'utf8');
513
+ } catch (error) {
514
+ console.error(`Error reading script file ${script.name}:`, error);
515
+ embeddedContent.scripts[script.name] = `console.error("Failed to load ${script.name}");`;
516
+ }
517
+ }
518
+
519
+ // Read style files
520
+ for (const style of dependencyFiles.styles) {
521
+ try {
522
+ embeddedContent.styles[style.name] = fs.readFileSync(style.path, 'utf8');
523
+ } catch (error) {
524
+ console.error(`Error reading style file ${style.name}:`, error);
525
+ embeddedContent.styles[style.name] = `/* Failed to load ${style.name} */`;
526
+ }
527
+ }
528
+
529
+ return embeddedContent;
530
+ }
531
+
532
+ module.exports = {
533
+ generatePrimeVueAppHtmlStatic,
534
+ getLogoAsBase64,
535
+ };
package/treeBuilder.js CHANGED
@@ -1,4 +1,4 @@
1
- const { Store_Config } = require("./common");
1
+ const { queueConfigStore } = require("./common");
2
2
  const { BacnetDevice } = require("./bacnet_device");
3
3
 
4
4
  /**
@@ -33,12 +33,12 @@ class treeBuilder {
33
33
  */
34
34
  cacheData() {
35
35
  // Cache the current state of the network tree and other data
36
- Store_Config(JSON.stringify({
36
+ queueConfigStore({
37
37
  renderList: this.renderList,
38
38
  deviceList: this.deviceList,
39
39
  pointList: this.networkTree,
40
40
  renderListCount: this.renderListCount,
41
- }));
41
+ });
42
42
  }
43
43
 
44
44
  /**