Files
homarr/packages/widgets/src/speedtest-tracker/helpers.ts
T

113 lines
4.1 KiB
TypeScript

import { formatNumber } from "@homarr/common";
import type {
SpeedtestTrackerDashboardData,
SpeedtestTrackerResult,
SpeedtestTrackerStats,
} from "@homarr/integrations/types";
export interface SpeedtestDashboardSource {
integrationId: string;
integrationName: string;
dashboard: SpeedtestTrackerDashboardData;
}
export const getAvailableSpeedtestDashboards = (
results: readonly {
integrationId: string;
integrationName: string;
dashboard: SpeedtestTrackerDashboardData | null;
}[],
): SpeedtestDashboardSource[] =>
results.flatMap(({ integrationId, integrationName, dashboard }) =>
dashboard === null ? [] : [{ integrationId, integrationName, dashboard }],
);
export const mergeStats = (
statsA: SpeedtestTrackerDashboardData["stats"],
statsB: SpeedtestTrackerDashboardData["stats"],
): SpeedtestTrackerDashboardData["stats"] => {
if (!statsB) return statsA;
if (!statsA) return statsB;
const totalResults = statsA.total_results + statsB.total_results;
const weightedAverage = (left: number, right: number) =>
totalResults > 0 ? (left * statsA.total_results + right * statsB.total_results) / totalResults : (left + right) / 2;
return {
ping: {
avg: weightedAverage(statsA.ping.avg, statsB.ping.avg),
min: Math.min(statsA.ping.min, statsB.ping.min),
max: Math.max(statsA.ping.max, statsB.ping.max),
},
download: {
avg: weightedAverage(statsA.download.avg, statsB.download.avg),
avg_bits:
statsA.download.avg_bits !== undefined && statsB.download.avg_bits !== undefined
? weightedAverage(statsA.download.avg_bits, statsB.download.avg_bits)
: (statsA.download.avg_bits ?? statsB.download.avg_bits),
min: Math.min(statsA.download.min, statsB.download.min),
max: Math.max(statsA.download.max, statsB.download.max),
},
upload: {
avg: weightedAverage(statsA.upload.avg, statsB.upload.avg),
avg_bits:
statsA.upload.avg_bits !== undefined && statsB.upload.avg_bits !== undefined
? weightedAverage(statsA.upload.avg_bits, statsB.upload.avg_bits)
: (statsA.upload.avg_bits ?? statsB.upload.avg_bits),
min: Math.min(statsA.upload.min, statsB.upload.min),
max: Math.max(statsA.upload.max, statsB.upload.max),
},
total_results: totalResults,
};
};
export const selectLatestResult = (
current: SpeedtestTrackerResult | null,
candidate: SpeedtestTrackerResult | null,
): SpeedtestTrackerResult | null => {
if (!candidate) return current;
if (!current) return candidate;
return candidate.created_at.getTime() > current.created_at.getTime() ? candidate : current;
};
export const combineSpeedtestDashboards = (
dashboards: SpeedtestTrackerDashboardData[],
): SpeedtestTrackerDashboardData =>
dashboards.reduce<SpeedtestTrackerDashboardData>(
(combined, dashboard) => ({
latestResult: selectLatestResult(combined.latestResult, dashboard.latestResult),
stats: mergeStats(combined.stats, dashboard.stats),
recentResults: [...combined.recentResults, ...dashboard.recentResults],
}),
{ latestResult: null, stats: null, recentResults: [] },
);
export const formatBitsPerSec = (bps: number): string => `${formatNumber(bps, 2)}bps`;
export const formatResultSpeed = (result: SpeedtestTrackerResult, dir: "download" | "upload"): string => {
const bits = dir === "download" ? result.download_bits : result.upload_bits;
if (bits != null) return formatBitsPerSec(bits);
return "—";
};
export const formatStatsSpeed = (band: SpeedtestTrackerStats["download"]): string => {
if (band.avg_bits != null) return formatBitsPerSec(band.avg_bits);
return formatBitsPerSec(band.avg * 8);
};
type CompactSection = "latest" | "chart" | "averages";
export const getCompactSections = (
height: number,
available: Record<CompactSection, boolean>,
): Record<CompactSection, boolean> => {
const budget = height < 240 ? 1 : height < 360 ? 2 : 3;
const visible = new Set(
(["latest", "chart", "averages"] as const).filter((section) => available[section]).slice(0, budget),
);
return {
latest: visible.has("latest"),
chart: visible.has("chart"),
averages: visible.has("averages"),
};
};