@bitpoolos/edge-bacnet 1.5.3 → 1.6.1

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/inspector.html ADDED
@@ -0,0 +1,455 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Bitpool BACnet Inspector</title>
8
+ <link rel="icon" type="image/x-icon" href="resources/@bitpoolos/edge-bacnet/icons/favicon.ico">
9
+
10
+ <!-- Vue 3 -->
11
+ <script src="resources/@bitpoolos/edge-bacnet/vue3513.global.prod.js"></script>
12
+ <script src="resources/@bitpoolos/edge-bacnet/primevue.min.js"></script>
13
+
14
+ <script src="resources/@bitpoolos/edge-bacnet/downloadAsHtml.js"></script>
15
+
16
+ <link href="resources/@bitpoolos/edge-bacnet/primevue-saga-blue-theme.css" rel="stylesheet" />
17
+ <link href="resources/@bitpoolos/edge-bacnet/primevue.min.css" rel="stylesheet" />
18
+ <link href="resources/@bitpoolos/edge-bacnet/primeflex.min.css" rel="stylesheet" />
19
+ <link href="resources/@bitpoolos/edge-bacnet/primeicons.css" rel="stylesheet" />
20
+ <link href="resources/@bitpoolos/edge-bacnet/inspectorStyles.css" rel="stylesheet" />
21
+ </head>
22
+
23
+ <body>
24
+ <div id="app">
25
+ <div class="card header-content">
26
+ <div class="header">
27
+ <div class="dividerRight">
28
+ <img src="resources/@bitpoolos/edge-bacnet/Logo_Simplified_Positive.svg" class="logo" alt="Bitpool" />
29
+ </div>
30
+ <div class="status">
31
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
32
+ <span class="status-icon-wrapper" style="background-color: #10B981;">
33
+ <img src="resources/@bitpoolos/edge-bacnet/icons/points-ok-icon.svg" class="status-icon ok-icon"
34
+ alt="Points OK" />
35
+ </span>
36
+ <div class="status-text">
37
+ <span class="statBlockValue">
38
+ {{statCounts?.statBlock.ok}}
39
+ <span class="stat-percentage">{{statPercentages.ok}}%</span>
40
+ </span>
41
+ <span class="statBlockKey">Points OK</span>
42
+ </div>
43
+ </span>
44
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
45
+ <span class="status-icon-wrapper" style="background-color: #F1707B;">
46
+ <img src="resources/@bitpoolos/edge-bacnet/icons/points-error-icon.svg" class="status-icon error-icon"
47
+ alt="Points Error" />
48
+ </span>
49
+ <div class="status-text">
50
+ <span class="statBlockValue">
51
+ {{statCounts?.statBlock.error}}
52
+ <span class="stat-percentage">{{statPercentages.error}}%</span>
53
+ </span>
54
+ <span class="statBlockKey">Points Error</span>
55
+ </div>
56
+ </span>
57
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
58
+ <span class="status-icon-wrapper" style="background-color: #133547;">
59
+ <img src="resources/@bitpoolos/edge-bacnet/icons/points-missing-icon.svg" class="status-icon missing-icon"
60
+ alt="Points Missing" />
61
+ </span>
62
+ <div class="status-text">
63
+ <span class="statBlockValue">
64
+ {{statCounts?.statBlock.missing}}
65
+ <span class="stat-percentage">{{statPercentages.missing}}%</span>
66
+ </span>
67
+ <span class="statBlockKey">Points Missing</span>
68
+ </div>
69
+ </span>
70
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
71
+ <span class="status-icon-wrapper" style="background-color: #F59E0B;">
72
+ <img src="resources/@bitpoolos/edge-bacnet/icons/points-warning-icon.svg" class="status-icon warning-icon"
73
+ alt="Points Warning" />
74
+ </span>
75
+ <div class="status-text">
76
+ <span class="statBlockValue">
77
+ {{statCounts?.statBlock.warnings}}
78
+ <span class="stat-percentage">{{statPercentages.warnings}}%</span>
79
+ </span>
80
+ <span class="statBlockKey">Points Warnings</span>
81
+ </div>
82
+ </span>
83
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
84
+ <span class="status-icon-wrapper" style="background-color: #00ADEF;">
85
+ <img src="resources/@bitpoolos/edge-bacnet/icons/points-unmapped-icon.svg"
86
+ class="status-icon unmapped-icon" alt="Points Unmapped" />
87
+ </span>
88
+ <div class="status-text">
89
+ <span class="statBlockValue">
90
+ {{statCounts?.statBlock.unmapped}}
91
+ <span class="stat-percentage">{{statPercentages.unmapped}}%</span>
92
+ </span>
93
+ <span class="statBlockKey">Points Unmapped</span>
94
+ </div>
95
+ </span>
96
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
97
+ <span class="status-icon-wrapper" style="background-color: #0689BC;">
98
+ <img src="resources/@bitpoolos/edge-bacnet/icons/device-id-change-icon.svg"
99
+ class="status-icon device-id-change-icon" alt="Device ID Change" />
100
+ </span>
101
+ <div class="status-text">
102
+ <span class="statBlockValue">
103
+ {{statCounts?.statBlock.deviceIdChange}}
104
+ <span class="stat-percentage">{{statPercentages.deviceIdChange}}%</span>
105
+ </span>
106
+ <span class="statBlockKey">Changed Device ID's</span>
107
+ </div>
108
+ </span>
109
+ <span @click="statusItemClicked" class="statusItem status-with-icon">
110
+ <span class="status-icon-wrapper" style="background-color: #406C7D;">
111
+ <img src="resources/@bitpoolos/edge-bacnet/icons/device-id-conflict-icon.svg"
112
+ class="status-icon device-id-conflict-icon" alt="Device ID Conflict" />
113
+ </span>
114
+ <div class="status-text">
115
+ <span class="statBlockValue">
116
+ {{statCounts?.statBlock.deviceIdConflict}}
117
+ <span class="stat-percentage">{{statPercentages.deviceIdConflict}}%</span>
118
+ </span>
119
+ <span class="statBlockKey">Conflicting Device ID's</span>
120
+ </div>
121
+ </span>
122
+ </div>
123
+ <div class="actionButtons">
124
+ <p-button icon="pi pi-refresh" onclick="window.location.reload()" class="refreshButton" label="Refresh">
125
+ </p-button>
126
+ <p-button icon="pi pi-download" @click="downloadCSV()" class="bitpool-blue" label="Download CSV"></p-button>
127
+ <p-button icon="pi pi-download" @click="downloadHTML()" class="bitpool-blue" label="Download HTML"></p-button>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ <div class="content-wrapper">
132
+ <div class="card datatable-card">
133
+ <div class="center">
134
+ <p class="headertext">{{siteName}} - Point Status Display</p>
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">
139
+ <template #header>
140
+ <div class="tableHeaderDiv">
141
+ <div style="margin-right: 5px">
142
+ <p-multiselect v-model="selectedColumns" :options="allColumns" optionLabel="header" dataKey="field"
143
+ placeholder="Select Columns" :maxSelectedLabels="3" class="columnSelector w-full md:w-20rem"
144
+ display="chip">
145
+ <template #option="slotProps">
146
+ <div style="display: flex; align-items: center">
147
+ <div class="custom-checkbox">
148
+ <div :class="['custom-checkbox-box', {'selected': isSelected(slotProps.option)}]">
149
+ <i v-if="isSelected(slotProps.option)" class="pi pi-check custom-checkbox-icon"></i>
150
+ </div>
151
+ </div>
152
+ <span>{{ slotProps.option.header }}</span>
153
+ </div>
154
+ </template>
155
+
156
+ <template #value="slotProps">
157
+ <template v-if="slotProps.value && slotProps.value.length > 0">
158
+ <template v-if="slotProps.value.length <= 3">
159
+ <div class="p-multiselect-token" v-for="(item, index) in slotProps.value" :key="item.field">
160
+ <span class="p-multiselect-token-label">{{ item.header }}</span>
161
+ <span v-if="index < slotProps.value.length - 1">, </span>
162
+ </div>
163
+ </template>
164
+ <template v-else>
165
+ <div class="p-multiselect-token-label">{{ slotProps.value.length }} items selected</div>
166
+ </template>
167
+ </template>
168
+ <span v-else class="p-multiselect-placeholder">{{ 'Select Columns' }}</span>
169
+ </template>
170
+ </p-multiselect>
171
+ </div>
172
+
173
+ <div style="width: 100%">
174
+ <i class="pi pi-search searchBarContainer"></i>
175
+ <p-input-text class="searchBar" type="text" pInputText v-model="filters['global'].value"
176
+ placeholder="Search"></p-input-text>
177
+ </div>
178
+ </div>
179
+ </template>
180
+ <p-column v-for="col in visibleColumns" :key="col.field" :field="col.field" :header="col.header" sortable
181
+ filter></p-column>
182
+ <template #paginatorstart>
183
+
184
+ </template>
185
+ <template #paginatorend="slotProps">
186
+ <span class="">
187
+ <span class="statBlockValue">{{displayedRowsCount}} results</span>
188
+ </span>
189
+ </template>
190
+ </p-datatable>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ <script>
196
+ const { createApp, ref } = Vue;
197
+ // Create app
198
+ const app = createApp({
199
+ data() {
200
+ return {
201
+ tableData: null,
202
+ loading: false,
203
+ totalRecords: 0,
204
+ first: 0,
205
+ lazyParams: {},
206
+ siteName: ref(),
207
+ statCounts: ref(),
208
+ displayedRowsCount: 0, // Track how many rows are visible
209
+ filters: {
210
+ global: { value: null, matchMode: "contains" },
211
+ deviceID: { value: null, matchMode: "contains" },
212
+ ObjectType: { value: null, matchMode: "contains" },
213
+ objectInstance: { value: null, matchMode: "contains" },
214
+ presentValue: { value: null, matchMode: "contains" },
215
+ dataModelStatus: { value: null, matchMode: "contains" },
216
+ pointName: { value: null, matchMode: "contains" },
217
+ discoveredBACnetPointName: { value: null, matchMode: "contains" },
218
+ displayName: { value: null, matchMode: "contains" },
219
+ deviceName: { value: null, matchMode: "contains" },
220
+ ipAddress: { value: null, matchMode: "contains" },
221
+ area: { value: null, matchMode: "contains" },
222
+ key: { value: null, matchMode: "contains" },
223
+ topic: { value: null, matchMode: "contains" },
224
+ lastSeen: { value: null, matchMode: "contains" },
225
+ error: { value: null, matchMode: "contains" },
226
+ },
227
+ allColumns: [
228
+ { field: "deviceID", header: "Device ID" },
229
+ { field: "objectType", header: "Object Type" },
230
+ { field: "objectInstance", header: "Object Instance" },
231
+ { field: "presentValue", header: "Present Value" },
232
+ { field: "dataModelStatus", header: "Data Model Status" },
233
+ { field: "pointName", header: "Mapped Point Name" },
234
+ { field: "discoveredBACnetPointName", header: "Discovered Point Name" },
235
+ { field: "displayName", header: "Display Name" },
236
+ { field: "deviceName", header: "Device Name" },
237
+ { field: "ipAddress", header: "IP Address" },
238
+ { field: "area", header: "Area" },
239
+ { field: "key", header: "Key" },
240
+ { field: "topic", header: "Topic" },
241
+ { field: "lastSeen", header: "Last Seen" },
242
+ { field: "error", header: "Error" },
243
+ ],
244
+ selectedColumns: [],
245
+ statPercentages: {
246
+ readCount: 0,
247
+ ok: 0,
248
+ error: 0,
249
+ missing: 0,
250
+ warnings: 0,
251
+ deviceIdChange: 0,
252
+ deviceIdConflict: 0,
253
+ unmapped: 0
254
+ },
255
+ activeFilter: null,
256
+ };
257
+ },
258
+ setup() {
259
+ return {};
260
+ },
261
+ mounted() {
262
+ let app = this;
263
+ this.observeHeaderHeight();
264
+
265
+ this.selectedColumns = JSON.parse(JSON.stringify(this.allColumns));
266
+
267
+ fetch("/getModelStats")
268
+ .then((response) => {
269
+ // Check if the response is successful
270
+ if (!response.ok) {
271
+ throw new Error(`HTTP error! Status: ${response.status}`);
272
+ }
273
+ // Parse the JSON response
274
+ return response.json();
275
+ })
276
+ .then((data) => {
277
+ let tableData = [];
278
+
279
+ if (data.resultList) {
280
+ for (let item in data.resultList) {
281
+ tableData.push(data.resultList[item]);
282
+ }
283
+ }
284
+ app.tableData = tableData;
285
+ app.siteName = data.siteName;
286
+ app.statCounts = data.statCounts;
287
+
288
+ // Calculate percentages
289
+ const total = tableData.length || 1; // Avoid division by zero
290
+ 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)
299
+ };
300
+ })
301
+ .catch((error) => {
302
+ // Handle any errors
303
+ console.error("Error:", error);
304
+ });
305
+ },
306
+ computed: {
307
+ visibleColumns() {
308
+ return this.selectedColumns || []; // Handle null case
309
+ },
310
+ displayData() {
311
+ if (!this.tableData || this.tableData.length === 0) {
312
+ // Create empty rows to maintain height
313
+ return Array(20)
314
+ .fill({})
315
+ .map(() => ({}));
316
+ }
317
+ return this.tableData;
318
+ },
319
+ },
320
+ methods: {
321
+ statusItemClicked(e) {
322
+ // Get the filter category from the clicked item's text content
323
+ const clickedItem = e.currentTarget;
324
+ const statusType = clickedItem.querySelector('.statBlockKey').textContent.trim();
325
+
326
+ // Clear previous active status styling
327
+ document.querySelectorAll('.statusItem').forEach(item => {
328
+ item.classList.remove('active-filter');
329
+ });
330
+
331
+ // If clicking the already active filter, clear it
332
+ if (this.activeFilter === statusType) {
333
+ this.activeFilter = null;
334
+ this.filters['dataModelStatus'].value = null;
335
+ return;
336
+ }
337
+
338
+ // Set this item as active
339
+ clickedItem.classList.add('active-filter');
340
+ this.activeFilter = statusType;
341
+
342
+ // Apply the appropriate filter based on which status item was clicked
343
+ let filterValue = '';
344
+ switch (statusType) {
345
+ case 'Points OK':
346
+ filterValue = 'Point Ok';
347
+ break;
348
+ case 'Points Error':
349
+ filterValue = 'Point Error';
350
+ break;
351
+ case 'Points Missing':
352
+ filterValue = 'Point Missing';
353
+ break;
354
+ case 'Points Warnings':
355
+ filterValue = 'Point Warning';
356
+ break;
357
+ case 'Points Unmapped':
358
+ filterValue = 'Point Unmapped';
359
+ break;
360
+ case 'Changed Device ID\'s':
361
+ filterValue = 'Device ID Changed';
362
+ break;
363
+ case 'Conflicting Device ID\'s':
364
+ filterValue = 'Device ID Conflict';
365
+ break;
366
+ default:
367
+ filterValue = '';
368
+ }
369
+
370
+ // Apply the filter
371
+ this.filters['dataModelStatus'].value = filterValue;
372
+ },
373
+ observeHeaderHeight() {
374
+ const header = document.querySelector('.header-content');
375
+ const updateContentMargin = () => {
376
+ const headerHeight = header.offsetHeight;
377
+ document.documentElement.style.setProperty('--header-height', `${headerHeight}px`);
378
+ };
379
+
380
+ // Update initially
381
+ updateContentMargin();
382
+
383
+ // Observe header size changes
384
+ const resizeObserver = new ResizeObserver(updateContentMargin);
385
+ resizeObserver.observe(header);
386
+
387
+ // Update on window resize
388
+ window.addEventListener('resize', updateContentMargin);
389
+ },
390
+ enforceTableHeight() {
391
+ // Force minimum height on table elements
392
+ const tableWrapper = document.querySelector(".p-datatable-wrapper");
393
+ if (tableWrapper) {
394
+ tableWrapper.style.minHeight = "800px";
395
+ }
396
+
397
+ const tableBody = document.querySelector(".p-datatable-tbody");
398
+ if (tableBody) {
399
+ tableBody.style.minHeight = "760px";
400
+ }
401
+ },
402
+ isSelected(option) {
403
+ if (!this.selectedColumns) return false;
404
+ return this.selectedColumns.some((item) => item.field === option.field);
405
+ },
406
+ downloadCSV() {
407
+ window.location.href = "/getmodelstatscsv";
408
+ },
409
+ downloadHTML() {
410
+ let filename = `${this.siteName}_BACnetStats_${Date.now()}.html`;
411
+ // Create data object with all necessary properties
412
+ const data = {
413
+ tableData: this.tableData,
414
+ siteName: this.siteName,
415
+ statCounts: this.statCounts,
416
+ displayedRowsCount: this.displayedRowsCount,
417
+ filters: this.filters,
418
+ selectedColumns: this.selectedColumns,
419
+ allColumns: this.allColumns,
420
+ statPercentages: this.statPercentages // Include the calculated percentages
421
+ };
422
+ downloadPrimeVueAppAsHtml(filename, data);
423
+ },
424
+ onFilter(e) {
425
+ // e.filteredValue contains the new filtered dataset
426
+ if (e.filteredValue) {
427
+ this.displayedRowsCount = e.filteredValue.length;
428
+ } else {
429
+ // if no filter is applied, revert to full tableData length
430
+ this.displayedRowsCount = this.tableData ? this.tableData.length : 0;
431
+ }
432
+
433
+ this.$nextTick(() => {
434
+ this.enforceTableHeight();
435
+ });
436
+ },
437
+ },
438
+ components: {
439
+ "p-button": PrimeVue.Button,
440
+ "p-card": PrimeVue.Card,
441
+ "p-input-text": PrimeVue.InputText,
442
+ "p-datatable": PrimeVue.DataTable,
443
+ "p-column": PrimeVue.Column,
444
+ "p-icon-field": PrimeVue.IconField,
445
+ "p-input-icon": PrimeVue.InputIcon,
446
+ "p-multiselect": PrimeVue.MultiSelect,
447
+ },
448
+ });
449
+
450
+ app.use(PrimeVue.Config);
451
+ app.mount("#app");
452
+ </script>
453
+ </body>
454
+
455
+ </html>
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@bitpoolos/edge-bacnet",
3
- "version": "1.5.3",
3
+ "version": "1.6.1",
4
4
  "description": "A bacnet gateway for node-red",
5
5
  "dependencies": {
6
6
  "@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
7
+ "@vue/server-renderer": "^3.5.13",
7
8
  "async-mutex": "^0.4.0",
8
9
  "cronosjs": "^1.7.1",
9
10
  "debug": "^4.1.1",
10
11
  "iconv-lite": "^0.6.3",
12
+ "primevue": "^4.3.2",
11
13
  "toad-scheduler": "^1.6.0",
12
14
  "underscore": "^1.10.2",
15
+ "vue": "^3.5.13",
13
16
  "winston": "^3.2.1"
14
17
  },
15
18
  "node-red": {
@@ -18,7 +21,8 @@
18
21
  "Bacnet-Gateway": "bacnet_gateway.js",
19
22
  "Bacnet-Discovery": "bacnet_read.js",
20
23
  "Bacnet-Write": "bacnet_write.js",
21
- "Bitpool-Inject": "bitpool_inject.js"
24
+ "Bitpool-Inject": "bitpool_inject.js",
25
+ "Bacnet-Inspector": "bacnet_inspector.js"
22
26
  }
23
27
  },
24
28
  "keywords": [
@@ -0,0 +1,32 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 617.93 192.79">
3
+ <defs>
4
+ <style>
5
+ .cls-1 {
6
+ fill: #162732;
7
+ }
8
+
9
+ .cls-1, .cls-2 {
10
+ stroke-width: 0px;
11
+ }
12
+
13
+ .cls-2 {
14
+ fill: url(#linear-gradient);
15
+ }
16
+ </style>
17
+ <linearGradient id="linear-gradient" x1="0" y1="96.39" x2="171.04" y2="96.39" gradientUnits="userSpaceOnUse">
18
+ <stop offset="0" stop-color="#83d7f7"/>
19
+ <stop offset="1" stop-color="#00adef"/>
20
+ </linearGradient>
21
+ </defs>
22
+ <g>
23
+ <path class="cls-1" d="M255.61,94.18c14.39,1.25,19.23,10.24,19.23,25.46,0,19.64-8.99,24.9-31.27,24.9-17.43,0-26.56-.14-38.04-1.1V47.97c10.24-.97,18.54-1.11,33.76-1.11,24.07,0,32.65,5.26,32.65,24.9,0,13.7-4.7,20.75-16.32,21.86v.55ZM239.84,87.68c11.62,0,15.49-2.77,15.49-13.7s-4.15-13.14-16.88-13.14h-15.91v26.84h17.29ZM240.67,130.57c12.87,0,16.88-2.63,16.88-14.11,0-12.73-4.29-16.05-17.43-16.19h-17.57v30.16l18.12.14Z"/>
24
+ <path class="cls-1" d="M295.64,46.17c3.32,0,4.84,1.8,4.84,4.84v7.61c0,3.18-1.52,4.84-4.84,4.84h-7.75c-3.18,0-4.84-1.66-4.84-4.84v-7.61c0-3.04,1.66-4.84,4.84-4.84h7.75ZM283.46,143.43v-67.79h16.74v67.79h-16.74Z"/>
25
+ <path class="cls-1" d="M337.38,122.27c0,5.95,2.07,7.88,8.16,7.88h9.27l1.8,12.45c-4.7,1.52-13.28,2.35-17.43,2.35-11.9,0-18.4-7.19-18.4-19.78v-37.35h-12.31v-11.48l12.17-.69v-19.51h16.74v19.51h19.92v12.17h-19.92v34.45Z"/>
26
+ <path class="cls-1" d="M380.95,85.74c6.36-7.06,17.85-11.9,27.81-11.9,16.46,0,23.1,13.28,23.1,36.25,0,26.56-8.85,35-26.01,35-8.3,0-16.6-1.94-23.52-6.64.41,4.7.55,9.27.41,14.11v17.85h-16.74v-94.76h13.83l1.11,10.1ZM382.75,128.49c6.78,1.52,11.62,2.9,18.12,2.9,9.96,0,13.69-3.46,13.69-21.44s-3.32-22.27-12.03-22.27c-6.5,0-11.9,3.04-19.78,8.44v32.37Z"/>
27
+ <path class="cls-1" d="M505.18,109.54c0,26.01-8.99,35.69-33.34,35.69s-33.62-9.68-33.62-35.69,9.13-35.83,33.62-35.83,33.34,9.82,33.34,35.83ZM455.8,109.54c0,17.43,3.6,22.27,16.05,22.27s15.91-4.84,15.91-22.27-3.74-22.41-15.91-22.41-16.05,4.84-16.05,22.41Z"/>
28
+ <path class="cls-1" d="M578.64,109.54c0,26.01-8.99,35.69-33.34,35.69s-33.62-9.68-33.62-35.69,9.13-35.83,33.62-35.83,33.34,9.82,33.34,35.83ZM529.25,109.54c0,17.43,3.6,22.27,16.05,22.27s15.91-4.84,15.91-22.27-3.74-22.41-15.91-22.41-16.05,4.84-16.05,22.41Z"/>
29
+ <path class="cls-1" d="M602.16,122.27c-.14,5.39,2.63,7.88,8.02,7.88h5.81l1.94,12.45c-3.04,1.52-10.52,2.35-14.39,2.35-11.21,0-18.26-6.64-18.26-18.54V46.59h16.88v75.67Z"/>
30
+ </g>
31
+ <path class="cls-2" d="M85.52,192.79c-2.6,0-5.1-.62-7.03-1.74L7.04,149.8C3.02,147.49,0,142.25,0,137.61V55.11C0,50.48,3.02,45.24,7.04,42.93L78.48,1.68c3.88-2.24,10.19-2.24,14.07,0l71.45,41.25c4.01,2.32,7.04,7.55,7.04,12.19v82.5c0,4.63-3.02,9.87-7.04,12.19l-71.45,41.25c-1.94,1.12-4.44,1.74-7.04,1.74ZM85.52,8.09c-1.16,0-2.26.24-2.96.65L11.11,49.98c-1.5.86-2.96,3.41-2.96,5.13v82.5c0,1.73,1.47,4.27,2.96,5.13l71.45,41.25c1.4.81,4.53.81,5.93,0l71.45-41.25c1.5-.86,2.96-3.4,2.96-5.13V55.11c0-1.73-1.47-4.27-2.96-5.13L88.48,8.73c-.7-.4-1.81-.65-2.96-.65ZM85.69,165.46h0s59.76-34.67,59.76-34.67v-19.74l-59.76,34.64-25.62-14.79v19.75l25.62,14.81h0s0,0,0,0h0ZM111.28,81.34l-25.6-14.83-25.6,14.83v29.74l25.58,14.8v.03l.02-.02.02.02v-.03l25.58-14.8v-29.61M145.45,61.72l-59.71-34.61-17.07,9.86,59.71,34.62v29.67l17.07-9.87v-29.67ZM25.9,61.77v68.85l17.07,9.88v-68.87l25.77-15.05-17.08-9.86-25.77,15.05Z"/>
32
+ </svg>