133 lines
5.5 KiB
TypeScript
133 lines
5.5 KiB
TypeScript
import type tls from "node:tls";
|
|
import axios from "axios";
|
|
import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http";
|
|
|
|
import {
|
|
createCustomCheckServerIdentity,
|
|
getAllTrustedCertificatesAsync,
|
|
getTrustedCertificateHostnamesAsync,
|
|
} from "@homarr/certificates/server";
|
|
import { getPortFromUrl } from "@homarr/common";
|
|
import type { SiteStats } from "@homarr/node-unifi";
|
|
import Unifi from "@homarr/node-unifi";
|
|
|
|
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
|
import { integrationAxiosHttpErrorHandler } from "../base/errors/http";
|
|
import type { IntegrationTestingInput } from "../base/integration";
|
|
import { Integration } from "../base/integration";
|
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
|
import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration";
|
|
import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types";
|
|
import type { HealthSubsystem } from "./unifi-controller-types";
|
|
|
|
@HandleIntegrationErrors([integrationAxiosHttpErrorHandler])
|
|
export class UnifiControllerIntegration extends Integration implements NetworkControllerSummaryIntegration {
|
|
public async getNetworkSummaryAsync(): Promise<NetworkControllerSummary> {
|
|
const client = await this.createControllerClientAsync();
|
|
const stats = await client.getSitesStats();
|
|
|
|
return {
|
|
wanStatus: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"),
|
|
www: {
|
|
status: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"),
|
|
latency: this.getNumericValueOverAllSites(stats, "www", (site) => site.latency, "max"),
|
|
ping: this.getNumericValueOverAllSites(stats, "www", (site) => site.speedtest_ping, "max"),
|
|
uptime: this.getNumericValueOverAllSites(stats, "www", (site) => site.uptime, "max"),
|
|
},
|
|
wifi: {
|
|
status: this.getStatusValueOverAllSites(stats, "wlan", (site) => site.status === "ok"),
|
|
users: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_user, "sum"),
|
|
guests: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_guest, "sum"),
|
|
},
|
|
lan: {
|
|
status: this.getStatusValueOverAllSites(stats, "lan", (site) => site.status === "ok"),
|
|
users: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_user, "sum"),
|
|
guests: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_guest, "sum"),
|
|
},
|
|
vpn: {
|
|
status: this.getStatusValueOverAllSites(stats, "vpn", (site) => site.status === "ok"),
|
|
users: this.getNumericValueOverAllSites(stats, "vpn", (site) => site.remote_user_num_active, "sum"),
|
|
},
|
|
} satisfies NetworkControllerSummary;
|
|
}
|
|
|
|
protected async testingAsync({ options }: IntegrationTestingInput): Promise<TestingResult> {
|
|
const client = await this.createControllerClientAsync(options);
|
|
await client.getSitesStats();
|
|
return { success: true };
|
|
}
|
|
|
|
private async createControllerClientAsync(options?: {
|
|
ca: string | string[];
|
|
checkServerIdentity: typeof tls.checkServerIdentity;
|
|
}) {
|
|
const url = new URL(this.integration.url);
|
|
const certificateOptions = options ?? {
|
|
ca: await getAllTrustedCertificatesAsync(),
|
|
checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()),
|
|
};
|
|
|
|
const client = new Unifi.Controller({
|
|
host: url.hostname,
|
|
port: getPortFromUrl(url),
|
|
username: this.getSecretValue("username"),
|
|
password: this.getSecretValue("password"),
|
|
createAxiosInstance({ cookies }) {
|
|
return axios.create({
|
|
adapter: "http",
|
|
httpAgent: new HttpCookieAgent({ cookies }),
|
|
httpsAgent: new HttpsCookieAgent({
|
|
cookies,
|
|
requestCert: true,
|
|
...certificateOptions,
|
|
}),
|
|
});
|
|
},
|
|
});
|
|
|
|
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
|
|
return client;
|
|
}
|
|
|
|
private getStatusValueOverAllSites<S extends HealthSubsystem>(
|
|
data: SiteStats[],
|
|
subsystem: S,
|
|
selectCallback: (obj: SiteStats["health"][number]) => boolean,
|
|
) {
|
|
return this.getBooleanValueOverAllSites(data, subsystem, selectCallback) ? "enabled" : "disabled";
|
|
}
|
|
|
|
private getNumericValueOverAllSites<
|
|
S extends HealthSubsystem,
|
|
T extends Extract<SiteStats["health"][number], { subsystem: S }>,
|
|
>(data: SiteStats[], subsystem: S, selectCallback: (obj: T) => number, strategy: "average" | "sum" | "max"): number {
|
|
const values = data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T));
|
|
|
|
if (strategy === "sum") {
|
|
return values.reduce((first, second) => first + second, 0);
|
|
}
|
|
|
|
if (strategy === "average") {
|
|
return values.reduce((first, second, _, array) => first + second / array.length, 0);
|
|
}
|
|
|
|
return Math.max(...values);
|
|
}
|
|
|
|
private getBooleanValueOverAllSites(
|
|
data: SiteStats[],
|
|
subsystem: HealthSubsystem,
|
|
selectCallback: (obj: SiteStats["health"][number]) => boolean,
|
|
): boolean {
|
|
return data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem)));
|
|
}
|
|
|
|
private getSubsystem(health: SiteStats["health"], subsystem: HealthSubsystem) {
|
|
const value = health.find((health) => health.subsystem === subsystem);
|
|
if (!value) {
|
|
throw new Error(`Subsystem ${subsystem} not found!`);
|
|
}
|
|
return value;
|
|
}
|
|
}
|