Display live and cached consumption on dashboard

This commit is contained in:
2024-09-02 22:17:34 +02:00
parent 539703b904
commit 1784a0a1f8
7 changed files with 604 additions and 1 deletions

View File

@ -0,0 +1,26 @@
import { Api } from "@mui/icons-material";
import { APIClient } from "./ApiClient";
export class EnergyApi {
/**
* Get current house consumption
*/
static async CurrConsumption(): Promise<number> {
const data = await APIClient.exec({
method: "GET",
uri: "/energy/curr_consumption",
});
return data.data.consumption;
}
/**
* Get current cached consumption
*/
static async CachedConsumption(): Promise<number> {
const data = await APIClient.exec({
method: "GET",
uri: "/energy/cached_consumption",
});
return data.data.consumption;
}
}

View File

@ -1,3 +1,27 @@
import { Typography } from "@mui/material";
import { CurrConsumptionWidget } from "./HomeRoute/CurrConsumptionWidget";
import Grid from "@mui/material/Grid2";
import { CachedConsumptionWidget } from "./HomeRoute/CachedConsumptionWidget";
export function HomeRoute(): React.ReactElement {
return <>home authenticated todo</>;
return (
<div style={{ flex: 1, padding: "10px" }}>
<Typography component="h2" variant="h6" sx={{ mb: 2 }}>
Overview
</Typography>
<Grid
container
spacing={2}
columns={12}
sx={{ mb: (theme) => theme.spacing(2) }}
>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<CurrConsumptionWidget />
</Grid>
<Grid size={{ xs: 12, sm: 6, lg: 3 }}>
<CachedConsumptionWidget />
</Grid>
</Grid>
</div>
);
}

View File

@ -0,0 +1,37 @@
import React from "react";
import { EnergyApi } from "../../api/EnergyApi";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
import StatCard from "../../widgets/StatCard";
export function CachedConsumptionWidget(): React.ReactElement {
const snackbar = useSnackbar();
const [val, setVal] = React.useState<undefined | number>();
const refresh = async () => {
try {
const s = await EnergyApi.CachedConsumption();
setVal(s);
} catch (e) {
console.error(e);
snackbar("Failed to refresh cached consumption!");
}
};
React.useEffect(() => {
refresh();
const i = setInterval(() => refresh(), 3000);
return () => clearInterval(i);
});
return (
<StatCard
title="Cached consumption"
data={[]}
interval="Current data"
trend="neutral"
value={val?.toString() ?? "Loading"}
/>
);
}

View File

@ -0,0 +1,37 @@
import React from "react";
import { EnergyApi } from "../../api/EnergyApi";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
import StatCard from "../../widgets/StatCard";
export function CurrConsumptionWidget(): React.ReactElement {
const snackbar = useSnackbar();
const [val, setVal] = React.useState<undefined | number>();
const refresh = async () => {
try {
const s = await EnergyApi.CurrConsumption();
setVal(s);
} catch (e) {
console.error(e);
snackbar("Failed to refresh current consumption!");
}
};
React.useEffect(() => {
refresh();
const i = setInterval(() => refresh(), 3000);
return () => clearInterval(i);
});
return (
<StatCard
title="Current consumption"
data={[]}
interval="Current data"
trend="neutral"
value={val?.toString() ?? "Loading"}
/>
);
}

View File

@ -0,0 +1,128 @@
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Chip from "@mui/material/Chip";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { SparkLineChart } from "@mui/x-charts/SparkLineChart";
import { areaElementClasses } from "@mui/x-charts/LineChart";
export type StatCardProps = {
title: string;
value: string;
interval: string;
trend: "up" | "down" | "neutral";
data: number[];
};
function getDaysInMonth(month: number, year: number) {
const date = new Date(year, month, 0);
const monthName = date.toLocaleDateString("en-US", {
month: "short",
});
const daysInMonth = date.getDate();
const days = [];
let i = 1;
while (days.length < daysInMonth) {
days.push(`${monthName} ${i}`);
i += 1;
}
return days;
}
function AreaGradient({ color, id }: { color: string; id: string }) {
return (
<defs>
<linearGradient id={id} x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stopColor={color} stopOpacity={0.3} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
</defs>
);
}
export default function StatCard({
title,
value,
interval,
trend,
data,
}: StatCardProps) {
const theme = useTheme();
const daysInWeek = getDaysInMonth(4, 2024);
const trendColors = {
up:
theme.palette.mode === "light"
? theme.palette.success.main
: theme.palette.success.dark,
down:
theme.palette.mode === "light"
? theme.palette.error.main
: theme.palette.error.dark,
neutral:
theme.palette.mode === "light"
? theme.palette.grey[400]
: theme.palette.grey[700],
};
const labelColors = {
up: "success" as const,
down: "error" as const,
neutral: "default" as const,
};
const color = labelColors[trend];
const chartColor = trendColors[trend];
const trendValues = { up: "+25%", down: "-25%", neutral: "+5%" };
return (
<Card variant="outlined" sx={{ height: "100%", flexGrow: 1 }}>
<CardContent>
<Typography component="h2" variant="subtitle2" gutterBottom>
{title}
</Typography>
<Stack
direction="column"
sx={{ justifyContent: "space-between", flexGrow: "1", gap: 1 }}
>
<Stack sx={{ justifyContent: "space-between" }}>
<Stack
direction="row"
sx={{ justifyContent: "space-between", alignItems: "center" }}
>
<Typography variant="h4" component="p">
{value}
</Typography>
<Chip size="small" color={color} label={trendValues[trend]} />
</Stack>
<Typography variant="caption" sx={{ color: "text.secondary" }}>
{interval}
</Typography>
</Stack>
<Box sx={{ width: "100%", height: 50 }}>
<SparkLineChart
colors={[chartColor]}
data={data}
area
showHighlight
showTooltip
xAxis={{
scaleType: "band",
data: daysInWeek, // Use the correct property 'data' for xAxis
}}
sx={{
[`& .${areaElementClasses.root}`]: {
fill: `url(#area-gradient-${value})`,
},
}}
>
<AreaGradient color={chartColor} id={`area-gradient-${value}`} />
</SparkLineChart>
</Box>
</Stack>
</CardContent>
</Card>
);
}