iobroker.beszel 0.1.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.
Files changed (46) hide show
  1. package/.github/auto-merge.yml +2 -0
  2. package/.github/dependabot.yml +12 -0
  3. package/.github/workflows/automerge-dependabot.yml +32 -0
  4. package/.github/workflows/test-and-release.yml +62 -0
  5. package/.vscode/settings.json +12 -0
  6. package/CHANGELOG.md +13 -0
  7. package/CLAUDE.md +91 -0
  8. package/LICENSE +21 -0
  9. package/README.md +187 -0
  10. package/admin/beszel.svg +9 -0
  11. package/admin/i18n/de/translations.json +43 -0
  12. package/admin/i18n/en/translations.json +43 -0
  13. package/admin/i18n/es/translations.json +43 -0
  14. package/admin/i18n/fr/translations.json +43 -0
  15. package/admin/i18n/it/translations.json +43 -0
  16. package/admin/i18n/nl/translations.json +43 -0
  17. package/admin/i18n/pl/translations.json +43 -0
  18. package/admin/i18n/pt/translations.json +43 -0
  19. package/admin/i18n/ru/translations.json +43 -0
  20. package/admin/i18n/uk/translations.json +43 -0
  21. package/admin/i18n/zh-cn/translations.json +43 -0
  22. package/admin/jsonConfig.json +240 -0
  23. package/build/lib/beszel-client.d.ts +39 -0
  24. package/build/lib/beszel-client.d.ts.map +1 -0
  25. package/build/lib/beszel-client.js +199 -0
  26. package/build/lib/state-manager.d.ts +47 -0
  27. package/build/lib/state-manager.d.ts.map +1 -0
  28. package/build/lib/state-manager.js +738 -0
  29. package/build/lib/types.d.ts +174 -0
  30. package/build/lib/types.d.ts.map +1 -0
  31. package/build/lib/types.js +2 -0
  32. package/build/main.d.ts +2 -0
  33. package/build/main.d.ts.map +1 -0
  34. package/build/main.js +191 -0
  35. package/eslint.config.mjs +36 -0
  36. package/io-package.json +162 -0
  37. package/package.json +61 -0
  38. package/scripts/version.js +28 -0
  39. package/src/lib/beszel-client.ts +216 -0
  40. package/src/lib/state-manager.ts +1050 -0
  41. package/src/lib/types.ts +192 -0
  42. package/src/main.ts +199 -0
  43. package/test/testPackageFiles.ts +5 -0
  44. package/tsconfig.build.json +7 -0
  45. package/tsconfig.json +24 -0
  46. package/tsconfig.test.json +9 -0
@@ -0,0 +1,1050 @@
1
+ import type * as utils from "@iobroker/adapter-core";
2
+ import type {
3
+ AdapterConfig,
4
+ BeszelContainer,
5
+ BeszelSystem,
6
+ SystemStats,
7
+ } from "./types.js";
8
+
9
+ type IoBrokerStateCommon = ioBroker.StateCommon;
10
+ type IoBrokerObjectCommon = ioBroker.ObjectCommon;
11
+
12
+ /**
13
+ * Manages creation and updates of ioBroker states for Beszel systems.
14
+ */
15
+ export class StateManager {
16
+ private readonly adapter: utils.AdapterInstance;
17
+
18
+ constructor(adapter: utils.AdapterInstance) {
19
+ this.adapter = adapter;
20
+ }
21
+
22
+ /**
23
+ * Sanitize a name to a valid ioBroker state ID segment.
24
+ * Lowercase, replace non-alphanumeric with _, max 50 chars, trim underscores.
25
+ *
26
+ * @param name
27
+ */
28
+ public sanitize(name: string): string {
29
+ return name
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9]+/g, "_")
32
+ .replace(/^_+|_+$/g, "")
33
+ .slice(0, 50);
34
+ }
35
+
36
+ /**
37
+ * Update all states for a single system.
38
+ *
39
+ * @param system
40
+ * @param stats
41
+ * @param containers
42
+ * @param config
43
+ */
44
+ public async updateSystem(
45
+ system: BeszelSystem,
46
+ stats: SystemStats | undefined,
47
+ containers: BeszelContainer[],
48
+ config: AdapterConfig,
49
+ ): Promise<void> {
50
+ const sysId = `systems.${this.sanitize(system.name)}`;
51
+
52
+ // Create device object
53
+ await this.adapter.setObjectNotExistsAsync(sysId, {
54
+ type: "device",
55
+ common: { name: system.name } as IoBrokerObjectCommon,
56
+ native: { id: system.id, host: system.host },
57
+ });
58
+
59
+ // Always: online + status
60
+ const isUp = system.status === "up";
61
+ await this.createAndSetState(
62
+ `${sysId}.online`,
63
+ {
64
+ name: "Online",
65
+ type: "boolean",
66
+ role: "indicator.reachable",
67
+ read: true,
68
+ write: false,
69
+ } as IoBrokerStateCommon,
70
+ isUp,
71
+ );
72
+
73
+ await this.createAndSetState(
74
+ `${sysId}.status`,
75
+ {
76
+ name: "Status",
77
+ type: "string",
78
+ role: "text",
79
+ read: true,
80
+ write: false,
81
+ } as IoBrokerStateCommon,
82
+ system.status,
83
+ );
84
+
85
+ // Uptime
86
+ if (config.metrics_uptime) {
87
+ const uptime = system.info.u ?? null;
88
+
89
+ await this.createAndSetState(
90
+ `${sysId}.uptime`,
91
+ {
92
+ name: "Uptime",
93
+ type: "number",
94
+ role: "value",
95
+ unit: "s",
96
+ read: true,
97
+ write: false,
98
+ } as IoBrokerStateCommon,
99
+ uptime,
100
+ );
101
+
102
+ await this.createAndSetState(
103
+ `${sysId}.uptime_text`,
104
+ {
105
+ name: "Uptime (formatted)",
106
+ type: "string",
107
+ role: "text",
108
+ read: true,
109
+ write: false,
110
+ } as IoBrokerStateCommon,
111
+ uptime !== null ? this.formatUptime(uptime) : null,
112
+ );
113
+ }
114
+
115
+ // Agent version
116
+ if (config.metrics_agentVersion) {
117
+ await this.createAndSetState(
118
+ `${sysId}.agent_version`,
119
+ {
120
+ name: "Agent Version",
121
+ type: "string",
122
+ role: "text",
123
+ read: true,
124
+ write: false,
125
+ } as IoBrokerStateCommon,
126
+ system.info.v ?? null,
127
+ );
128
+ }
129
+
130
+ // Systemd services
131
+ if (config.metrics_services) {
132
+ const sv = system.info.sv;
133
+ await this.createAndSetState(
134
+ `${sysId}.services_total`,
135
+ {
136
+ name: "Services Total",
137
+ type: "number",
138
+ role: "value",
139
+ read: true,
140
+ write: false,
141
+ } as IoBrokerStateCommon,
142
+ sv?.[0] ?? null,
143
+ );
144
+
145
+ await this.createAndSetState(
146
+ `${sysId}.services_failed`,
147
+ {
148
+ name: "Services Failed",
149
+ type: "number",
150
+ role: "value.warning",
151
+ read: true,
152
+ write: false,
153
+ } as IoBrokerStateCommon,
154
+ sv?.[1] ?? null,
155
+ );
156
+ }
157
+
158
+ // Stats-based metrics (only if stats available)
159
+ if (stats) {
160
+ await this.updateStatsStates(sysId, system, stats, config);
161
+ }
162
+
163
+ // Load avg fallback to system.info.la if no stats
164
+ if (config.metrics_loadAvg && !stats && system.info.la) {
165
+ const la = system.info.la;
166
+ await this.createAndSetState(
167
+ `${sysId}.load_avg_1m`,
168
+ {
169
+ name: "Load Average 1m",
170
+ type: "number",
171
+ role: "value",
172
+ read: true,
173
+ write: false,
174
+ } as IoBrokerStateCommon,
175
+ la[0],
176
+ );
177
+ await this.createAndSetState(
178
+ `${sysId}.load_avg_5m`,
179
+ {
180
+ name: "Load Average 5m",
181
+ type: "number",
182
+ role: "value",
183
+ read: true,
184
+ write: false,
185
+ } as IoBrokerStateCommon,
186
+ la[1],
187
+ );
188
+ await this.createAndSetState(
189
+ `${sysId}.load_avg_15m`,
190
+ {
191
+ name: "Load Average 15m",
192
+ type: "number",
193
+ role: "value",
194
+ read: true,
195
+ write: false,
196
+ } as IoBrokerStateCommon,
197
+ la[2],
198
+ );
199
+ }
200
+
201
+ // Containers
202
+ if (config.metrics_containers) {
203
+ await this.updateContainers(sysId, system.id, containers, config);
204
+ } else {
205
+ await this.deleteChannelIfExists(`${sysId}.containers`);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Remove device objects for systems that are no longer in Beszel.
211
+ *
212
+ * @param activeSystemNames
213
+ */
214
+ public async cleanupSystems(activeSystemNames: string[]): Promise<void> {
215
+ const activeIds = new Set(
216
+ activeSystemNames.map((n) => `systems.${this.sanitize(n)}`),
217
+ );
218
+
219
+ const objects = await this.adapter.getObjectViewAsync("system", "device", {
220
+ startkey: `${this.adapter.namespace}.systems.`,
221
+ endkey: `${this.adapter.namespace}.systems.\u9999`,
222
+ });
223
+
224
+ if (!objects?.rows) {
225
+ return;
226
+ }
227
+
228
+ for (const row of objects.rows) {
229
+ const id = row.id;
230
+ // Extract the relative id part
231
+ const relativeId = id.startsWith(`${this.adapter.namespace}.`)
232
+ ? id.slice(this.adapter.namespace.length + 1)
233
+ : id;
234
+
235
+ // Only delete direct children of "systems." (one level deep)
236
+ const parts = relativeId.split(".");
237
+ if (
238
+ parts.length === 2 &&
239
+ parts[0] === "systems" &&
240
+ !activeIds.has(relativeId)
241
+ ) {
242
+ this.adapter.log.info(`Removing stale system: ${relativeId}`);
243
+ await this.adapter.delObjectAsync(relativeId, { recursive: true });
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Delete states for metrics that have been disabled in the config.
250
+ * Called on startup to clean up previously-enabled states.
251
+ *
252
+ * @param systemId
253
+ * @param config
254
+ */
255
+ public async cleanupMetrics(
256
+ systemId: string,
257
+ config: AdapterConfig,
258
+ ): Promise<void> {
259
+ const sysId = `systems.${systemId}`;
260
+ const toDelete: string[] = [];
261
+
262
+ if (!config.metrics_uptime) {
263
+ toDelete.push(`${sysId}.uptime`, `${sysId}.uptime_text`);
264
+ }
265
+ if (!config.metrics_agentVersion) {
266
+ toDelete.push(`${sysId}.agent_version`);
267
+ }
268
+ if (!config.metrics_services) {
269
+ toDelete.push(`${sysId}.services_total`, `${sysId}.services_failed`);
270
+ }
271
+ if (!config.metrics_cpu) {
272
+ toDelete.push(`${sysId}.cpu_usage`);
273
+ }
274
+ if (!config.metrics_loadAvg) {
275
+ toDelete.push(
276
+ `${sysId}.load_avg_1m`,
277
+ `${sysId}.load_avg_5m`,
278
+ `${sysId}.load_avg_15m`,
279
+ );
280
+ }
281
+ if (!config.metrics_cpuBreakdown) {
282
+ toDelete.push(
283
+ `${sysId}.cpu_user`,
284
+ `${sysId}.cpu_system`,
285
+ `${sysId}.cpu_iowait`,
286
+ `${sysId}.cpu_steal`,
287
+ `${sysId}.cpu_idle`,
288
+ );
289
+ }
290
+ if (!config.metrics_memory) {
291
+ toDelete.push(
292
+ `${sysId}.memory_percent`,
293
+ `${sysId}.memory_used`,
294
+ `${sysId}.memory_total`,
295
+ );
296
+ }
297
+ if (!config.metrics_memoryDetails) {
298
+ toDelete.push(`${sysId}.memory_buffers`, `${sysId}.memory_zfs_arc`);
299
+ }
300
+ if (!config.metrics_swap) {
301
+ toDelete.push(`${sysId}.swap_used`, `${sysId}.swap_total`);
302
+ }
303
+ if (!config.metrics_disk) {
304
+ toDelete.push(
305
+ `${sysId}.disk_percent`,
306
+ `${sysId}.disk_used`,
307
+ `${sysId}.disk_total`,
308
+ );
309
+ }
310
+ if (!config.metrics_diskSpeed) {
311
+ toDelete.push(`${sysId}.disk_read`, `${sysId}.disk_write`);
312
+ }
313
+ if (!config.metrics_network) {
314
+ toDelete.push(`${sysId}.network_sent`, `${sysId}.network_recv`);
315
+ }
316
+ if (!config.metrics_temperature) {
317
+ toDelete.push(`${sysId}.temperature`);
318
+ }
319
+ if (!config.metrics_battery) {
320
+ toDelete.push(`${sysId}.battery_percent`, `${sysId}.battery_charging`);
321
+ }
322
+
323
+ for (const id of toDelete) {
324
+ const obj = await this.adapter.getObjectAsync(id);
325
+ if (obj) {
326
+ await this.adapter.delObjectAsync(id);
327
+ }
328
+ }
329
+
330
+ // Channels
331
+ if (!config.metrics_temperatureDetails) {
332
+ await this.deleteChannelIfExists(`${sysId}.temperatures`);
333
+ }
334
+ if (!config.metrics_gpu) {
335
+ await this.deleteChannelIfExists(`${sysId}.gpu`);
336
+ }
337
+ if (!config.metrics_extraFs) {
338
+ await this.deleteChannelIfExists(`${sysId}.filesystems`);
339
+ }
340
+ if (!config.metrics_containers) {
341
+ await this.deleteChannelIfExists(`${sysId}.containers`);
342
+ }
343
+ }
344
+
345
+ // -------------------------------------------------------------------------
346
+ // Private helpers
347
+ // -------------------------------------------------------------------------
348
+
349
+ private async updateStatsStates(
350
+ sysId: string,
351
+ system: BeszelSystem,
352
+ stats: SystemStats,
353
+ config: AdapterConfig,
354
+ ): Promise<void> {
355
+ // CPU
356
+ if (config.metrics_cpu) {
357
+ await this.createAndSetState(
358
+ `${sysId}.cpu_usage`,
359
+ {
360
+ name: "CPU Usage",
361
+ type: "number",
362
+ role: "level",
363
+ unit: "%",
364
+ min: 0,
365
+ max: 100,
366
+ read: true,
367
+ write: false,
368
+ } as IoBrokerStateCommon,
369
+ stats.cpu ?? null,
370
+ );
371
+ }
372
+
373
+ // Load avg — prefer stats.la, fallback to system.info.la
374
+ if (config.metrics_loadAvg) {
375
+ const la = stats.la ?? system.info.la;
376
+ await this.createAndSetState(
377
+ `${sysId}.load_avg_1m`,
378
+ {
379
+ name: "Load Average 1m",
380
+ type: "number",
381
+ role: "value",
382
+ read: true,
383
+ write: false,
384
+ } as IoBrokerStateCommon,
385
+ la?.[0] ?? null,
386
+ );
387
+ await this.createAndSetState(
388
+ `${sysId}.load_avg_5m`,
389
+ {
390
+ name: "Load Average 5m",
391
+ type: "number",
392
+ role: "value",
393
+ read: true,
394
+ write: false,
395
+ } as IoBrokerStateCommon,
396
+ la?.[1] ?? null,
397
+ );
398
+ await this.createAndSetState(
399
+ `${sysId}.load_avg_15m`,
400
+ {
401
+ name: "Load Average 15m",
402
+ type: "number",
403
+ role: "value",
404
+ read: true,
405
+ write: false,
406
+ } as IoBrokerStateCommon,
407
+ la?.[2] ?? null,
408
+ );
409
+ }
410
+
411
+ // CPU breakdown
412
+ if (config.metrics_cpuBreakdown && stats.cpub && stats.cpub.length >= 5) {
413
+ const [user, sys, iowait, steal, idle] = stats.cpub;
414
+ await this.createAndSetState(
415
+ `${sysId}.cpu_user`,
416
+ {
417
+ name: "CPU User %",
418
+ type: "number",
419
+ role: "level",
420
+ unit: "%",
421
+ min: 0,
422
+ max: 100,
423
+ read: true,
424
+ write: false,
425
+ } as IoBrokerStateCommon,
426
+ user,
427
+ );
428
+ await this.createAndSetState(
429
+ `${sysId}.cpu_system`,
430
+ {
431
+ name: "CPU System %",
432
+ type: "number",
433
+ role: "level",
434
+ unit: "%",
435
+ min: 0,
436
+ max: 100,
437
+ read: true,
438
+ write: false,
439
+ } as IoBrokerStateCommon,
440
+ sys,
441
+ );
442
+ await this.createAndSetState(
443
+ `${sysId}.cpu_iowait`,
444
+ {
445
+ name: "CPU IOWait %",
446
+ type: "number",
447
+ role: "level",
448
+ unit: "%",
449
+ min: 0,
450
+ max: 100,
451
+ read: true,
452
+ write: false,
453
+ } as IoBrokerStateCommon,
454
+ iowait,
455
+ );
456
+ await this.createAndSetState(
457
+ `${sysId}.cpu_steal`,
458
+ {
459
+ name: "CPU Steal %",
460
+ type: "number",
461
+ role: "level",
462
+ unit: "%",
463
+ min: 0,
464
+ max: 100,
465
+ read: true,
466
+ write: false,
467
+ } as IoBrokerStateCommon,
468
+ steal,
469
+ );
470
+ await this.createAndSetState(
471
+ `${sysId}.cpu_idle`,
472
+ {
473
+ name: "CPU Idle %",
474
+ type: "number",
475
+ role: "level",
476
+ unit: "%",
477
+ min: 0,
478
+ max: 100,
479
+ read: true,
480
+ write: false,
481
+ } as IoBrokerStateCommon,
482
+ idle,
483
+ );
484
+ }
485
+
486
+ // Memory
487
+ if (config.metrics_memory) {
488
+ await this.createAndSetState(
489
+ `${sysId}.memory_percent`,
490
+ {
491
+ name: "Memory %",
492
+ type: "number",
493
+ role: "level",
494
+ unit: "%",
495
+ min: 0,
496
+ max: 100,
497
+ read: true,
498
+ write: false,
499
+ } as IoBrokerStateCommon,
500
+ stats.mp ?? null,
501
+ );
502
+ await this.createAndSetState(
503
+ `${sysId}.memory_used`,
504
+ {
505
+ name: "Memory Used",
506
+ type: "number",
507
+ role: "value",
508
+ unit: "GB",
509
+ read: true,
510
+ write: false,
511
+ } as IoBrokerStateCommon,
512
+ stats.mu ?? null,
513
+ );
514
+ await this.createAndSetState(
515
+ `${sysId}.memory_total`,
516
+ {
517
+ name: "Memory Total",
518
+ type: "number",
519
+ role: "value",
520
+ unit: "GB",
521
+ read: true,
522
+ write: false,
523
+ } as IoBrokerStateCommon,
524
+ stats.m ?? null,
525
+ );
526
+ }
527
+
528
+ // Memory details
529
+ if (config.metrics_memoryDetails) {
530
+ await this.createAndSetState(
531
+ `${sysId}.memory_buffers`,
532
+ {
533
+ name: "Memory Buffers+Cache",
534
+ type: "number",
535
+ role: "value",
536
+ unit: "GB",
537
+ read: true,
538
+ write: false,
539
+ } as IoBrokerStateCommon,
540
+ stats.mb ?? null,
541
+ );
542
+ await this.createAndSetState(
543
+ `${sysId}.memory_zfs_arc`,
544
+ {
545
+ name: "Memory ZFS ARC",
546
+ type: "number",
547
+ role: "value",
548
+ unit: "GB",
549
+ read: true,
550
+ write: false,
551
+ } as IoBrokerStateCommon,
552
+ stats.mz ?? null,
553
+ );
554
+ }
555
+
556
+ // Swap
557
+ if (config.metrics_swap) {
558
+ await this.createAndSetState(
559
+ `${sysId}.swap_used`,
560
+ {
561
+ name: "Swap Used",
562
+ type: "number",
563
+ role: "value",
564
+ unit: "GB",
565
+ read: true,
566
+ write: false,
567
+ } as IoBrokerStateCommon,
568
+ stats.su ?? null,
569
+ );
570
+ await this.createAndSetState(
571
+ `${sysId}.swap_total`,
572
+ {
573
+ name: "Swap Total",
574
+ type: "number",
575
+ role: "value",
576
+ unit: "GB",
577
+ read: true,
578
+ write: false,
579
+ } as IoBrokerStateCommon,
580
+ stats.s ?? null,
581
+ );
582
+ }
583
+
584
+ // Disk
585
+ if (config.metrics_disk) {
586
+ await this.createAndSetState(
587
+ `${sysId}.disk_percent`,
588
+ {
589
+ name: "Disk %",
590
+ type: "number",
591
+ role: "level",
592
+ unit: "%",
593
+ min: 0,
594
+ max: 100,
595
+ read: true,
596
+ write: false,
597
+ } as IoBrokerStateCommon,
598
+ stats.dp ?? null,
599
+ );
600
+ await this.createAndSetState(
601
+ `${sysId}.disk_used`,
602
+ {
603
+ name: "Disk Used",
604
+ type: "number",
605
+ role: "value",
606
+ unit: "GB",
607
+ read: true,
608
+ write: false,
609
+ } as IoBrokerStateCommon,
610
+ stats.du ?? null,
611
+ );
612
+ await this.createAndSetState(
613
+ `${sysId}.disk_total`,
614
+ {
615
+ name: "Disk Total",
616
+ type: "number",
617
+ role: "value",
618
+ unit: "GB",
619
+ read: true,
620
+ write: false,
621
+ } as IoBrokerStateCommon,
622
+ stats.d ?? null,
623
+ );
624
+ }
625
+
626
+ // Disk speed
627
+ if (config.metrics_diskSpeed) {
628
+ await this.createAndSetState(
629
+ `${sysId}.disk_read`,
630
+ {
631
+ name: "Disk Read",
632
+ type: "number",
633
+ role: "value",
634
+ unit: "MB/s",
635
+ read: true,
636
+ write: false,
637
+ } as IoBrokerStateCommon,
638
+ stats.dr ?? null,
639
+ );
640
+ await this.createAndSetState(
641
+ `${sysId}.disk_write`,
642
+ {
643
+ name: "Disk Write",
644
+ type: "number",
645
+ role: "value",
646
+ unit: "MB/s",
647
+ read: true,
648
+ write: false,
649
+ } as IoBrokerStateCommon,
650
+ stats.dw ?? null,
651
+ );
652
+ }
653
+
654
+ // Network
655
+ if (config.metrics_network) {
656
+ await this.createAndSetState(
657
+ `${sysId}.network_sent`,
658
+ {
659
+ name: "Network Sent",
660
+ type: "number",
661
+ role: "value",
662
+ unit: "MB/s",
663
+ read: true,
664
+ write: false,
665
+ } as IoBrokerStateCommon,
666
+ stats.ns ?? null,
667
+ );
668
+ await this.createAndSetState(
669
+ `${sysId}.network_recv`,
670
+ {
671
+ name: "Network Received",
672
+ type: "number",
673
+ role: "value",
674
+ unit: "MB/s",
675
+ read: true,
676
+ write: false,
677
+ } as IoBrokerStateCommon,
678
+ stats.nr ?? null,
679
+ );
680
+ }
681
+
682
+ // Temperature (average of top 3)
683
+ if (config.metrics_temperature) {
684
+ const avgTemp = this.computeTopAvgTemp(stats.t);
685
+ await this.createAndSetState(
686
+ `${sysId}.temperature`,
687
+ {
688
+ name: "Temperature (avg top 3)",
689
+ type: "number",
690
+ role: "value.temperature",
691
+ unit: "°C",
692
+ read: true,
693
+ write: false,
694
+ } as IoBrokerStateCommon,
695
+ avgTemp,
696
+ );
697
+ }
698
+
699
+ // Temperature details
700
+ if (config.metrics_temperatureDetails && stats.t) {
701
+ await this.ensureChannel(`${sysId}.temperatures`, "Temperatures");
702
+ for (const [sensor, temp] of Object.entries(stats.t)) {
703
+ const sensorId = this.sanitize(sensor);
704
+ await this.createAndSetState(
705
+ `${sysId}.temperatures.${sensorId}`,
706
+ {
707
+ name: sensor,
708
+ type: "number",
709
+ role: "value.temperature",
710
+ unit: "°C",
711
+ read: true,
712
+ write: false,
713
+ } as IoBrokerStateCommon,
714
+ temp,
715
+ );
716
+ }
717
+ } else if (!config.metrics_temperatureDetails) {
718
+ await this.deleteChannelIfExists(`${sysId}.temperatures`);
719
+ }
720
+
721
+ // Battery
722
+ if (config.metrics_battery) {
723
+ const bat = stats.bat ?? system.info.bat;
724
+ await this.createAndSetState(
725
+ `${sysId}.battery_percent`,
726
+ {
727
+ name: "Battery %",
728
+ type: "number",
729
+ role: "value",
730
+ unit: "%",
731
+ min: 0,
732
+ max: 100,
733
+ read: true,
734
+ write: false,
735
+ } as IoBrokerStateCommon,
736
+ bat?.[0] ?? null,
737
+ );
738
+ await this.createAndSetState(
739
+ `${sysId}.battery_charging`,
740
+ {
741
+ name: "Battery Charging",
742
+ type: "boolean",
743
+ role: "indicator",
744
+ read: true,
745
+ write: false,
746
+ } as IoBrokerStateCommon,
747
+ bat ? bat[1] > 0 : null,
748
+ );
749
+ }
750
+
751
+ // GPU
752
+ if (config.metrics_gpu && stats.g && Object.keys(stats.g).length > 0) {
753
+ await this.ensureChannel(`${sysId}.gpu`, "GPU");
754
+ for (const [gpuId, gpuData] of Object.entries(stats.g)) {
755
+ const safeId = this.sanitize(gpuId);
756
+ const gpuLabel = gpuData.n ?? gpuId;
757
+ await this.ensureChannel(`${sysId}.gpu.${safeId}`, gpuLabel);
758
+ await this.createAndSetState(
759
+ `${sysId}.gpu.${safeId}.usage`,
760
+ {
761
+ name: "GPU Usage",
762
+ type: "number",
763
+ role: "level",
764
+ unit: "%",
765
+ min: 0,
766
+ max: 100,
767
+ read: true,
768
+ write: false,
769
+ } as IoBrokerStateCommon,
770
+ gpuData.u ?? null,
771
+ );
772
+ await this.createAndSetState(
773
+ `${sysId}.gpu.${safeId}.memory_used`,
774
+ {
775
+ name: "GPU Memory Used",
776
+ type: "number",
777
+ role: "value",
778
+ unit: "GB",
779
+ read: true,
780
+ write: false,
781
+ } as IoBrokerStateCommon,
782
+ gpuData.mu ?? null,
783
+ );
784
+ await this.createAndSetState(
785
+ `${sysId}.gpu.${safeId}.memory_total`,
786
+ {
787
+ name: "GPU Memory Total",
788
+ type: "number",
789
+ role: "value",
790
+ unit: "GB",
791
+ read: true,
792
+ write: false,
793
+ } as IoBrokerStateCommon,
794
+ gpuData.mt ?? null,
795
+ );
796
+ await this.createAndSetState(
797
+ `${sysId}.gpu.${safeId}.power`,
798
+ {
799
+ name: "GPU Power",
800
+ type: "number",
801
+ role: "value",
802
+ unit: "W",
803
+ read: true,
804
+ write: false,
805
+ } as IoBrokerStateCommon,
806
+ gpuData.p ?? null,
807
+ );
808
+ }
809
+ } else if (!config.metrics_gpu) {
810
+ await this.deleteChannelIfExists(`${sysId}.gpu`);
811
+ }
812
+
813
+ // Extra filesystems
814
+ if (
815
+ config.metrics_extraFs &&
816
+ stats.efs &&
817
+ Object.keys(stats.efs).length > 0
818
+ ) {
819
+ await this.ensureChannel(`${sysId}.filesystems`, "Filesystems");
820
+ for (const [fsName, fsData] of Object.entries(stats.efs)) {
821
+ const safeId = this.sanitize(fsName);
822
+ await this.ensureChannel(`${sysId}.filesystems.${safeId}`, fsName);
823
+
824
+ const total = fsData.d ?? null;
825
+ const used = fsData.du ?? null;
826
+ const percent =
827
+ total !== null && used !== null && total > 0
828
+ ? Math.round((used / total) * 100)
829
+ : null;
830
+
831
+ await this.createAndSetState(
832
+ `${sysId}.filesystems.${safeId}.disk_percent`,
833
+ {
834
+ name: "Disk %",
835
+ type: "number",
836
+ role: "level",
837
+ unit: "%",
838
+ min: 0,
839
+ max: 100,
840
+ read: true,
841
+ write: false,
842
+ } as IoBrokerStateCommon,
843
+ percent,
844
+ );
845
+ await this.createAndSetState(
846
+ `${sysId}.filesystems.${safeId}.disk_used`,
847
+ {
848
+ name: "Disk Used",
849
+ type: "number",
850
+ role: "value",
851
+ unit: "GB",
852
+ read: true,
853
+ write: false,
854
+ } as IoBrokerStateCommon,
855
+ used,
856
+ );
857
+ await this.createAndSetState(
858
+ `${sysId}.filesystems.${safeId}.disk_total`,
859
+ {
860
+ name: "Disk Total",
861
+ type: "number",
862
+ role: "value",
863
+ unit: "GB",
864
+ read: true,
865
+ write: false,
866
+ } as IoBrokerStateCommon,
867
+ total,
868
+ );
869
+ await this.createAndSetState(
870
+ `${sysId}.filesystems.${safeId}.read_speed`,
871
+ {
872
+ name: "Read Speed",
873
+ type: "number",
874
+ role: "value",
875
+ unit: "MB/s",
876
+ read: true,
877
+ write: false,
878
+ } as IoBrokerStateCommon,
879
+ fsData.r ?? null,
880
+ );
881
+ await this.createAndSetState(
882
+ `${sysId}.filesystems.${safeId}.write_speed`,
883
+ {
884
+ name: "Write Speed",
885
+ type: "number",
886
+ role: "value",
887
+ unit: "MB/s",
888
+ read: true,
889
+ write: false,
890
+ } as IoBrokerStateCommon,
891
+ fsData.w ?? null,
892
+ );
893
+ }
894
+ } else if (!config.metrics_extraFs) {
895
+ await this.deleteChannelIfExists(`${sysId}.filesystems`);
896
+ }
897
+ }
898
+
899
+ private async updateContainers(
900
+ sysId: string,
901
+ systemId: string,
902
+ allContainers: BeszelContainer[],
903
+ _config: AdapterConfig,
904
+ ): Promise<void> {
905
+ const sysContainers = allContainers.filter((c) => c.system === systemId);
906
+ if (sysContainers.length === 0) {
907
+ return;
908
+ }
909
+
910
+ await this.ensureChannel(`${sysId}.containers`, "Containers");
911
+
912
+ const healthLabels = ["none", "starting", "healthy", "unhealthy"];
913
+
914
+ for (const container of sysContainers) {
915
+ const cId = this.sanitize(container.name);
916
+ await this.ensureChannel(`${sysId}.containers.${cId}`, container.name);
917
+
918
+ await this.createAndSetState(
919
+ `${sysId}.containers.${cId}.status`,
920
+ {
921
+ name: "Status",
922
+ type: "string",
923
+ role: "text",
924
+ read: true,
925
+ write: false,
926
+ } as IoBrokerStateCommon,
927
+ container.status,
928
+ );
929
+
930
+ await this.createAndSetState(
931
+ `${sysId}.containers.${cId}.health`,
932
+ {
933
+ name: "Health",
934
+ type: "string",
935
+ role: "text",
936
+ read: true,
937
+ write: false,
938
+ } as IoBrokerStateCommon,
939
+ healthLabels[container.health] ?? "unknown",
940
+ );
941
+
942
+ await this.createAndSetState(
943
+ `${sysId}.containers.${cId}.cpu`,
944
+ {
945
+ name: "CPU Usage",
946
+ type: "number",
947
+ role: "level",
948
+ unit: "%",
949
+ min: 0,
950
+ max: 100,
951
+ read: true,
952
+ write: false,
953
+ } as IoBrokerStateCommon,
954
+ container.cpu,
955
+ );
956
+
957
+ await this.createAndSetState(
958
+ `${sysId}.containers.${cId}.memory`,
959
+ {
960
+ name: "Memory",
961
+ type: "number",
962
+ role: "value",
963
+ unit: "MB",
964
+ read: true,
965
+ write: false,
966
+ } as IoBrokerStateCommon,
967
+ container.memory,
968
+ );
969
+
970
+ await this.createAndSetState(
971
+ `${sysId}.containers.${cId}.image`,
972
+ {
973
+ name: "Image",
974
+ type: "string",
975
+ role: "text",
976
+ read: true,
977
+ write: false,
978
+ } as IoBrokerStateCommon,
979
+ container.image,
980
+ );
981
+ }
982
+ }
983
+
984
+ private async ensureChannel(id: string, name: string): Promise<void> {
985
+ await this.adapter.setObjectNotExistsAsync(id, {
986
+ type: "channel",
987
+ common: { name } as IoBrokerObjectCommon,
988
+ native: {},
989
+ });
990
+ }
991
+
992
+ private async deleteChannelIfExists(id: string): Promise<void> {
993
+ try {
994
+ const obj = await this.adapter.getObjectAsync(id);
995
+ if (obj) {
996
+ await this.adapter.delObjectAsync(id, { recursive: true });
997
+ }
998
+ } catch {
999
+ // ignore
1000
+ }
1001
+ }
1002
+
1003
+ private async createAndSetState(
1004
+ id: string,
1005
+ common: IoBrokerStateCommon,
1006
+ value: ioBroker.StateValue,
1007
+ ): Promise<void> {
1008
+ await this.adapter.setObjectNotExistsAsync(id, {
1009
+ type: "state",
1010
+ common,
1011
+ native: {},
1012
+ });
1013
+ await this.adapter.setStateAsync(id, { val: value, ack: true });
1014
+ }
1015
+
1016
+ private computeTopAvgTemp(
1017
+ temps: Record<string, number> | undefined,
1018
+ ): number | null {
1019
+ if (!temps) {
1020
+ return null;
1021
+ }
1022
+ const values = Object.values(temps).filter(
1023
+ (v) => typeof v === "number" && isFinite(v),
1024
+ );
1025
+ if (values.length === 0) {
1026
+ return null;
1027
+ }
1028
+ values.sort((a, b) => b - a);
1029
+ const top3 = values.slice(0, 3);
1030
+ const avg = top3.reduce((sum, v) => sum + v, 0) / top3.length;
1031
+ return Math.round(avg * 10) / 10;
1032
+ }
1033
+
1034
+ private formatUptime(seconds: number): string {
1035
+ const d = Math.floor(seconds / 86400);
1036
+ const h = Math.floor((seconds % 86400) / 3600);
1037
+ const m = Math.floor((seconds % 3600) / 60);
1038
+ const parts: string[] = [];
1039
+ if (d > 0) {
1040
+ parts.push(`${d}d`);
1041
+ }
1042
+ if (h > 0) {
1043
+ parts.push(`${h}h`);
1044
+ }
1045
+ if (m > 0 || parts.length === 0) {
1046
+ parts.push(`${m}m`);
1047
+ }
1048
+ return parts.join(" ");
1049
+ }
1050
+ }