113 lines
4.1 KiB
TypeScript
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"),
|
|
};
|
|
};
|