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,192 @@
1
+ /**
2
+ * Adapter configuration as stored in ioBroker native config
3
+ */
4
+ export interface AdapterConfig {
5
+ url: string;
6
+ username: string;
7
+ password: string;
8
+ pollInterval: number;
9
+
10
+ // Metric toggles
11
+ metrics_uptime: boolean;
12
+ metrics_agentVersion: boolean;
13
+ metrics_services: boolean;
14
+
15
+ metrics_cpu: boolean;
16
+ metrics_loadAvg: boolean;
17
+ metrics_cpuBreakdown: boolean;
18
+
19
+ metrics_memory: boolean;
20
+ metrics_memoryDetails: boolean;
21
+ metrics_swap: boolean;
22
+
23
+ metrics_disk: boolean;
24
+ metrics_diskSpeed: boolean;
25
+ metrics_extraFs: boolean;
26
+
27
+ metrics_network: boolean;
28
+
29
+ metrics_temperature: boolean;
30
+ metrics_temperatureDetails: boolean;
31
+
32
+ metrics_gpu: boolean;
33
+
34
+ metrics_containers: boolean;
35
+
36
+ metrics_battery: boolean;
37
+ }
38
+
39
+ /**
40
+ * System info object from Beszel systems record
41
+ */
42
+ export interface SystemInfo {
43
+ /** Uptime in seconds */
44
+ u?: number;
45
+ /** Agent version */
46
+ v?: string;
47
+ /** Systemd services [total, failed] */
48
+ sv?: [number, number];
49
+ /** Load average [1m, 5m, 15m] */
50
+ la?: [number, number, number];
51
+ /** Battery [percent, charge_state] */
52
+ bat?: [number, number];
53
+ /** Connection type */
54
+ ct?: number;
55
+ }
56
+
57
+ /**
58
+ * A system record from /api/collections/systems/records
59
+ */
60
+ export interface BeszelSystem {
61
+ id: string;
62
+ name: string;
63
+ status: "up" | "down" | "paused" | "pending";
64
+ host: string;
65
+ info: SystemInfo;
66
+ }
67
+
68
+ /**
69
+ * Extra filesystem stats
70
+ */
71
+ export interface FsStats {
72
+ /** disk total GB */
73
+ d?: number;
74
+ /** disk used GB */
75
+ du?: number;
76
+ /** read MB/s */
77
+ r?: number;
78
+ /** write MB/s */
79
+ w?: number;
80
+ }
81
+
82
+ /**
83
+ * GPU data
84
+ */
85
+ export interface GPUData {
86
+ /** GPU name */
87
+ n?: string;
88
+ /** GPU usage % */
89
+ u?: number;
90
+ /** GPU memory used GB */
91
+ mu?: number;
92
+ /** GPU memory total GB */
93
+ mt?: number;
94
+ /** GPU power W */
95
+ p?: number;
96
+ }
97
+
98
+ /**
99
+ * The stats object inside a system_stats record
100
+ */
101
+ export interface SystemStats {
102
+ /** CPU usage % */
103
+ cpu?: number;
104
+ /** RAM used GB */
105
+ mu?: number;
106
+ /** RAM total GB */
107
+ m?: number;
108
+ /** RAM % */
109
+ mp?: number;
110
+ /** Buffers + cache GB */
111
+ mb?: number;
112
+ /** ZFS ARC GB */
113
+ mz?: number;
114
+ /** Swap used GB */
115
+ su?: number;
116
+ /** Swap total GB */
117
+ s?: number;
118
+ /** Disk used GB */
119
+ du?: number;
120
+ /** Disk total GB */
121
+ d?: number;
122
+ /** Disk % */
123
+ dp?: number;
124
+ /** Disk read MB/s */
125
+ dr?: number;
126
+ /** Disk write MB/s */
127
+ dw?: number;
128
+ /** Network sent MB/s */
129
+ ns?: number;
130
+ /** Network recv MB/s */
131
+ nr?: number;
132
+ /** Temperatures map sensor->°C */
133
+ t?: Record<string, number>;
134
+ /** Load avg [1m, 5m, 15m] */
135
+ la?: [number, number, number];
136
+ /** GPU data */
137
+ g?: Record<string, GPUData>;
138
+ /** Extra filesystems */
139
+ efs?: Record<string, FsStats>;
140
+ /** Battery [%, charge_state] */
141
+ bat?: [number, number];
142
+ /** CPU breakdown [user, sys, iowait, steal, idle] % */
143
+ cpub?: number[];
144
+ }
145
+
146
+ /**
147
+ * A system_stats record from /api/collections/system_stats/records
148
+ */
149
+ export interface BeszelSystemStats {
150
+ id: string;
151
+ system: string;
152
+ type: string;
153
+ stats: SystemStats;
154
+ updated: string;
155
+ }
156
+
157
+ /**
158
+ * A container record from /api/collections/containers/records
159
+ */
160
+ export interface BeszelContainer {
161
+ id: string;
162
+ system: string;
163
+ name: string;
164
+ status: string;
165
+ health: number;
166
+ cpu: number;
167
+ memory: number;
168
+ image: string;
169
+ }
170
+
171
+ /**
172
+ * PocketBase list response
173
+ */
174
+ export interface PocketBaseList<T> {
175
+ page: number;
176
+ perPage: number;
177
+ totalItems: number;
178
+ totalPages: number;
179
+ items: T[];
180
+ }
181
+
182
+ /**
183
+ * PocketBase auth response
184
+ */
185
+ export interface AuthResponse {
186
+ token: string;
187
+ record: {
188
+ id: string;
189
+ email: string;
190
+ [key: string]: unknown;
191
+ };
192
+ }
package/src/main.ts ADDED
@@ -0,0 +1,199 @@
1
+ import * as utils from "@iobroker/adapter-core";
2
+ import { BeszelClient } from "./lib/beszel-client.js";
3
+ import { StateManager } from "./lib/state-manager.js";
4
+ import type { AdapterConfig } from "./lib/types.js";
5
+
6
+ class BeszelAdapter extends utils.Adapter {
7
+ private client: BeszelClient | null = null;
8
+ private stateManager: StateManager | null = null;
9
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
10
+ private isPolling = false;
11
+
12
+ public constructor(options: Partial<utils.AdapterOptions> = {}) {
13
+ super({
14
+ ...options,
15
+ name: "beszel",
16
+ });
17
+ this.on("ready", this.onReady.bind(this));
18
+ this.on("unload", this.onUnload.bind(this));
19
+ this.on("message", this.onMessage.bind(this));
20
+ }
21
+
22
+ private async onReady(): Promise<void> {
23
+ const config = this.config as unknown as AdapterConfig;
24
+
25
+ // Ensure info objects exist before any setState calls
26
+ await this.setObjectNotExistsAsync("info", {
27
+ type: "channel",
28
+ common: { name: "Information" },
29
+ native: {},
30
+ });
31
+ await this.setObjectNotExistsAsync("info.connection", {
32
+ type: "state",
33
+ common: {
34
+ name: "Connection status",
35
+ type: "boolean",
36
+ role: "indicator.connected",
37
+ read: true,
38
+ write: false,
39
+ def: false,
40
+ },
41
+ native: {},
42
+ });
43
+ await this.setStateAsync("info.connection", { val: false, ack: true });
44
+
45
+ // Validate required config
46
+ if (!config.url || !config.username || !config.password) {
47
+ this.log.error(
48
+ "Beszel adapter: URL, username, and password are required. Please configure the adapter.",
49
+ );
50
+ await this.setStateAsync("info.connection", { val: false, ack: true });
51
+ return;
52
+ }
53
+
54
+ this.client = new BeszelClient(
55
+ config.url,
56
+ config.username,
57
+ config.password,
58
+ );
59
+ this.stateManager = new StateManager(this);
60
+
61
+ // Cleanup disabled metric states for existing systems (config may have changed)
62
+ const existingObjects = await this.getObjectViewAsync("system", "device", {
63
+ startkey: `${this.namespace}.systems.`,
64
+ endkey: `${this.namespace}.systems.\u9999`,
65
+ });
66
+ if (existingObjects?.rows) {
67
+ for (const row of existingObjects.rows) {
68
+ const relId = row.id.startsWith(`${this.namespace}.`)
69
+ ? row.id.slice(this.namespace.length + 1)
70
+ : row.id;
71
+ const parts = relId.split(".");
72
+ if (parts.length === 2 && parts[0] === "systems") {
73
+ await this.stateManager.cleanupMetrics(parts[1], config);
74
+ }
75
+ }
76
+ }
77
+
78
+ // Initial poll
79
+ await this.poll();
80
+
81
+ // Set up recurring poll
82
+ const intervalMs = Math.max(10, config.pollInterval ?? 60) * 1000;
83
+ this.pollTimer = setInterval(() => {
84
+ void this.poll();
85
+ }, intervalMs);
86
+
87
+ this.log.info(
88
+ `Beszel adapter started. Polling every ${config.pollInterval ?? 60}s from ${config.url}`,
89
+ );
90
+ }
91
+
92
+ private async onUnload(callback: () => void): Promise<void> {
93
+ try {
94
+ if (this.pollTimer) {
95
+ clearInterval(this.pollTimer);
96
+ this.pollTimer = null;
97
+ }
98
+ await this.setStateAsync("info.connection", { val: false, ack: true });
99
+ } catch {
100
+ // ignore
101
+ }
102
+ callback();
103
+ }
104
+
105
+ private async onMessage(obj: ioBroker.Message): Promise<void> {
106
+ if (obj.command === "checkConnection") {
107
+ const config = obj.message as Partial<AdapterConfig>;
108
+ const url = config.url ?? "";
109
+ const username = config.username ?? "";
110
+ const password = config.password ?? "";
111
+
112
+ if (!url || !username || !password) {
113
+ this.sendTo(
114
+ obj.from,
115
+ obj.command,
116
+ {
117
+ success: false,
118
+ message: "URL, username and password are required",
119
+ },
120
+ obj.callback,
121
+ );
122
+ return;
123
+ }
124
+
125
+ const testClient = new BeszelClient(url, username, password);
126
+ const result = await testClient.checkConnection();
127
+ this.sendTo(obj.from, obj.command, result, obj.callback);
128
+ }
129
+ }
130
+
131
+ private async poll(): Promise<void> {
132
+ if (this.isPolling) {
133
+ this.log.debug("Skipping poll — previous poll still running");
134
+ return;
135
+ }
136
+ if (!this.client || !this.stateManager) {
137
+ return;
138
+ }
139
+
140
+ this.isPolling = true;
141
+ try {
142
+ const config = this.config as unknown as AdapterConfig;
143
+
144
+ // Fetch all data
145
+ const [systems, containers] = await Promise.all([
146
+ this.client.getSystems(),
147
+ config.metrics_containers
148
+ ? this.client.getContainers()
149
+ : Promise.resolve([]),
150
+ ]);
151
+
152
+ const systemIds = systems.map((s) => s.id);
153
+ const statsMap = await this.client.getLatestStats(systemIds);
154
+
155
+ // Update connection state
156
+ await this.setStateAsync("info.connection", { val: true, ack: true });
157
+
158
+ // Update each system
159
+ for (const system of systems) {
160
+ const stats = statsMap.get(system.id);
161
+ const sysContainers = containers.filter((c) => c.system === system.id);
162
+ await this.stateManager.updateSystem(
163
+ system,
164
+ stats,
165
+ sysContainers,
166
+ config,
167
+ );
168
+ }
169
+
170
+ // Cleanup stale systems
171
+ await this.stateManager.cleanupSystems(systems.map((s) => s.name));
172
+
173
+ this.log.debug(`Polled ${systems.length} systems successfully`);
174
+ } catch (err) {
175
+ const errMsg = err instanceof Error ? err.message : String(err);
176
+ this.log.error(`Poll failed: ${errMsg}`);
177
+
178
+ // On 401, invalidate token so next poll re-authenticates
179
+ if (
180
+ err instanceof Error &&
181
+ (err as NodeJS.ErrnoException).code === "UNAUTHORIZED"
182
+ ) {
183
+ this.client?.invalidateToken();
184
+ }
185
+
186
+ await this.setStateAsync("info.connection", { val: false, ack: true });
187
+ } finally {
188
+ this.isPolling = false;
189
+ }
190
+ }
191
+ }
192
+
193
+ if (require.main !== module) {
194
+ // Export the constructor in compact mode
195
+ module.exports = (options: Partial<utils.AdapterOptions> | undefined) =>
196
+ new BeszelAdapter(options);
197
+ } else {
198
+ (() => new BeszelAdapter())();
199
+ }
@@ -0,0 +1,5 @@
1
+ import path from "node:path";
2
+ import { tests } from "@iobroker/testing";
3
+
4
+ // __dirname is build/test/ after compilation, navigate to project root
5
+ tests.packageFiles(path.join(__dirname, "..", ".."));
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "sourceMap": false
5
+ },
6
+ "exclude": ["node_modules", "build", "**/*.test.ts", "test"]
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "outDir": "./build",
9
+ "rootDir": "./src",
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "sourceMap": true,
13
+ "noImplicitAny": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "noImplicitReturns": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "resolveJsonModule": true,
19
+ "skipLibCheck": true,
20
+ "forceConsistentCasingInFileNames": true
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "build", "test"]
24
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "sourceMap": true,
5
+ "rootDir": "."
6
+ },
7
+ "include": ["src/**/*", "test/**/*"],
8
+ "exclude": ["node_modules", "build"]
9
+ }