627 lines
22 KiB
TypeScript
627 lines
22 KiB
TypeScript
import {
|
|
Avatar,
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
Grid2 as Grid,
|
|
Theme,
|
|
Typography,
|
|
useTheme
|
|
} from "@mui/material";
|
|
import { useDeviceAPI, useGlobalState, useMutationAPI } from "providers";
|
|
import { Component, Device } from "models";
|
|
import moment from "moment";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { quack } from "protobuf-ts/quack";
|
|
import { GetDefaultDateRange } from "common/time/DateRange";
|
|
import React, { useEffect, useState } from "react";
|
|
import MultiLineGraph, { LineData } from "charts/measurementCharts/MultiLineGraph";
|
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
|
import DateSelect from "common/time/DateSelect";
|
|
import {
|
|
Area,
|
|
AreaChart,
|
|
ReferenceArea,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
TooltipProps,
|
|
XAxis,
|
|
YAxis
|
|
} from "recharts";
|
|
import FuelDark from "assets/components/fuelDark.png";
|
|
import FuelLight from "assets/components/fuelLight.png";
|
|
import { useThemeType } from "hooks";
|
|
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
|
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
|
|
import { UnitMeasurement } from "models/UnitMeasurement";
|
|
import MaterialChartTooltip from "charts/MaterialChartTooltip";
|
|
import { roundTo } from "utils";
|
|
import { makeStyles } from "@mui/styles";
|
|
//import TimeBar from "common/time/TimeBar";
|
|
|
|
interface Props {
|
|
device: pond.ComprehensiveDevice;
|
|
linearMutations: pond.LinearMutation[];
|
|
xDomain: string[] | number[];
|
|
changeXDomain: (domain: string[] | number[]) => void;
|
|
getFuelLevel?: (level: string) => void;
|
|
}
|
|
|
|
interface GraphData {
|
|
time: number;
|
|
value: number;
|
|
}
|
|
|
|
interface MutationData {
|
|
mutation: pond.LinearMutation;
|
|
data: pond.ValueAt[];
|
|
}
|
|
|
|
const useStyles = makeStyles((theme: Theme) => ({
|
|
avatarIcon: {
|
|
width: theme.spacing(3),
|
|
height: theme.spacing(3)
|
|
},
|
|
card: {
|
|
height: 550
|
|
},
|
|
cardHeader: {
|
|
height: "20%"
|
|
},
|
|
cardContent: {
|
|
height: "80%"
|
|
}
|
|
})
|
|
);
|
|
|
|
export default function ObjectHeaterCharts(props: Props) {
|
|
const { device, linearMutations, getFuelLevel, xDomain, changeXDomain } = props;
|
|
const deviceAPI = useDeviceAPI();
|
|
const mutationAPI = useMutationAPI();
|
|
const [tempHum, setTempHum] = useState<Component>();
|
|
const [ambientTemps, setAmbientTemps] = useState<LineData[]>([]);
|
|
const [heaterTemps, setHeaterTemps] = useState<pond.ExportedUnitMeasurement[]>([]);
|
|
const [heaterState, setHeaterState] = useState<pond.ExportedUnitMeasurement[]>([]);
|
|
const [tempOnly, setTempOnly] = useState<Component>();
|
|
const [heater, setHeater] = useState<Component>();
|
|
const [start, setStart] = useState(GetDefaultDateRange().start);
|
|
const [end, setEnd] = useState(GetDefaultDateRange().end);
|
|
const now = moment();
|
|
const theme = useTheme();
|
|
const [mutationData, setMutationData] = useState<MutationData[]>([]);
|
|
const [{ user, as }] = useGlobalState();
|
|
const themeType = useThemeType();
|
|
const classes = useStyles();
|
|
const [mutationsLoading, setMutationsLoading] = useState(false);
|
|
const [refLeft, setRefLeft] = useState<number | undefined>();
|
|
const [refRight, setRefRight] = useState<number | undefined>();
|
|
|
|
useEffect(() => {
|
|
if (!mutationsLoading) {
|
|
setMutationsLoading(true);
|
|
//make call to the backend to get measurements for the mutation
|
|
let md: MutationData[] = [];
|
|
linearMutations.forEach(mut => {
|
|
mutationAPI.linearMutation(mut, start.toISOString(), end.toISOString()).then(resp => {
|
|
let d: pond.ValueAt[] = resp.data.values;
|
|
if (mut.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
|
d = [];
|
|
resp.data.values.forEach(valAt => {
|
|
d.push(
|
|
pond.ValueAt.create({
|
|
time: valAt.time,
|
|
value: valAt.value / 10
|
|
})
|
|
);
|
|
});
|
|
}
|
|
md.push({
|
|
mutation: mut,
|
|
data: d
|
|
});
|
|
if (md.length >= linearMutations.length) {
|
|
setMutationsLoading(false);
|
|
}
|
|
setMutationData([...md]);
|
|
if (getFuelLevel && mut.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
|
if (resp.data.values.length > 0) {
|
|
getFuelLevel(
|
|
(resp.data.values[resp.data.values.length - 1].value / 10).toFixed(2) + "%"
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}, [linearMutations, start, end, getFuelLevel, mutationAPI]); //eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
useEffect(() => {
|
|
let tempHum: Component | undefined = undefined;
|
|
let tempOnly: Component | undefined = undefined;
|
|
let heater: Component | undefined = undefined;
|
|
|
|
let dev = Device.any(device.device);
|
|
let components = device.components;
|
|
let measurementsFor: Component[] = [];
|
|
components.forEach(comp => {
|
|
let c = Component.any(comp);
|
|
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_DHT) {
|
|
tempHum = c;
|
|
measurementsFor.push(c);
|
|
}
|
|
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_TEMPERATURE) {
|
|
tempOnly = c;
|
|
measurementsFor.push(c);
|
|
}
|
|
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT) {
|
|
if (c.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER) {
|
|
heater = c;
|
|
measurementsFor.push(c);
|
|
}
|
|
}
|
|
});
|
|
setTempHum(tempHum);
|
|
setTempOnly(tempOnly);
|
|
setHeater(heater);
|
|
deviceAPI
|
|
.listJSONMeasurements(dev.id(), measurementsFor, start, end, 0, 0, "asc", "timestamp", as)
|
|
.then(resp => {
|
|
resp.data.components.forEach(comp => {
|
|
if (comp.componentId === tempHum?.key()) {
|
|
if (comp.measurementType === "Temperature") {
|
|
let lineData: LineData[] = [];
|
|
comp.exportedMeasurements.forEach(m => {
|
|
lineData.push({
|
|
timestamp: moment(m.timestamp).valueOf(),
|
|
points: m.measurementValue.map((t, i) => ({ value: convertTemp(t), node: i }))
|
|
});
|
|
});
|
|
setAmbientTemps(lineData);
|
|
}
|
|
}
|
|
if (comp.componentId === tempOnly?.key()) {
|
|
if (comp.measurementType === "Temperature") {
|
|
setHeaterTemps(comp.exportedMeasurements);
|
|
}
|
|
}
|
|
if (comp.componentId === heater?.key()) {
|
|
if (comp.measurementType === "State") {
|
|
setHeaterState(comp.exportedMeasurements);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}, [device, deviceAPI, start, end, as]); //eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const updateDateRange = (newStartDate: any, newEndDate: any, live: boolean) => {
|
|
setStart(newStartDate);
|
|
setEnd(newEndDate);
|
|
// setLive(live);
|
|
// if (props.updateDate) {
|
|
// props.updateDate(newStartDate, newEndDate, live);
|
|
// }
|
|
};
|
|
|
|
const convertTemp = (temp: number) => {
|
|
let t = temp;
|
|
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
t = temp * 1.8 + 32;
|
|
}
|
|
return t;
|
|
};
|
|
|
|
//had to make custom chart for this in order to show the heater state and the temp of the ds18
|
|
const heaterStateWithTemps = (tempComponent: Component, heaterComponent: Component) => {
|
|
let tempDescriber = describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
|
tempComponent.type(),
|
|
tempComponent.subType(),
|
|
undefined,
|
|
user
|
|
);
|
|
let heaterDescriber = describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN,
|
|
heaterComponent.type(),
|
|
heaterComponent.subType(),
|
|
undefined,
|
|
user
|
|
);
|
|
let tempData: LineData[] = [];
|
|
heaterTemps.forEach(ht => {
|
|
tempData.push({
|
|
timestamp: moment(ht.timestamp).valueOf(),
|
|
points: ht.measurementValue.map((t, i) => ({ value: convertTemp(t), node: i }))
|
|
});
|
|
});
|
|
let stateData: LineData[] = [];
|
|
heaterState.forEach(hs => {
|
|
stateData.push({
|
|
timestamp: moment(hs.timestamp).valueOf(),
|
|
points: hs.measurementValue.map((s, i) => ({ value: s, node: i }))
|
|
});
|
|
});
|
|
return (
|
|
<React.Fragment>
|
|
<MultiLineGraph
|
|
describer={tempDescriber}
|
|
lineData={tempData}
|
|
numLines={1}
|
|
customHeight={"70%"}
|
|
tooltip
|
|
newXDomain={xDomain}
|
|
multiGraphZoom={domain => {
|
|
changeXDomain(domain);
|
|
}}
|
|
multiGraphZoomOut
|
|
/>
|
|
<MultiLineGraph
|
|
describer={heaterDescriber}
|
|
lineData={stateData}
|
|
numLines={1}
|
|
customHeight={"30%"}
|
|
tooltip
|
|
yMin={0}
|
|
yMax={1}
|
|
hideY
|
|
newXDomain={xDomain}
|
|
multiGraphZoom={domain => {
|
|
changeXDomain(domain);
|
|
}}
|
|
multiGraphZoomOut
|
|
/>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
//// TODO-CS: Make a mutator describer to put the get functions into ////
|
|
const getMutationIcon = (mutation: pond.Mutator, themeType: string) => {
|
|
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
|
return themeType === "light" ? FuelDark : FuelLight;
|
|
}
|
|
return "";
|
|
};
|
|
|
|
const getMutationLabel = (mutation: pond.Mutator) => {
|
|
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
|
return "Fuel Level";
|
|
}
|
|
return "";
|
|
};
|
|
|
|
const getMutationColour = (mutation: pond.Mutator) => {
|
|
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
|
return "lightBlue";
|
|
}
|
|
return "";
|
|
};
|
|
|
|
const getMutationUnit = (mutation: pond.Mutator) => {
|
|
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
|
return "%";
|
|
}
|
|
return "";
|
|
};
|
|
|
|
//// /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ ////
|
|
|
|
const zoom = () => {
|
|
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
|
|
if (refLeft && refRight && refLeft !== refRight) {
|
|
refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]);
|
|
setRefLeft(undefined);
|
|
setRefRight(undefined);
|
|
changeXDomain(newDomain);
|
|
}
|
|
};
|
|
|
|
const mutationGraphCard = (mutation: pond.LinearMutation, data: pond.ValueAt[], key: string) => {
|
|
let graphData: GraphData[] = [];
|
|
let lastReading: pond.ValueAt | undefined;
|
|
data.forEach(d => {
|
|
graphData.push({
|
|
time: moment(d.time).valueOf(),
|
|
value: d.value
|
|
});
|
|
lastReading = d;
|
|
});
|
|
return (
|
|
<Grid size={{
|
|
xs:12,
|
|
sm:12,
|
|
md:4
|
|
}}
|
|
key={key}>
|
|
<Card className={classes.card}>
|
|
{graphData.length > 0 ? (
|
|
<React.Fragment>
|
|
<CardHeader
|
|
className={classes.cardHeader}
|
|
avatar={
|
|
<Avatar
|
|
variant="square"
|
|
src={getMutationIcon(mutation.mutation, themeType)}
|
|
className={classes.avatarIcon}
|
|
alt={"mutIcon"}
|
|
/>
|
|
}
|
|
title={mutation.mutationName}
|
|
titleTypographyProps={{ variant: "subtitle1" }}
|
|
subheader={
|
|
<Grid container>
|
|
<Grid size={12}>
|
|
<Typography variant="caption" color="textPrimary">
|
|
<span>
|
|
{getMutationLabel(mutation.mutation) + ": "}{" "}
|
|
<span style={{ color: getMutationColour(mutation.mutation) }}>
|
|
{lastReading?.value.toFixed(2) + getMutationUnit(mutation.mutation)}
|
|
</span>{" "}
|
|
<br />
|
|
</span>
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={12}>
|
|
<Typography color="textSecondary" variant="caption">
|
|
{moment(lastReading?.time).fromNow()}
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
}
|
|
/>
|
|
<CardContent className={classes.cardContent}>
|
|
<ResponsiveContainer width={"100%"} height={"100%"}>
|
|
<AreaChart
|
|
data={graphData}
|
|
onMouseDown={(e: any) => {
|
|
if (e) {
|
|
setRefLeft(e.activeLabel);
|
|
}
|
|
}}
|
|
onMouseMove={(e: any) => {
|
|
if (e) {
|
|
setRefRight(e.activeLabel);
|
|
}
|
|
}}
|
|
onMouseUp={() => {
|
|
setRefLeft(undefined);
|
|
setRefRight(undefined);
|
|
zoom();
|
|
}}>
|
|
<Tooltip
|
|
animationEasing="ease-out"
|
|
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
|
|
labelFormatter={timestamp => moment(timestamp).format("lll")}
|
|
content={(props: TooltipProps<any, any>) => {
|
|
return (
|
|
<MaterialChartTooltip
|
|
{...props}
|
|
valueFormatter={value =>
|
|
roundTo(parseFloat(String(value)), 2).toString()
|
|
}
|
|
/>
|
|
);
|
|
}}
|
|
/>
|
|
<XAxis
|
|
dataKey="time"
|
|
allowDataOverflow
|
|
domain={xDomain}
|
|
name="Time"
|
|
tickFormatter={timestamp => {
|
|
let t = moment(timestamp);
|
|
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
|
|
}}
|
|
scale="time"
|
|
type="number"
|
|
tick={{ fill: theme.palette.text.primary }}
|
|
stroke={theme.palette.divider}
|
|
interval="preserveStartEnd"
|
|
/>
|
|
<YAxis
|
|
type="number"
|
|
domain={
|
|
mutation.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL
|
|
? [0, 100]
|
|
: ["auto", "auto"]
|
|
}
|
|
label={{
|
|
value: mutation.mutationName,
|
|
position: "insideLeft",
|
|
angle: -90,
|
|
fill: theme.palette.text.primary
|
|
}}
|
|
tick={{ fill: theme.palette.text.primary }}
|
|
/>
|
|
<Area dataKey={"value"} />
|
|
{refLeft && refRight ? (
|
|
<ReferenceArea x1={refLeft} x2={refRight} strokeOpacity={0.3} />
|
|
) : null}
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</React.Fragment>
|
|
) : (
|
|
<Box display="flex" justifyContent="center" alignItems="center" height="100%">
|
|
<Typography style={{ fontWeight: 650 }}>
|
|
No measurements for selected timeframe
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
</Grid>
|
|
);
|
|
};
|
|
|
|
const mutationGraphs = () => {
|
|
let graphCards: JSX.Element[] = [];
|
|
if (mutationData.length === 0) {
|
|
return (
|
|
<Grid size={{
|
|
xs:12,
|
|
sm:12,
|
|
md:4
|
|
}}>
|
|
<Card className={classes.card}>
|
|
<Box display="flex" justifyContent="center" alignItems="center" height={"100%"}>
|
|
<Typography style={{ fontWeight: 650 }}>No Calculated Measurements Set</Typography>
|
|
</Box>
|
|
</Card>
|
|
</Grid>
|
|
);
|
|
}
|
|
mutationData.forEach((mutation, i) => {
|
|
graphCards.push(mutationGraphCard(mutation.mutation, mutation.data, "mutation" + i));
|
|
});
|
|
return graphCards;
|
|
};
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{/* <TimeBar updateDateRange={updateDateRange} startDate={start} endDate={end}/> */}
|
|
<Grid container direction="row" spacing={1}>
|
|
<Grid size={{
|
|
xs:12,
|
|
sm:12,
|
|
md:4
|
|
}}>
|
|
{tempOnly && heater && (
|
|
<Card className={classes.card}>
|
|
{(heaterTemps.length > 0 || heaterState.length > 0) ? (
|
|
<React.Fragment>
|
|
<Grid container className={classes.cardHeader}>
|
|
{tempOnly && (
|
|
<Grid size={6}>
|
|
<CardHeader
|
|
avatar={
|
|
<Avatar
|
|
variant="square"
|
|
src={GetComponentIcon(tempOnly.type(), tempOnly.subType(), themeType)}
|
|
className={classes.avatarIcon}
|
|
alt={"tempIcon"}
|
|
/>
|
|
}
|
|
title={tempOnly.name()}
|
|
subheader={
|
|
<UnitMeasurementSummary
|
|
component={tempOnly}
|
|
reading={UnitMeasurement.convertLastMeasurement(
|
|
tempOnly.lastMeasurement.map(um => UnitMeasurement.any(um, user))
|
|
)}
|
|
/>
|
|
}
|
|
titleTypographyProps={{ variant: "subtitle1" }}
|
|
/>
|
|
</Grid>
|
|
)}
|
|
{heater && (
|
|
<Grid size={6}>
|
|
<CardHeader
|
|
avatar={
|
|
<Avatar
|
|
variant="square"
|
|
src={GetComponentIcon(heater.type(), heater.subType(), themeType)}
|
|
className={classes.avatarIcon}
|
|
alt={"tempIcon"}
|
|
/>
|
|
}
|
|
title={heater.name()}
|
|
subheader={
|
|
<UnitMeasurementSummary
|
|
component={heater}
|
|
reading={UnitMeasurement.convertLastMeasurement(
|
|
heater.lastMeasurement.map(um => UnitMeasurement.create(um, user))
|
|
)}
|
|
/>
|
|
}
|
|
titleTypographyProps={{ variant: "subtitle1" }}
|
|
/>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
|
|
<CardContent className={classes.cardContent}>
|
|
{heaterStateWithTemps(tempOnly, heater)}
|
|
</CardContent>
|
|
</React.Fragment>
|
|
) : (
|
|
<Box display="flex" justifyContent="center" alignItems="center" height={400}>
|
|
<Typography style={{ fontWeight: 650 }}>
|
|
No measurements for selected timeframe
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
)}
|
|
</Grid>
|
|
<Grid size={{
|
|
xs:12,
|
|
sm:12,
|
|
md:4
|
|
}}>
|
|
{tempHum && (
|
|
<Card className={classes.card}>
|
|
{ambientTemps.length > 0 ? (
|
|
<React.Fragment>
|
|
<CardHeader
|
|
className={classes.cardHeader}
|
|
avatar={
|
|
<Avatar
|
|
variant="square"
|
|
src={GetComponentIcon(tempHum.type(), tempHum.subType(), themeType)}
|
|
className={classes.avatarIcon}
|
|
alt={"tempIcon"}
|
|
/>
|
|
}
|
|
title={tempHum.name()}
|
|
subheader={
|
|
<UnitMeasurementSummary
|
|
component={tempHum}
|
|
reading={UnitMeasurement.convertLastMeasurement(
|
|
tempHum.lastMeasurement
|
|
.filter(
|
|
um => um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
|
|
)
|
|
.map(um => UnitMeasurement.any(um, user))
|
|
)}
|
|
/>
|
|
}
|
|
titleTypographyProps={{ variant: "subtitle1" }}
|
|
/>
|
|
<CardContent className={classes.cardContent}>
|
|
<MultiLineGraph
|
|
describer={describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
|
tempHum.type(),
|
|
tempHum.subType(),
|
|
undefined,
|
|
user
|
|
)}
|
|
lineData={ambientTemps}
|
|
numLines={1}
|
|
customHeight={"100%"}
|
|
tooltip
|
|
newXDomain={xDomain}
|
|
multiGraphZoom={domain => {
|
|
changeXDomain(domain);
|
|
}}
|
|
multiGraphZoomOut
|
|
/>
|
|
</CardContent>
|
|
</React.Fragment>
|
|
) : (
|
|
<Box display="flex" justifyContent="center" alignItems="center" height={400}>
|
|
<Typography style={{ fontWeight: 650 }}>
|
|
No measurements for selected timeframe
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Card>
|
|
)}
|
|
</Grid>
|
|
{mutationGraphs()}
|
|
</Grid>
|
|
|
|
<DateSelect updateDateRange={updateDateRange} startDate={start} endDate={end} />
|
|
</React.Fragment>
|
|
);
|
|
}
|