feat: speedtest tracker integration (#5363)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
1 parent
c861878058
commit
22d599c0cf
21 files changed
+1005
-1
No files matched your search
@@ -42,6 +42,7 @@ import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||
import { QuayIntegration } from "../quay/quay-integration";
|
||||
import { SearchChIntegration } from "../search-ch/search-ch-integration";
|
||||
import { SeerrIntegration } from "../seerr/seerr-integration";
|
||||
import { SpeedtestTrackerIntegration } from "../speedtest-tracker/speedtest-tracker-integration";
|
||||
import { TracearrIntegration } from "../tracearr/tracearr-integration";
|
||||
import { TrueNasIntegration } from "../truenas/truenas-integration";
|
||||
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
|
||||
@@ -117,6 +118,7 @@ export const integrationCreators = {
|
||||
glances: GlancesIntegration,
|
||||
searchCh: SearchChIntegration,
|
||||
immich: ImmichIntegration,
|
||||
speedtestTracker: SpeedtestTrackerIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||
|
||||
@@ -31,6 +31,7 @@ export { ICalIntegration } from "./ical/ical-integration";
|
||||
export { CoolifyIntegration } from "./coolify/coolify-integration";
|
||||
export { ImmichIntegration } from "./immich/immich-integration";
|
||||
export { TracearrIntegration } from "./tracearr/tracearr-integration";
|
||||
export { SpeedtestTrackerIntegration } from "./speedtest-tracker/speedtest-tracker-integration";
|
||||
|
||||
// Types
|
||||
export type { IntegrationInput } from "./base/integration";
|
||||
@@ -58,6 +59,7 @@ export type { ReleasesRepository, ReleaseResponse } from "./interfaces/releases-
|
||||
export type { Notification } from "./interfaces/notifications/notification-types";
|
||||
export type { ImmichServerStats, ImmichAlbum, ImmichAsset } from "./immich/immich-integration";
|
||||
export type { TracearrDashboardData } from "./tracearr/tracearr-types";
|
||||
export type { SpeedtestTrackerDashboardData } from "./speedtest-tracker/speedtest-tracker-types";
|
||||
|
||||
// Schemas
|
||||
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import {
|
||||
speedtestTrackerLatestResultEnvelopeSchema,
|
||||
speedtestTrackerResultsCollectionSchema,
|
||||
speedtestTrackerStatsEnvelopeSchema,
|
||||
} from "./speedtest-tracker-types";
|
||||
import type {
|
||||
SpeedtestTrackerDashboardData,
|
||||
SpeedtestTrackerResult,
|
||||
SpeedtestTrackerResultsCollection,
|
||||
SpeedtestTrackerStats,
|
||||
} from "./speedtest-tracker-types";
|
||||
|
||||
export class SpeedtestTrackerIntegration extends Integration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const url = this.url("/api/v1/results/latest");
|
||||
const response = await input.fetchAsync(url, {
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
public async getLatestResultAsync(): Promise<SpeedtestTrackerResult | null> {
|
||||
const response = await this.getAsync("/api/v1/results/latest");
|
||||
|
||||
// Speedtest Tracker returns 404 when no tests have been run yet (fresh install)
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
return speedtestTrackerLatestResultEnvelopeSchema.parse(await response.json()).data;
|
||||
}
|
||||
|
||||
public async getStatsAsync(): Promise<SpeedtestTrackerStats | null> {
|
||||
const response = await this.getAsync("/api/v1/stats");
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
return speedtestTrackerStatsEnvelopeSchema.parse(await response.json()).data;
|
||||
}
|
||||
|
||||
public async getRecentResultsAsync(perPage = 10): Promise<SpeedtestTrackerResultsCollection> {
|
||||
const response = await this.getAsync("/api/v1/results", {
|
||||
result_count: String(perPage),
|
||||
sort: "-created_at",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new ResponseError(response);
|
||||
}
|
||||
|
||||
return speedtestTrackerResultsCollectionSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
public async getDashboardDataAsync(): Promise<SpeedtestTrackerDashboardData> {
|
||||
const [latestResult, stats, recentCollection] = await Promise.all([
|
||||
this.getLatestResultAsync(),
|
||||
this.getStatsAsync(),
|
||||
this.getRecentResultsAsync(30),
|
||||
]);
|
||||
|
||||
return {
|
||||
latestResult,
|
||||
stats,
|
||||
recentResults: recentCollection.data,
|
||||
};
|
||||
}
|
||||
|
||||
private async getAsync(path: `/api/v1/${string}`, queryParams?: Record<string, string>) {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url(path, queryParams), {
|
||||
headers: this.getAuthHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${this.getSecretValue("apiKey")}`,
|
||||
Accept: "application/json",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const createEnvelopeSchema = <T extends z.ZodType>(dataSchema: T) => z.object({ data: dataSchema });
|
||||
|
||||
const parseTimestamp = (timestamp: string): Date => new Date(`${timestamp.replace(" ", "T")}Z`);
|
||||
|
||||
export const speedtestTrackerResultSchema = z.object({
|
||||
id: z.number().int(),
|
||||
ping: z.number().nullable(),
|
||||
download_bits: z.number().int().nullish(),
|
||||
upload_bits: z.number().int().nullish(),
|
||||
healthy: z.boolean().nullable(),
|
||||
created_at: z.string().transform(parseTimestamp),
|
||||
});
|
||||
|
||||
export type SpeedtestTrackerResult = z.infer<typeof speedtestTrackerResultSchema>;
|
||||
|
||||
export const speedtestTrackerLatestResultEnvelopeSchema = createEnvelopeSchema(speedtestTrackerResultSchema);
|
||||
|
||||
export const speedtestTrackerStatsBandwidthSchema = z.object({
|
||||
avg: z.number(),
|
||||
avg_bits: z.number().optional(),
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
});
|
||||
|
||||
export const speedtestTrackerStatsSchema = z.object({
|
||||
ping: z.object({
|
||||
avg: z.number(),
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
}),
|
||||
download: speedtestTrackerStatsBandwidthSchema,
|
||||
upload: speedtestTrackerStatsBandwidthSchema,
|
||||
total_results: z.number().int(),
|
||||
});
|
||||
|
||||
export type SpeedtestTrackerStats = z.infer<typeof speedtestTrackerStatsSchema>;
|
||||
|
||||
export const speedtestTrackerStatsEnvelopeSchema = createEnvelopeSchema(speedtestTrackerStatsSchema);
|
||||
|
||||
export const speedtestTrackerResultsCollectionSchema = z.object({
|
||||
data: z.array(speedtestTrackerResultSchema),
|
||||
meta: z.object({
|
||||
current_page: z.number().int(),
|
||||
last_page: z.number().int(),
|
||||
total: z.number().int(),
|
||||
per_page: z.number().int().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type SpeedtestTrackerResultsCollection = z.infer<typeof speedtestTrackerResultsCollectionSchema>;
|
||||
|
||||
export interface SpeedtestTrackerDashboardData {
|
||||
latestResult: SpeedtestTrackerResult | null;
|
||||
stats: SpeedtestTrackerStats | null;
|
||||
recentResults: SpeedtestTrackerResult[];
|
||||
}
|
||||
@@ -14,3 +14,4 @@ export * from "./interfaces/media-releases";
|
||||
export * from "./coolify/coolify-types";
|
||||
export * from "./interfaces/timetable/timetable-types";
|
||||
export * from "./tracearr/tracearr-types";
|
||||
export * from "./speedtest-tracker/speedtest-tracker-types";
|
||||
Reference in new issue
Block a user