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