feat: add GPU monitoring support via Glances integration (#5255)
* feat: add GPU monitoring support via Glances integration - Add GPU fields to SystemHealthMonitoring interface (gpuId, name, memoryUtilization, processorUtilization, temperature, fanSpeed) - Extend Glances integration to parse GPU data from /api/4/all endpoint (supports NVIDIA, AMD, and Intel GPUs via Glances) - Add GPU utilization ring to Health Monitoring widget with tooltip showing VRAM usage, temperature, and fan speed - Add GPU time-series chart to System Resources widget (grape color) - Add GPU toggle option to both widgets - Add English translations for GPU labels - Add gpu: [] to all other integrations (dashdot, truenas, unraid, openmediavault) for backward compatibility - Add GPU mock data for development/testing Closes community request from PR #5102 for GPU monitoring support. * fix: implement GPU monitoring for dashdot integration and clarify OMV comment - Add GPU load fetching via dashdot /load/gpu endpoint - Add GPU info parsing from dashdot /info endpoint - Add gpuLoadApi and gpu field to internalServerInfoApi schemas - Add getCurrentGpuLoadAsync method to DashDotIntegration - Map GPU load data to SystemHealthMonitoring gpu array - Add explanatory comment for OMV's empty gpu array Addresses review comments from manuel-rw on PR #5255 * Delete manifest.json
This commit is contained in:
1 parent
d7f0596db8
commit
003dac4744
15 files changed
+231
-2
No files matched your search
@@ -19,6 +19,9 @@ export const { definition, componentLoader } = createWidgetDefinition("healthMon
|
||||
memory: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
gpu: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
showUptime: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Center, RingProgress, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { IconDeviceDesktop } from "@tabler/icons-react";
|
||||
|
||||
import { progressColor } from "../system-health";
|
||||
|
||||
interface GpuRingProps {
|
||||
gpu: {
|
||||
gpuId: string;
|
||||
name: string;
|
||||
memoryUtilization: number;
|
||||
processorUtilization: number;
|
||||
temperature: number | null;
|
||||
fanSpeed: number | null;
|
||||
};
|
||||
isTiny: boolean;
|
||||
fahrenheit: boolean;
|
||||
}
|
||||
|
||||
export const GpuRing = ({ gpu, isTiny, fahrenheit }: GpuRingProps) => {
|
||||
const tempDisplay =
|
||||
gpu.temperature != null
|
||||
? fahrenheit
|
||||
? `${(gpu.temperature * 1.8 + 32).toFixed(0)}°F`
|
||||
: `${gpu.temperature}°C`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={
|
||||
<Stack gap={2}>
|
||||
<Text size="xs" fw={600}>
|
||||
{gpu.name}
|
||||
</Text>
|
||||
<Text size="xs">VRAM: {gpu.memoryUtilization.toFixed(1)}%</Text>
|
||||
{tempDisplay && <Text size="xs">Temp: {tempDisplay}</Text>}
|
||||
{gpu.fanSpeed != null && <Text size="xs">Fan: {gpu.fanSpeed} RPM</Text>}
|
||||
</Stack>
|
||||
}
|
||||
multiline
|
||||
>
|
||||
<RingProgress
|
||||
className={`health-monitoring-gpu health-monitoring-gpu-${gpu.gpuId}`}
|
||||
roundCaps
|
||||
size={isTiny ? 50 : 100}
|
||||
thickness={isTiny ? 4 : 8}
|
||||
label={
|
||||
<Center style={{ flexDirection: "column" }}>
|
||||
<Text
|
||||
className="health-monitoring-gpu-utilization-value"
|
||||
size={isTiny ? "8px" : "xs"}
|
||||
>{`${gpu.processorUtilization.toFixed(0)}%`}</Text>
|
||||
<IconDeviceDesktop className="health-monitoring-gpu-utilization-icon" size={isTiny ? 8 : 16} />
|
||||
</Center>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: Number(gpu.processorUtilization.toFixed(2)),
|
||||
color: progressColor(Number(gpu.processorUtilization.toFixed(2))),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -39,6 +39,7 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { CpuRing } from "./rings/cpu-ring";
|
||||
import { CpuTempRing } from "./rings/cpu-temp-ring";
|
||||
import { GpuRing } from "./rings/gpu-ring";
|
||||
import { formatMemoryUsage, MemoryRing } from "./rings/memory-ring";
|
||||
import classes from "./system-health.module.css";
|
||||
|
||||
@@ -187,6 +188,10 @@ export const SystemHealthMonitoring = ({
|
||||
isTiny={isTiny}
|
||||
/>
|
||||
)}
|
||||
{options.gpu &&
|
||||
healthInfo.gpu.map((gpu) => (
|
||||
<GpuRing key={gpu.gpuId} gpu={gpu} isTiny={isTiny} fahrenheit={options.fahrenheit} />
|
||||
))}
|
||||
</Flex>
|
||||
{
|
||||
<Text className="health-monitoring-status-update-time" c="dimmed" size="xs" ta="center">
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Paper, Text } from "@mantine/core";
|
||||
import { IconDeviceDesktop } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { LabelDisplayModeOption } from "..";
|
||||
import { CommonChart } from "./common-chart";
|
||||
|
||||
export const SystemResourceGPUChart = ({
|
||||
gpuUsageOverTime,
|
||||
hasShadow,
|
||||
labelDisplayMode,
|
||||
}: {
|
||||
gpuUsageOverTime: number[];
|
||||
hasShadow: boolean;
|
||||
labelDisplayMode: LabelDisplayModeOption;
|
||||
}) => {
|
||||
const chartData = gpuUsageOverTime.map((usage, index) => ({ index, usage }));
|
||||
const t = useScopedI18n("widget.systemResources.card");
|
||||
|
||||
return (
|
||||
<CommonChart
|
||||
data={chartData}
|
||||
dataKey={"index"}
|
||||
series={[{ name: "usage", color: "grape.5" }]}
|
||||
title={t("gpu")}
|
||||
icon={IconDeviceDesktop}
|
||||
lastValue={
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
gpuUsageOverTime.length > 0 ? `${Math.round(gpuUsageOverTime[gpuUsageOverTime.length - 1]!)}%` : undefined
|
||||
}
|
||||
chartType={hasShadow ? "area" : "line"}
|
||||
yAxisProps={{ domain: [0, 100] }}
|
||||
labelDisplayMode={labelDisplayMode}
|
||||
tooltipProps={{
|
||||
content: ({ payload }) => {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
const value = payload[0] ? Number(payload[0].value) : 0;
|
||||
return (
|
||||
<Paper px={3} py={2} withBorder shadow="md" radius="md">
|
||||
<Text c="dimmed" size="xs">
|
||||
{value.toFixed(0)}%
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { CombinedNetworkTrafficChart } from "./chart/combined-network-traffic";
|
||||
import { SystemResourceCPUChart } from "./chart/cpu-chart";
|
||||
import { SystemResourceGPUChart } from "./chart/gpu-chart";
|
||||
import { SystemResourceMemoryChart } from "./chart/memory-chart";
|
||||
import { NetworkTrafficChart } from "./chart/network-traffic";
|
||||
|
||||
@@ -23,10 +24,16 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
|
||||
const memoryCapacityInBytes =
|
||||
(data[0]?.healthInfo.memAvailableInBytes ?? 0) + (data[0]?.healthInfo.memUsedInBytes ?? 0);
|
||||
|
||||
const [items, setItems] = useState<{ cpu: number; memory: number; network: { up: number; down: number } | null }[]>(
|
||||
const [items, setItems] = useState<
|
||||
{ cpu: number; memory: number; gpu: number; network: { up: number; down: number } | null }[]
|
||||
>(
|
||||
data.map((item) => ({
|
||||
cpu: item.healthInfo.cpuUtilization,
|
||||
memory: item.healthInfo.memUsedInBytes,
|
||||
gpu:
|
||||
item.healthInfo.gpu.length > 0
|
||||
? item.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / item.healthInfo.gpu.length
|
||||
: 0,
|
||||
network: item.healthInfo.network,
|
||||
})),
|
||||
);
|
||||
@@ -41,6 +48,10 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
|
||||
const next = {
|
||||
cpu: data.healthInfo.cpuUtilization,
|
||||
memory: data.healthInfo.memUsedInBytes,
|
||||
gpu:
|
||||
data.healthInfo.gpu.length > 0
|
||||
? data.healthInfo.gpu.reduce((acc, g) => acc + g.processorUtilization, 0) / data.healthInfo.gpu.length
|
||||
: 0,
|
||||
network: data.healthInfo.network,
|
||||
};
|
||||
|
||||
@@ -75,6 +86,15 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{options.visibleCharts.includes("gpu") && (
|
||||
<Box h={rowHeight}>
|
||||
<SystemResourceGPUChart
|
||||
gpuUsageOverTime={items.map((item) => item.gpu)}
|
||||
hasShadow={options.hasShadow}
|
||||
labelDisplayMode={options.labelDisplayMode}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{showNetwork &&
|
||||
(width > 256 ? (
|
||||
<Group h={rowHeight} gap="xs" grow>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const { definition, componentLoader } = createWidgetDefinition("systemRes
|
||||
return optionsBuilder.from((factory) => ({
|
||||
hasShadow: factory.switch({ defaultValue: true }),
|
||||
visibleCharts: factory.multiSelect({
|
||||
options: (["cpu", "memory", "network"] as const).map((key) => ({
|
||||
options: (["cpu", "memory", "gpu", "network"] as const).map((key) => ({
|
||||
value: key,
|
||||
label: (t) => t(`widget.systemResources.option.visibleCharts.option.${key}`),
|
||||
})),
|
||||
|
||||
Reference in new issue
Block a user