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.
- package/.github/auto-merge.yml +2 -0
- package/.github/dependabot.yml +12 -0
- package/.github/workflows/automerge-dependabot.yml +32 -0
- package/.github/workflows/test-and-release.yml +62 -0
- package/.vscode/settings.json +12 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +91 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/admin/beszel.svg +9 -0
- package/admin/i18n/de/translations.json +43 -0
- package/admin/i18n/en/translations.json +43 -0
- package/admin/i18n/es/translations.json +43 -0
- package/admin/i18n/fr/translations.json +43 -0
- package/admin/i18n/it/translations.json +43 -0
- package/admin/i18n/nl/translations.json +43 -0
- package/admin/i18n/pl/translations.json +43 -0
- package/admin/i18n/pt/translations.json +43 -0
- package/admin/i18n/ru/translations.json +43 -0
- package/admin/i18n/uk/translations.json +43 -0
- package/admin/i18n/zh-cn/translations.json +43 -0
- package/admin/jsonConfig.json +240 -0
- package/build/lib/beszel-client.d.ts +39 -0
- package/build/lib/beszel-client.d.ts.map +1 -0
- package/build/lib/beszel-client.js +199 -0
- package/build/lib/state-manager.d.ts +47 -0
- package/build/lib/state-manager.d.ts.map +1 -0
- package/build/lib/state-manager.js +738 -0
- package/build/lib/types.d.ts +174 -0
- package/build/lib/types.d.ts.map +1 -0
- package/build/lib/types.js +2 -0
- package/build/main.d.ts +2 -0
- package/build/main.d.ts.map +1 -0
- package/build/main.js +191 -0
- package/eslint.config.mjs +36 -0
- package/io-package.json +162 -0
- package/package.json +61 -0
- package/scripts/version.js +28 -0
- package/src/lib/beszel-client.ts +216 -0
- package/src/lib/state-manager.ts +1050 -0
- package/src/lib/types.ts +192 -0
- package/src/main.ts +199 -0
- package/test/testPackageFiles.ts +5 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +24 -0
- package/tsconfig.test.json +9 -0
package/src/lib/types.ts
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|