feat: OMV integration & health monitoring widget (#1142)
This commit is contained in:
@@ -14,6 +14,7 @@ import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
||||
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
||||
import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration";
|
||||
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
|
||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
|
||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||
@@ -60,4 +61,5 @@ export const integrationCreators = {
|
||||
jellyseerr: JellyseerrIntegration,
|
||||
overseerr: OverseerrIntegration,
|
||||
prowlarr: ProwlarrIntegration,
|
||||
openmediavault: OpenMediaVaultIntegration,
|
||||
} satisfies Partial<Record<IntegrationKind, new (integration: IntegrationInput) => Integration>>;
|
||||
|
||||
@@ -6,23 +6,25 @@ export { DownloadClientIntegration } from "./interfaces/downloads/download-clien
|
||||
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
|
||||
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
|
||||
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
|
||||
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
|
||||
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
|
||||
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
|
||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
|
||||
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
|
||||
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
|
||||
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
|
||||
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
|
||||
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
|
||||
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
|
||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||
|
||||
// Types
|
||||
export type { IntegrationInput } from "./base/integration";
|
||||
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||
export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
|
||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
|
||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
|
||||
export type { StreamSession } from "./interfaces/media-server/session";
|
||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||
export type { IntegrationInput } from "./base/integration";
|
||||
|
||||
// Schemas
|
||||
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface HealthMonitoring {
|
||||
version: string;
|
||||
cpuModelName: string;
|
||||
cpuUtilization: number;
|
||||
memUsed: string;
|
||||
memAvailable: string;
|
||||
uptime: number;
|
||||
loadAverage: {
|
||||
"1min": number;
|
||||
"5min": number;
|
||||
"15min": number;
|
||||
};
|
||||
rebootRequired: boolean;
|
||||
availablePkgUpdates: number;
|
||||
cpuTemp: number;
|
||||
fileSystem: {
|
||||
deviceName: string;
|
||||
used: string;
|
||||
available: string;
|
||||
percentage: number;
|
||||
}[];
|
||||
smart: {
|
||||
deviceName: string;
|
||||
temperature: number;
|
||||
overallStatus: string;
|
||||
}[];
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type { HealthMonitoring } from "../types";
|
||||
import { cpuTempSchema, fileSystemSchema, smartSchema, systemInformationSchema } from "./openmediavault-types";
|
||||
|
||||
export class OpenMediaVaultIntegration extends Integration {
|
||||
static extractSessionIdFromCookies(headers: Headers): string {
|
||||
const cookies = headers.get("set-cookie") ?? "";
|
||||
const sessionId = cookies
|
||||
.split(";")
|
||||
.find((cookie) => cookie.includes("X-OPENMEDIAVAULT-SESSIONID") || cookie.includes("OPENMEDIAVAULT-SESSIONID"));
|
||||
|
||||
if (sessionId) {
|
||||
return sessionId;
|
||||
} else {
|
||||
throw new Error("Session ID not found in cookies");
|
||||
}
|
||||
}
|
||||
|
||||
static extractLoginTokenFromCookies(headers: Headers): string {
|
||||
const cookies = headers.get("set-cookie") ?? "";
|
||||
const loginToken = cookies
|
||||
.split(";")
|
||||
.find((cookie) => cookie.includes("X-OPENMEDIAVAULT-LOGIN") || cookie.includes("OPENMEDIAVAULT-LOGIN"));
|
||||
|
||||
if (loginToken) {
|
||||
return loginToken;
|
||||
} else {
|
||||
throw new Error("Login token not found in cookies");
|
||||
}
|
||||
}
|
||||
|
||||
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
|
||||
if (!this.headers) {
|
||||
await this.authenticateAndConstructSessionInHeaderAsync();
|
||||
}
|
||||
|
||||
const systemResponses = await this.makeOpenMediaVaultRPCCallAsync("system", "getInformation", {}, this.headers);
|
||||
const fileSystemResponse = await this.makeOpenMediaVaultRPCCallAsync(
|
||||
"filesystemmgmt",
|
||||
"enumerateMountedFilesystems",
|
||||
{ includeroot: true },
|
||||
this.headers,
|
||||
);
|
||||
const smartResponse = await this.makeOpenMediaVaultRPCCallAsync("smart", "enumerateDevices", {}, this.headers);
|
||||
const cpuTempResponse = await this.makeOpenMediaVaultRPCCallAsync("cputemp", "get", {}, this.headers);
|
||||
|
||||
const systemResult = systemInformationSchema.safeParse(await systemResponses.json());
|
||||
const fileSystemResult = fileSystemSchema.safeParse(await fileSystemResponse.json());
|
||||
const smartResult = smartSchema.safeParse(await smartResponse.json());
|
||||
const cpuTempResult = cpuTempSchema.safeParse(await cpuTempResponse.json());
|
||||
|
||||
if (!systemResult.success) {
|
||||
throw new Error("Invalid system information response");
|
||||
}
|
||||
if (!fileSystemResult.success) {
|
||||
throw new Error("Invalid file system response");
|
||||
}
|
||||
if (!smartResult.success) {
|
||||
throw new Error("Invalid SMART information response");
|
||||
}
|
||||
if (!cpuTempResult.success) {
|
||||
throw new Error("Invalid CPU temperature response");
|
||||
}
|
||||
|
||||
const fileSystem = fileSystemResult.data.response.map((fileSystem) => ({
|
||||
deviceName: fileSystem.devicename,
|
||||
used: fileSystem.used,
|
||||
available: fileSystem.available,
|
||||
percentage: fileSystem.percentage,
|
||||
}));
|
||||
|
||||
const smart = smartResult.data.response.map((smart) => ({
|
||||
deviceName: smart.devicename,
|
||||
temperature: smart.temperature,
|
||||
overallStatus: smart.overallstatus,
|
||||
}));
|
||||
|
||||
return {
|
||||
version: systemResult.data.response.version,
|
||||
cpuModelName: systemResult.data.response.cpuModelName,
|
||||
cpuUtilization: systemResult.data.response.cpuUtilization,
|
||||
memUsed: systemResult.data.response.memUsed,
|
||||
memAvailable: systemResult.data.response.memAvailable,
|
||||
uptime: systemResult.data.response.uptime,
|
||||
loadAverage: {
|
||||
"1min": systemResult.data.response.loadAverage["1min"],
|
||||
"5min": systemResult.data.response.loadAverage["5min"],
|
||||
"15min": systemResult.data.response.loadAverage["15min"],
|
||||
},
|
||||
rebootRequired: systemResult.data.response.rebootRequired,
|
||||
availablePkgUpdates: systemResult.data.response.availablePkgUpdates,
|
||||
cpuTemp: cpuTempResult.data.response.cputemp,
|
||||
fileSystem,
|
||||
smart,
|
||||
};
|
||||
}
|
||||
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const response = await this.makeOpenMediaVaultRPCCallAsync("session", "login", {
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new IntegrationTestConnectionError("invalidCredentials");
|
||||
}
|
||||
const result = (await response.json()) as unknown;
|
||||
if (typeof result !== "object" || result === null || !("response" in result)) {
|
||||
throw new IntegrationTestConnectionError("invalidJson");
|
||||
}
|
||||
}
|
||||
|
||||
private async makeOpenMediaVaultRPCCallAsync(
|
||||
serviceName: string,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
headers: Record<string, string> = {},
|
||||
): Promise<Response> {
|
||||
return await fetch(`${this.integration.url}/rpc.php`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
service: serviceName,
|
||||
method,
|
||||
params,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private headers: Record<string, string> | undefined = undefined;
|
||||
|
||||
private async authenticateAndConstructSessionInHeaderAsync() {
|
||||
const authResponse = await this.makeOpenMediaVaultRPCCallAsync("session", "login", {
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
});
|
||||
const authResult = (await authResponse.json()) as Response;
|
||||
const response = (authResult as { response?: { sessionid?: string } }).response;
|
||||
let sessionId;
|
||||
const headers: Record<string, string> = {};
|
||||
if (response?.sessionid) {
|
||||
sessionId = response.sessionid;
|
||||
headers["X-OPENMEDIAVAULT-SESSIONID"] = sessionId;
|
||||
} else {
|
||||
sessionId = OpenMediaVaultIntegration.extractSessionIdFromCookies(authResponse.headers);
|
||||
const loginToken = OpenMediaVaultIntegration.extractLoginTokenFromCookies(authResponse.headers);
|
||||
headers.Cookie = `${loginToken};${sessionId}`;
|
||||
}
|
||||
this.headers = headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Schema for system information
|
||||
export const systemInformationSchema = z.object({
|
||||
response: z.object({
|
||||
version: z.string(),
|
||||
cpuModelName: z.string(),
|
||||
cpuUtilization: z.number(),
|
||||
memUsed: z.string(),
|
||||
memAvailable: z.string(),
|
||||
uptime: z.number(),
|
||||
loadAverage: z.object({
|
||||
"1min": z.number(),
|
||||
"5min": z.number(),
|
||||
"15min": z.number(),
|
||||
}),
|
||||
rebootRequired: z.boolean(),
|
||||
availablePkgUpdates: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Schema for file systems
|
||||
export const fileSystemSchema = z.object({
|
||||
response: z.array(
|
||||
z.object({
|
||||
devicename: z.string(),
|
||||
used: z.string(),
|
||||
available: z.string(),
|
||||
percentage: z.number(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Schema for SMART information
|
||||
export const smartSchema = z.object({
|
||||
response: z.array(
|
||||
z.object({
|
||||
devicename: z.string(),
|
||||
temperature: z.union([z.string(), z.number()]).transform((val) => {
|
||||
// Convert string to number if necessary
|
||||
const temp = typeof val === "string" ? parseFloat(val) : val;
|
||||
if (isNaN(temp)) {
|
||||
throw new Error("Invalid temperature value");
|
||||
}
|
||||
return temp;
|
||||
}),
|
||||
overallstatus: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Schema for CPU temperature
|
||||
export const cpuTempSchema = z.object({
|
||||
response: z.object({
|
||||
cputemp: z.number(),
|
||||
}),
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./calendar-types";
|
||||
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
||||
export * from "./interfaces/health-monitoring/healt-monitoring";
|
||||
export * from "./interfaces/indexer-manager/indexer";
|
||||
export * from "./interfaces/media-requests/media-request";
|
||||
export * from "./pi-hole/pi-hole-types";
|
||||
|
||||
Reference in New Issue
Block a user