223 lines
No EOL
8.6 KiB
TypeScript
223 lines
No EOL
8.6 KiB
TypeScript
import { Card, CardHeader, Grid2, Typography } from "@mui/material"
|
|
import { blue, orange } from "@mui/material/colors"
|
|
import SingleSetAreaChart, { SSAreaDataPoint } from "charts/SingleSetAreaChart"
|
|
import { useComponentAPI } from "hooks"
|
|
import { cloneDeep } from "lodash"
|
|
import { Component } from "models"
|
|
import { UnitMeasurement } from "models/UnitMeasurement"
|
|
import moment from "moment"
|
|
import { Moment } from "moment"
|
|
import { pond } from "protobuf-ts/pond"
|
|
import { quack } from "protobuf-ts/quack"
|
|
import { useGlobalState } from "providers"
|
|
import { useEffect, useState } from "react"
|
|
import { getTemperatureUnit } from "utils"
|
|
|
|
|
|
interface Props {
|
|
//the key for the temperature chain, this may be overhauled to only be a single node in the future for the outlet and the 'inlet' would be the ambient
|
|
deviceID: number
|
|
tempComponent: Component
|
|
ambient?: Component
|
|
start: Moment;
|
|
end: Moment;
|
|
}
|
|
|
|
export default function GateDeltaTempGraph(props: Props){
|
|
const {
|
|
tempComponent,
|
|
ambient,
|
|
deviceID,
|
|
start,
|
|
end
|
|
} = props
|
|
const [data, setData] = useState<SSAreaDataPoint[]>([])
|
|
const [{user}] = useGlobalState()
|
|
const componentAPI = useComponentAPI();
|
|
const [recent, setRecent] = useState<SSAreaDataPoint>()
|
|
const warmingColour = orange["500"]
|
|
const coolingColour = blue["500"]
|
|
|
|
const getUnitByType = (unitMeasurements: UnitMeasurement[], type: quack.MeasurementType) => {
|
|
return unitMeasurements.find(u => u.type === type);
|
|
}
|
|
|
|
const buildDeltas = (ambientMeasurements: UnitMeasurement[], tempCompMeasurements: UnitMeasurement[], measurementType: quack.MeasurementType) => {
|
|
const ambient = getUnitByType(ambientMeasurements, measurementType);
|
|
const outlet = getUnitByType(tempCompMeasurements, measurementType);
|
|
|
|
if (!ambient || !outlet) return [];
|
|
|
|
let i = 0;
|
|
let j = 0;
|
|
|
|
let lastAmbient: number | null = null;
|
|
let lastOutlet: number | null = null;
|
|
|
|
const deltas: SSAreaDataPoint[] = [];
|
|
|
|
const ambientTimes = ambient.timestamps.map(t => moment(t).valueOf()).reverse();
|
|
const outletTimes = outlet.timestamps.map(t => moment(t).valueOf()).reverse();
|
|
const ambientValues = [...ambient.values].reverse();
|
|
const outletValues = [...outlet.values].reverse();
|
|
|
|
while (i < ambientTimes.length || j < outletTimes.length) {
|
|
|
|
const aTime = i < ambientTimes.length ? ambientTimes[i] : Infinity;
|
|
const bTime = j < outletTimes.length ? outletTimes[j] : Infinity;
|
|
|
|
if (aTime <= bTime) {
|
|
const valueArray = ambientValues[i++];
|
|
if (!valueArray.error && valueArray.values.length > 0) {
|
|
lastAmbient = valueArray.values[valueArray.values.length -1];
|
|
}
|
|
|
|
if (lastAmbient !== null && lastOutlet !== null) {
|
|
deltas.push({
|
|
timestamp: aTime,
|
|
value: Math.round((lastOutlet - lastAmbient)*100)/100
|
|
});
|
|
}
|
|
|
|
} else {
|
|
const valueArray = outletValues[j++];
|
|
if (!valueArray.error && valueArray.values.length > 0) {
|
|
lastOutlet = valueArray.values[valueArray.values.length -1];
|
|
}
|
|
|
|
if (lastAmbient !== null && lastOutlet !== null) {
|
|
deltas.push({
|
|
timestamp: bTime,
|
|
value: Math.round((lastOutlet - lastAmbient)*100)/100
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return deltas
|
|
}
|
|
|
|
/**
|
|
* the use effect will use the temp component passed in get the measurements, then use the first node and the last node to determine the difference,
|
|
* if there is only one node, or only one node is reading due to errors, then it will use that as both the inlet and outlet so the delta will be 0,
|
|
*
|
|
* if an ambient is passed in it will use the readings from the ambient as the inlet and the temp component as the outlet
|
|
*/
|
|
useEffect(()=>{
|
|
//get the measurements for the temp component
|
|
componentAPI.sampleUnitMeasurements(
|
|
deviceID,
|
|
tempComponent.key(),
|
|
start.toISOString(),
|
|
end.toISOString(),
|
|
500
|
|
).then(resp => {
|
|
console.log(resp)
|
|
if(resp.data.measurements){
|
|
let tempCompMeasurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
|
|
//if there is an ambient component, then treat the temp component like a single sensor and not a chain, and use the ambient measurements as the inlet
|
|
if(ambient){
|
|
//need to then load the measurements for the ambient component
|
|
componentAPI.sampleUnitMeasurements(
|
|
deviceID,
|
|
ambient.key(),
|
|
start.toISOString(),
|
|
end.toISOString(),
|
|
500
|
|
).then(resp => {
|
|
let ambientMeasurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
|
|
//now loop through each of them to 'combine' them into a single array
|
|
const deltas = buildDeltas(ambientMeasurements, tempCompMeasurements, quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE)
|
|
if(deltas.length > 0){
|
|
setData(deltas)
|
|
setRecent(deltas[deltas.length-1])
|
|
}
|
|
})
|
|
|
|
}else{//else no ambient is passed in treat the temp component like a chain
|
|
let deltas: SSAreaDataPoint[] = []
|
|
tempCompMeasurements.forEach(um => {
|
|
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE){
|
|
um.values.forEach((nodes, index) => {
|
|
if (nodes.values && nodes.values.length > 0 && um.timestamps[index]){
|
|
//use the first node as the inlet
|
|
let inletNode = nodes.values[0]
|
|
//use the second node as the outlet, if there is an error or there is only one node then it will be used as both and the delta will be 0
|
|
let outletNode = nodes.values[nodes.values.length -1]
|
|
let newDelta: SSAreaDataPoint = {
|
|
value: Math.round((outletNode - inletNode)*100)/100,
|
|
timestamp: moment(um.timestamps[index]).valueOf(),
|
|
}
|
|
deltas.push(newDelta)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
setData(deltas)
|
|
//use the components status to set the recent to show in the header of the card
|
|
let recent: SSAreaDataPoint | undefined
|
|
if(tempComponent.status.measurement){
|
|
tempComponent.status.measurement.forEach(um => {
|
|
if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE){
|
|
let clone = cloneDeep(um)
|
|
let m = UnitMeasurement.create(clone, user)
|
|
if(m.values.length > 0){
|
|
let lastReading = m.values[m.values.length - 1]
|
|
if (lastReading.values.length > 0){
|
|
recent = {
|
|
value: lastReading.values[lastReading.values.length -1] - lastReading.values[0],
|
|
timestamp: moment(m.timestamps[m.values.length - 1]).valueOf()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
setRecent(recent)
|
|
}
|
|
}
|
|
})
|
|
},[tempComponent, ambient, deviceID, start, end])
|
|
|
|
return (
|
|
<Card raised style={{ padding: 10, marginBottom: 15, marginTop: 15 }}>
|
|
<CardHeader
|
|
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>Delta Temp</Typography>}
|
|
subheader={
|
|
recent ?
|
|
(
|
|
<Grid2 container>
|
|
<Grid2 size={12}>
|
|
<span>
|
|
{"Delta Temp: "}
|
|
<span style={{ color: recent.value > 0 ? warmingColour : coolingColour, fontWeight: 500 }}>
|
|
{recent.value.toFixed(2) + (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")}
|
|
</span>
|
|
<br />
|
|
</span>
|
|
</Grid2>
|
|
<Grid2 size={12}>
|
|
<Typography color="textSecondary" variant={"caption"}>
|
|
{moment(recent.timestamp).fromNow()}
|
|
</Typography>
|
|
</Grid2>
|
|
</Grid2>
|
|
)
|
|
: (
|
|
<Typography variant="body1" color="textPrimary">
|
|
No Data
|
|
</Typography>
|
|
)
|
|
|
|
}
|
|
/>
|
|
<SingleSetAreaChart
|
|
data={data}
|
|
tooltipLabel="Delta Temp"
|
|
tooltipUnit={getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C"}
|
|
yAxisLabel={"Delta T" + (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? " °F" : " °C")}
|
|
colourAboveZero={{colour: warmingColour, label: "Heating"}}
|
|
colourBelowZero={{colour: coolingColour, label: "Cooling"}}/>
|
|
</Card>
|
|
)
|
|
|
|
} |