Display live and cached consumption on dashboard
This commit is contained in:
26
central_frontend/src/api/EnergyApi.ts
Normal file
26
central_frontend/src/api/EnergyApi.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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"}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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"}
|
||||
/>
|
||||
);
|
||||
}
|
128
central_frontend/src/widgets/StatCard.tsx
Normal file
128
central_frontend/src/widgets/StatCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user