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,738 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StateManager = void 0;
4
+ /**
5
+ * Manages creation and updates of ioBroker states for Beszel systems.
6
+ */
7
+ class StateManager {
8
+ adapter;
9
+ constructor(adapter) {
10
+ this.adapter = adapter;
11
+ }
12
+ /**
13
+ * Sanitize a name to a valid ioBroker state ID segment.
14
+ * Lowercase, replace non-alphanumeric with _, max 50 chars, trim underscores.
15
+ *
16
+ * @param name
17
+ */
18
+ sanitize(name) {
19
+ return name
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, "_")
22
+ .replace(/^_+|_+$/g, "")
23
+ .slice(0, 50);
24
+ }
25
+ /**
26
+ * Update all states for a single system.
27
+ *
28
+ * @param system
29
+ * @param stats
30
+ * @param containers
31
+ * @param config
32
+ */
33
+ async updateSystem(system, stats, containers, config) {
34
+ const sysId = `systems.${this.sanitize(system.name)}`;
35
+ // Create device object
36
+ await this.adapter.setObjectNotExistsAsync(sysId, {
37
+ type: "device",
38
+ common: { name: system.name },
39
+ native: { id: system.id, host: system.host },
40
+ });
41
+ // Always: online + status
42
+ const isUp = system.status === "up";
43
+ await this.createAndSetState(`${sysId}.online`, {
44
+ name: "Online",
45
+ type: "boolean",
46
+ role: "indicator.reachable",
47
+ read: true,
48
+ write: false,
49
+ }, isUp);
50
+ await this.createAndSetState(`${sysId}.status`, {
51
+ name: "Status",
52
+ type: "string",
53
+ role: "text",
54
+ read: true,
55
+ write: false,
56
+ }, system.status);
57
+ // Uptime
58
+ if (config.metrics_uptime) {
59
+ const uptime = system.info.u ?? null;
60
+ await this.createAndSetState(`${sysId}.uptime`, {
61
+ name: "Uptime",
62
+ type: "number",
63
+ role: "value",
64
+ unit: "s",
65
+ read: true,
66
+ write: false,
67
+ }, uptime);
68
+ await this.createAndSetState(`${sysId}.uptime_text`, {
69
+ name: "Uptime (formatted)",
70
+ type: "string",
71
+ role: "text",
72
+ read: true,
73
+ write: false,
74
+ }, uptime !== null ? this.formatUptime(uptime) : null);
75
+ }
76
+ // Agent version
77
+ if (config.metrics_agentVersion) {
78
+ await this.createAndSetState(`${sysId}.agent_version`, {
79
+ name: "Agent Version",
80
+ type: "string",
81
+ role: "text",
82
+ read: true,
83
+ write: false,
84
+ }, system.info.v ?? null);
85
+ }
86
+ // Systemd services
87
+ if (config.metrics_services) {
88
+ const sv = system.info.sv;
89
+ await this.createAndSetState(`${sysId}.services_total`, {
90
+ name: "Services Total",
91
+ type: "number",
92
+ role: "value",
93
+ read: true,
94
+ write: false,
95
+ }, sv?.[0] ?? null);
96
+ await this.createAndSetState(`${sysId}.services_failed`, {
97
+ name: "Services Failed",
98
+ type: "number",
99
+ role: "value.warning",
100
+ read: true,
101
+ write: false,
102
+ }, sv?.[1] ?? null);
103
+ }
104
+ // Stats-based metrics (only if stats available)
105
+ if (stats) {
106
+ await this.updateStatsStates(sysId, system, stats, config);
107
+ }
108
+ // Load avg fallback to system.info.la if no stats
109
+ if (config.metrics_loadAvg && !stats && system.info.la) {
110
+ const la = system.info.la;
111
+ await this.createAndSetState(`${sysId}.load_avg_1m`, {
112
+ name: "Load Average 1m",
113
+ type: "number",
114
+ role: "value",
115
+ read: true,
116
+ write: false,
117
+ }, la[0]);
118
+ await this.createAndSetState(`${sysId}.load_avg_5m`, {
119
+ name: "Load Average 5m",
120
+ type: "number",
121
+ role: "value",
122
+ read: true,
123
+ write: false,
124
+ }, la[1]);
125
+ await this.createAndSetState(`${sysId}.load_avg_15m`, {
126
+ name: "Load Average 15m",
127
+ type: "number",
128
+ role: "value",
129
+ read: true,
130
+ write: false,
131
+ }, la[2]);
132
+ }
133
+ // Containers
134
+ if (config.metrics_containers) {
135
+ await this.updateContainers(sysId, system.id, containers, config);
136
+ }
137
+ else {
138
+ await this.deleteChannelIfExists(`${sysId}.containers`);
139
+ }
140
+ }
141
+ /**
142
+ * Remove device objects for systems that are no longer in Beszel.
143
+ *
144
+ * @param activeSystemNames
145
+ */
146
+ async cleanupSystems(activeSystemNames) {
147
+ const activeIds = new Set(activeSystemNames.map((n) => `systems.${this.sanitize(n)}`));
148
+ const objects = await this.adapter.getObjectViewAsync("system", "device", {
149
+ startkey: `${this.adapter.namespace}.systems.`,
150
+ endkey: `${this.adapter.namespace}.systems.\u9999`,
151
+ });
152
+ if (!objects?.rows) {
153
+ return;
154
+ }
155
+ for (const row of objects.rows) {
156
+ const id = row.id;
157
+ // Extract the relative id part
158
+ const relativeId = id.startsWith(`${this.adapter.namespace}.`)
159
+ ? id.slice(this.adapter.namespace.length + 1)
160
+ : id;
161
+ // Only delete direct children of "systems." (one level deep)
162
+ const parts = relativeId.split(".");
163
+ if (parts.length === 2 &&
164
+ parts[0] === "systems" &&
165
+ !activeIds.has(relativeId)) {
166
+ this.adapter.log.info(`Removing stale system: ${relativeId}`);
167
+ await this.adapter.delObjectAsync(relativeId, { recursive: true });
168
+ }
169
+ }
170
+ }
171
+ /**
172
+ * Delete states for metrics that have been disabled in the config.
173
+ * Called on startup to clean up previously-enabled states.
174
+ *
175
+ * @param systemId
176
+ * @param config
177
+ */
178
+ async cleanupMetrics(systemId, config) {
179
+ const sysId = `systems.${systemId}`;
180
+ const toDelete = [];
181
+ if (!config.metrics_uptime) {
182
+ toDelete.push(`${sysId}.uptime`, `${sysId}.uptime_text`);
183
+ }
184
+ if (!config.metrics_agentVersion) {
185
+ toDelete.push(`${sysId}.agent_version`);
186
+ }
187
+ if (!config.metrics_services) {
188
+ toDelete.push(`${sysId}.services_total`, `${sysId}.services_failed`);
189
+ }
190
+ if (!config.metrics_cpu) {
191
+ toDelete.push(`${sysId}.cpu_usage`);
192
+ }
193
+ if (!config.metrics_loadAvg) {
194
+ toDelete.push(`${sysId}.load_avg_1m`, `${sysId}.load_avg_5m`, `${sysId}.load_avg_15m`);
195
+ }
196
+ if (!config.metrics_cpuBreakdown) {
197
+ toDelete.push(`${sysId}.cpu_user`, `${sysId}.cpu_system`, `${sysId}.cpu_iowait`, `${sysId}.cpu_steal`, `${sysId}.cpu_idle`);
198
+ }
199
+ if (!config.metrics_memory) {
200
+ toDelete.push(`${sysId}.memory_percent`, `${sysId}.memory_used`, `${sysId}.memory_total`);
201
+ }
202
+ if (!config.metrics_memoryDetails) {
203
+ toDelete.push(`${sysId}.memory_buffers`, `${sysId}.memory_zfs_arc`);
204
+ }
205
+ if (!config.metrics_swap) {
206
+ toDelete.push(`${sysId}.swap_used`, `${sysId}.swap_total`);
207
+ }
208
+ if (!config.metrics_disk) {
209
+ toDelete.push(`${sysId}.disk_percent`, `${sysId}.disk_used`, `${sysId}.disk_total`);
210
+ }
211
+ if (!config.metrics_diskSpeed) {
212
+ toDelete.push(`${sysId}.disk_read`, `${sysId}.disk_write`);
213
+ }
214
+ if (!config.metrics_network) {
215
+ toDelete.push(`${sysId}.network_sent`, `${sysId}.network_recv`);
216
+ }
217
+ if (!config.metrics_temperature) {
218
+ toDelete.push(`${sysId}.temperature`);
219
+ }
220
+ if (!config.metrics_battery) {
221
+ toDelete.push(`${sysId}.battery_percent`, `${sysId}.battery_charging`);
222
+ }
223
+ for (const id of toDelete) {
224
+ const obj = await this.adapter.getObjectAsync(id);
225
+ if (obj) {
226
+ await this.adapter.delObjectAsync(id);
227
+ }
228
+ }
229
+ // Channels
230
+ if (!config.metrics_temperatureDetails) {
231
+ await this.deleteChannelIfExists(`${sysId}.temperatures`);
232
+ }
233
+ if (!config.metrics_gpu) {
234
+ await this.deleteChannelIfExists(`${sysId}.gpu`);
235
+ }
236
+ if (!config.metrics_extraFs) {
237
+ await this.deleteChannelIfExists(`${sysId}.filesystems`);
238
+ }
239
+ if (!config.metrics_containers) {
240
+ await this.deleteChannelIfExists(`${sysId}.containers`);
241
+ }
242
+ }
243
+ // -------------------------------------------------------------------------
244
+ // Private helpers
245
+ // -------------------------------------------------------------------------
246
+ async updateStatsStates(sysId, system, stats, config) {
247
+ // CPU
248
+ if (config.metrics_cpu) {
249
+ await this.createAndSetState(`${sysId}.cpu_usage`, {
250
+ name: "CPU Usage",
251
+ type: "number",
252
+ role: "level",
253
+ unit: "%",
254
+ min: 0,
255
+ max: 100,
256
+ read: true,
257
+ write: false,
258
+ }, stats.cpu ?? null);
259
+ }
260
+ // Load avg — prefer stats.la, fallback to system.info.la
261
+ if (config.metrics_loadAvg) {
262
+ const la = stats.la ?? system.info.la;
263
+ await this.createAndSetState(`${sysId}.load_avg_1m`, {
264
+ name: "Load Average 1m",
265
+ type: "number",
266
+ role: "value",
267
+ read: true,
268
+ write: false,
269
+ }, la?.[0] ?? null);
270
+ await this.createAndSetState(`${sysId}.load_avg_5m`, {
271
+ name: "Load Average 5m",
272
+ type: "number",
273
+ role: "value",
274
+ read: true,
275
+ write: false,
276
+ }, la?.[1] ?? null);
277
+ await this.createAndSetState(`${sysId}.load_avg_15m`, {
278
+ name: "Load Average 15m",
279
+ type: "number",
280
+ role: "value",
281
+ read: true,
282
+ write: false,
283
+ }, la?.[2] ?? null);
284
+ }
285
+ // CPU breakdown
286
+ if (config.metrics_cpuBreakdown && stats.cpub && stats.cpub.length >= 5) {
287
+ const [user, sys, iowait, steal, idle] = stats.cpub;
288
+ await this.createAndSetState(`${sysId}.cpu_user`, {
289
+ name: "CPU User %",
290
+ type: "number",
291
+ role: "level",
292
+ unit: "%",
293
+ min: 0,
294
+ max: 100,
295
+ read: true,
296
+ write: false,
297
+ }, user);
298
+ await this.createAndSetState(`${sysId}.cpu_system`, {
299
+ name: "CPU System %",
300
+ type: "number",
301
+ role: "level",
302
+ unit: "%",
303
+ min: 0,
304
+ max: 100,
305
+ read: true,
306
+ write: false,
307
+ }, sys);
308
+ await this.createAndSetState(`${sysId}.cpu_iowait`, {
309
+ name: "CPU IOWait %",
310
+ type: "number",
311
+ role: "level",
312
+ unit: "%",
313
+ min: 0,
314
+ max: 100,
315
+ read: true,
316
+ write: false,
317
+ }, iowait);
318
+ await this.createAndSetState(`${sysId}.cpu_steal`, {
319
+ name: "CPU Steal %",
320
+ type: "number",
321
+ role: "level",
322
+ unit: "%",
323
+ min: 0,
324
+ max: 100,
325
+ read: true,
326
+ write: false,
327
+ }, steal);
328
+ await this.createAndSetState(`${sysId}.cpu_idle`, {
329
+ name: "CPU Idle %",
330
+ type: "number",
331
+ role: "level",
332
+ unit: "%",
333
+ min: 0,
334
+ max: 100,
335
+ read: true,
336
+ write: false,
337
+ }, idle);
338
+ }
339
+ // Memory
340
+ if (config.metrics_memory) {
341
+ await this.createAndSetState(`${sysId}.memory_percent`, {
342
+ name: "Memory %",
343
+ type: "number",
344
+ role: "level",
345
+ unit: "%",
346
+ min: 0,
347
+ max: 100,
348
+ read: true,
349
+ write: false,
350
+ }, stats.mp ?? null);
351
+ await this.createAndSetState(`${sysId}.memory_used`, {
352
+ name: "Memory Used",
353
+ type: "number",
354
+ role: "value",
355
+ unit: "GB",
356
+ read: true,
357
+ write: false,
358
+ }, stats.mu ?? null);
359
+ await this.createAndSetState(`${sysId}.memory_total`, {
360
+ name: "Memory Total",
361
+ type: "number",
362
+ role: "value",
363
+ unit: "GB",
364
+ read: true,
365
+ write: false,
366
+ }, stats.m ?? null);
367
+ }
368
+ // Memory details
369
+ if (config.metrics_memoryDetails) {
370
+ await this.createAndSetState(`${sysId}.memory_buffers`, {
371
+ name: "Memory Buffers+Cache",
372
+ type: "number",
373
+ role: "value",
374
+ unit: "GB",
375
+ read: true,
376
+ write: false,
377
+ }, stats.mb ?? null);
378
+ await this.createAndSetState(`${sysId}.memory_zfs_arc`, {
379
+ name: "Memory ZFS ARC",
380
+ type: "number",
381
+ role: "value",
382
+ unit: "GB",
383
+ read: true,
384
+ write: false,
385
+ }, stats.mz ?? null);
386
+ }
387
+ // Swap
388
+ if (config.metrics_swap) {
389
+ await this.createAndSetState(`${sysId}.swap_used`, {
390
+ name: "Swap Used",
391
+ type: "number",
392
+ role: "value",
393
+ unit: "GB",
394
+ read: true,
395
+ write: false,
396
+ }, stats.su ?? null);
397
+ await this.createAndSetState(`${sysId}.swap_total`, {
398
+ name: "Swap Total",
399
+ type: "number",
400
+ role: "value",
401
+ unit: "GB",
402
+ read: true,
403
+ write: false,
404
+ }, stats.s ?? null);
405
+ }
406
+ // Disk
407
+ if (config.metrics_disk) {
408
+ await this.createAndSetState(`${sysId}.disk_percent`, {
409
+ name: "Disk %",
410
+ type: "number",
411
+ role: "level",
412
+ unit: "%",
413
+ min: 0,
414
+ max: 100,
415
+ read: true,
416
+ write: false,
417
+ }, stats.dp ?? null);
418
+ await this.createAndSetState(`${sysId}.disk_used`, {
419
+ name: "Disk Used",
420
+ type: "number",
421
+ role: "value",
422
+ unit: "GB",
423
+ read: true,
424
+ write: false,
425
+ }, stats.du ?? null);
426
+ await this.createAndSetState(`${sysId}.disk_total`, {
427
+ name: "Disk Total",
428
+ type: "number",
429
+ role: "value",
430
+ unit: "GB",
431
+ read: true,
432
+ write: false,
433
+ }, stats.d ?? null);
434
+ }
435
+ // Disk speed
436
+ if (config.metrics_diskSpeed) {
437
+ await this.createAndSetState(`${sysId}.disk_read`, {
438
+ name: "Disk Read",
439
+ type: "number",
440
+ role: "value",
441
+ unit: "MB/s",
442
+ read: true,
443
+ write: false,
444
+ }, stats.dr ?? null);
445
+ await this.createAndSetState(`${sysId}.disk_write`, {
446
+ name: "Disk Write",
447
+ type: "number",
448
+ role: "value",
449
+ unit: "MB/s",
450
+ read: true,
451
+ write: false,
452
+ }, stats.dw ?? null);
453
+ }
454
+ // Network
455
+ if (config.metrics_network) {
456
+ await this.createAndSetState(`${sysId}.network_sent`, {
457
+ name: "Network Sent",
458
+ type: "number",
459
+ role: "value",
460
+ unit: "MB/s",
461
+ read: true,
462
+ write: false,
463
+ }, stats.ns ?? null);
464
+ await this.createAndSetState(`${sysId}.network_recv`, {
465
+ name: "Network Received",
466
+ type: "number",
467
+ role: "value",
468
+ unit: "MB/s",
469
+ read: true,
470
+ write: false,
471
+ }, stats.nr ?? null);
472
+ }
473
+ // Temperature (average of top 3)
474
+ if (config.metrics_temperature) {
475
+ const avgTemp = this.computeTopAvgTemp(stats.t);
476
+ await this.createAndSetState(`${sysId}.temperature`, {
477
+ name: "Temperature (avg top 3)",
478
+ type: "number",
479
+ role: "value.temperature",
480
+ unit: "°C",
481
+ read: true,
482
+ write: false,
483
+ }, avgTemp);
484
+ }
485
+ // Temperature details
486
+ if (config.metrics_temperatureDetails && stats.t) {
487
+ await this.ensureChannel(`${sysId}.temperatures`, "Temperatures");
488
+ for (const [sensor, temp] of Object.entries(stats.t)) {
489
+ const sensorId = this.sanitize(sensor);
490
+ await this.createAndSetState(`${sysId}.temperatures.${sensorId}`, {
491
+ name: sensor,
492
+ type: "number",
493
+ role: "value.temperature",
494
+ unit: "°C",
495
+ read: true,
496
+ write: false,
497
+ }, temp);
498
+ }
499
+ }
500
+ else if (!config.metrics_temperatureDetails) {
501
+ await this.deleteChannelIfExists(`${sysId}.temperatures`);
502
+ }
503
+ // Battery
504
+ if (config.metrics_battery) {
505
+ const bat = stats.bat ?? system.info.bat;
506
+ await this.createAndSetState(`${sysId}.battery_percent`, {
507
+ name: "Battery %",
508
+ type: "number",
509
+ role: "value",
510
+ unit: "%",
511
+ min: 0,
512
+ max: 100,
513
+ read: true,
514
+ write: false,
515
+ }, bat?.[0] ?? null);
516
+ await this.createAndSetState(`${sysId}.battery_charging`, {
517
+ name: "Battery Charging",
518
+ type: "boolean",
519
+ role: "indicator",
520
+ read: true,
521
+ write: false,
522
+ }, bat ? bat[1] > 0 : null);
523
+ }
524
+ // GPU
525
+ if (config.metrics_gpu && stats.g && Object.keys(stats.g).length > 0) {
526
+ await this.ensureChannel(`${sysId}.gpu`, "GPU");
527
+ for (const [gpuId, gpuData] of Object.entries(stats.g)) {
528
+ const safeId = this.sanitize(gpuId);
529
+ const gpuLabel = gpuData.n ?? gpuId;
530
+ await this.ensureChannel(`${sysId}.gpu.${safeId}`, gpuLabel);
531
+ await this.createAndSetState(`${sysId}.gpu.${safeId}.usage`, {
532
+ name: "GPU Usage",
533
+ type: "number",
534
+ role: "level",
535
+ unit: "%",
536
+ min: 0,
537
+ max: 100,
538
+ read: true,
539
+ write: false,
540
+ }, gpuData.u ?? null);
541
+ await this.createAndSetState(`${sysId}.gpu.${safeId}.memory_used`, {
542
+ name: "GPU Memory Used",
543
+ type: "number",
544
+ role: "value",
545
+ unit: "GB",
546
+ read: true,
547
+ write: false,
548
+ }, gpuData.mu ?? null);
549
+ await this.createAndSetState(`${sysId}.gpu.${safeId}.memory_total`, {
550
+ name: "GPU Memory Total",
551
+ type: "number",
552
+ role: "value",
553
+ unit: "GB",
554
+ read: true,
555
+ write: false,
556
+ }, gpuData.mt ?? null);
557
+ await this.createAndSetState(`${sysId}.gpu.${safeId}.power`, {
558
+ name: "GPU Power",
559
+ type: "number",
560
+ role: "value",
561
+ unit: "W",
562
+ read: true,
563
+ write: false,
564
+ }, gpuData.p ?? null);
565
+ }
566
+ }
567
+ else if (!config.metrics_gpu) {
568
+ await this.deleteChannelIfExists(`${sysId}.gpu`);
569
+ }
570
+ // Extra filesystems
571
+ if (config.metrics_extraFs &&
572
+ stats.efs &&
573
+ Object.keys(stats.efs).length > 0) {
574
+ await this.ensureChannel(`${sysId}.filesystems`, "Filesystems");
575
+ for (const [fsName, fsData] of Object.entries(stats.efs)) {
576
+ const safeId = this.sanitize(fsName);
577
+ await this.ensureChannel(`${sysId}.filesystems.${safeId}`, fsName);
578
+ const total = fsData.d ?? null;
579
+ const used = fsData.du ?? null;
580
+ const percent = total !== null && used !== null && total > 0
581
+ ? Math.round((used / total) * 100)
582
+ : null;
583
+ await this.createAndSetState(`${sysId}.filesystems.${safeId}.disk_percent`, {
584
+ name: "Disk %",
585
+ type: "number",
586
+ role: "level",
587
+ unit: "%",
588
+ min: 0,
589
+ max: 100,
590
+ read: true,
591
+ write: false,
592
+ }, percent);
593
+ await this.createAndSetState(`${sysId}.filesystems.${safeId}.disk_used`, {
594
+ name: "Disk Used",
595
+ type: "number",
596
+ role: "value",
597
+ unit: "GB",
598
+ read: true,
599
+ write: false,
600
+ }, used);
601
+ await this.createAndSetState(`${sysId}.filesystems.${safeId}.disk_total`, {
602
+ name: "Disk Total",
603
+ type: "number",
604
+ role: "value",
605
+ unit: "GB",
606
+ read: true,
607
+ write: false,
608
+ }, total);
609
+ await this.createAndSetState(`${sysId}.filesystems.${safeId}.read_speed`, {
610
+ name: "Read Speed",
611
+ type: "number",
612
+ role: "value",
613
+ unit: "MB/s",
614
+ read: true,
615
+ write: false,
616
+ }, fsData.r ?? null);
617
+ await this.createAndSetState(`${sysId}.filesystems.${safeId}.write_speed`, {
618
+ name: "Write Speed",
619
+ type: "number",
620
+ role: "value",
621
+ unit: "MB/s",
622
+ read: true,
623
+ write: false,
624
+ }, fsData.w ?? null);
625
+ }
626
+ }
627
+ else if (!config.metrics_extraFs) {
628
+ await this.deleteChannelIfExists(`${sysId}.filesystems`);
629
+ }
630
+ }
631
+ async updateContainers(sysId, systemId, allContainers, _config) {
632
+ const sysContainers = allContainers.filter((c) => c.system === systemId);
633
+ if (sysContainers.length === 0) {
634
+ return;
635
+ }
636
+ await this.ensureChannel(`${sysId}.containers`, "Containers");
637
+ const healthLabels = ["none", "starting", "healthy", "unhealthy"];
638
+ for (const container of sysContainers) {
639
+ const cId = this.sanitize(container.name);
640
+ await this.ensureChannel(`${sysId}.containers.${cId}`, container.name);
641
+ await this.createAndSetState(`${sysId}.containers.${cId}.status`, {
642
+ name: "Status",
643
+ type: "string",
644
+ role: "text",
645
+ read: true,
646
+ write: false,
647
+ }, container.status);
648
+ await this.createAndSetState(`${sysId}.containers.${cId}.health`, {
649
+ name: "Health",
650
+ type: "string",
651
+ role: "text",
652
+ read: true,
653
+ write: false,
654
+ }, healthLabels[container.health] ?? "unknown");
655
+ await this.createAndSetState(`${sysId}.containers.${cId}.cpu`, {
656
+ name: "CPU Usage",
657
+ type: "number",
658
+ role: "level",
659
+ unit: "%",
660
+ min: 0,
661
+ max: 100,
662
+ read: true,
663
+ write: false,
664
+ }, container.cpu);
665
+ await this.createAndSetState(`${sysId}.containers.${cId}.memory`, {
666
+ name: "Memory",
667
+ type: "number",
668
+ role: "value",
669
+ unit: "MB",
670
+ read: true,
671
+ write: false,
672
+ }, container.memory);
673
+ await this.createAndSetState(`${sysId}.containers.${cId}.image`, {
674
+ name: "Image",
675
+ type: "string",
676
+ role: "text",
677
+ read: true,
678
+ write: false,
679
+ }, container.image);
680
+ }
681
+ }
682
+ async ensureChannel(id, name) {
683
+ await this.adapter.setObjectNotExistsAsync(id, {
684
+ type: "channel",
685
+ common: { name },
686
+ native: {},
687
+ });
688
+ }
689
+ async deleteChannelIfExists(id) {
690
+ try {
691
+ const obj = await this.adapter.getObjectAsync(id);
692
+ if (obj) {
693
+ await this.adapter.delObjectAsync(id, { recursive: true });
694
+ }
695
+ }
696
+ catch {
697
+ // ignore
698
+ }
699
+ }
700
+ async createAndSetState(id, common, value) {
701
+ await this.adapter.setObjectNotExistsAsync(id, {
702
+ type: "state",
703
+ common,
704
+ native: {},
705
+ });
706
+ await this.adapter.setStateAsync(id, { val: value, ack: true });
707
+ }
708
+ computeTopAvgTemp(temps) {
709
+ if (!temps) {
710
+ return null;
711
+ }
712
+ const values = Object.values(temps).filter((v) => typeof v === "number" && isFinite(v));
713
+ if (values.length === 0) {
714
+ return null;
715
+ }
716
+ values.sort((a, b) => b - a);
717
+ const top3 = values.slice(0, 3);
718
+ const avg = top3.reduce((sum, v) => sum + v, 0) / top3.length;
719
+ return Math.round(avg * 10) / 10;
720
+ }
721
+ formatUptime(seconds) {
722
+ const d = Math.floor(seconds / 86400);
723
+ const h = Math.floor((seconds % 86400) / 3600);
724
+ const m = Math.floor((seconds % 3600) / 60);
725
+ const parts = [];
726
+ if (d > 0) {
727
+ parts.push(`${d}d`);
728
+ }
729
+ if (h > 0) {
730
+ parts.push(`${h}h`);
731
+ }
732
+ if (m > 0 || parts.length === 0) {
733
+ parts.push(`${m}m`);
734
+ }
735
+ return parts.join(" ");
736
+ }
737
+ }
738
+ exports.StateManager = StateManager;