diff --git a/src/bin/binSensorsDisplay.tsx b/src/bin/binSensorsDisplay.tsx index 6f46c7a..09b5a3d 100644 --- a/src/bin/binSensorsDisplay.tsx +++ b/src/bin/binSensorsDisplay.tsx @@ -1,5 +1,5 @@ import { AccessTime, MoreVert } from "@mui/icons-material"; -import { Avatar, Box, Button, Card, Grid2, IconButton, Menu, MenuItem, Typography, useTheme } from "@mui/material"; +import { Avatar, Box, Button, Card, Checkbox, FormControlLabel, Grid2, IconButton, Menu, MenuItem, Typography, useTheme } from "@mui/material"; import BinSensorCard from "bin/BinSensorCard"; import { GetDefaultDateRange } from "common/time/DateRange"; import { ExtractMoisture } from "grain"; @@ -71,6 +71,7 @@ export default function BinSensorsDisplay(props: Props){ const defaultDateRange = GetDefaultDateRange(); const [startDate, setStartDate] = useState(defaultDateRange.start); const [endDate, setEndDate] = useState(defaultDateRange.end); + const [filterNodes, setFilterNodes] = useState(true) //organize the components into the correct 'positions' using the bins preferences useEffect(()=>{ @@ -303,21 +304,21 @@ export default function BinSensorsDisplay(props: Props){ const rows: { label: string; min: string; avg: string; max: string }[] = [ { label: `Temp ${unit}`, - min: cable.minTemp(user.tempUnit()).toFixed(2), - avg: cable.aveTemp(user.tempUnit()).toFixed(2), - max: cable.maxTemp(user.tempUnit()).toFixed(2), + min: cable.minTemp(user.tempUnit(), filterNodes).toFixed(2), + avg: cable.aveTemp(user.tempUnit(), filterNodes).toFixed(2), + max: cable.maxTemp(user.tempUnit(), filterNodes).toFixed(2), }, ...(hasHumidity ? [{ label: "RH %", - min: cable.minHumidity().toFixed(2), - avg: cable.aveHumidity().toFixed(2), - max: cable.maxHumidity().toFixed(2), + min: cable.minHumidity(filterNodes).toFixed(2), + avg: cable.aveHumidity(filterNodes).toFixed(2), + max: cable.maxHumidity(filterNodes).toFixed(2), }] : []), ...(hasMoisture ? [{ label: "EMC %", - min: cable.minMoisture().toFixed(2), - avg: cable.aveMoisture().toFixed(2), - max: cable.maxMoisture().toFixed(2), + min: cable.minMoisture(filterNodes).toFixed(2), + avg: cable.aveMoisture(filterNodes).toFixed(2), + max: cable.maxMoisture(filterNodes).toFixed(2), }] : []), ] @@ -695,6 +696,20 @@ export default function BinSensorsDisplay(props: Props){ {showGraphs && } + {cables.length > 0 && + { + setFilterNodes(!filterNodes) + }} + /> + } + label={Show All Nodes + } + /> + } ) } @@ -718,11 +733,12 @@ export default function BinSensorsDisplay(props: Props){ { setComponentUnnassigned(false); setSelectedComponentType(cable.type()); @@ -752,7 +768,7 @@ export default function BinSensorsDisplay(props: Props){ title={plenum.name()} tag={"Plenum"} device={device} - component={plenum.key()} + component={components.get(plenum.key())!} startDate={startDate} endDate={endDate} onMenuClick={(event) => { @@ -783,7 +799,7 @@ export default function BinSensorsDisplay(props: Props){ title={pressure.name()} tag={"Pressure"} device={device} - component={pressure.key()} + component={components.get(pressure.key())!} startDate={startDate} endDate={endDate} onMenuClick={(event) => { @@ -813,7 +829,7 @@ export default function BinSensorsDisplay(props: Props){ title={ambient.name()} tag={"Ambient"} device={device} - component={ambient.key()} + component={components.get(ambient.key())!} startDate={startDate} endDate={endDate} onMenuClick={(event) => { @@ -844,7 +860,7 @@ export default function BinSensorsDisplay(props: Props){ title={h.name()} tag={"Headspace"} device={device} - component={h.key()} + component={components.get(h.key())!} startDate={startDate} endDate={endDate} onMenuClick={(event) => { @@ -870,7 +886,7 @@ export default function BinSensorsDisplay(props: Props){ let icon = GetComponentIcon(co2.type(), co2.subType(), themeType) return( - + ) } @@ -885,7 +901,7 @@ export default function BinSensorsDisplay(props: Props){ if(showGraphs && device){ return( - + ) } @@ -908,7 +924,7 @@ export default function BinSensorsDisplay(props: Props){ if(showGraphs && device){ return( - + ) } @@ -923,7 +939,7 @@ export default function BinSensorsDisplay(props: Props){ if(showGraphs && device){ return( - + ) } diff --git a/src/bin/graphs/BinSensorGraph.tsx b/src/bin/graphs/BinSensorGraph.tsx index 6d277b8..443e49e 100644 --- a/src/bin/graphs/BinSensorGraph.tsx +++ b/src/bin/graphs/BinSensorGraph.tsx @@ -1,8 +1,10 @@ -import { Avatar, Box, Card, CardHeader, CircularProgress, IconButton, Paper, Typography, useTheme } from "@mui/material"; +import { Avatar, Box, Card, CircularProgress, IconButton, Paper, Typography, useTheme } from "@mui/material"; import moment, { Moment } from "moment"; +import { Component } from "models"; import { useComponentAPI, useGlobalState } from "providers"; import { useEffect, useMemo, useState } from "react"; import { UnitMeasurement } from "models/UnitMeasurement"; +import { quack } from "protobuf-ts/quack"; import { Legend, Line, @@ -20,12 +22,47 @@ interface Props { device: number; icon?: string; title?: string; - tag?: string; + tag?: string; onMenuClick?: (event: React.MouseEvent) => void; - component: string; + component: Component; startDate: Moment; endDate: Moment; customHeight?: string | number; + /** + * this will cause all of the readings for a cable to be used in the average, + * by default it will filter any excluded nodes and only use nodes below the top node + */ + allNodes?: boolean +} + +/** Grain-cable nodes to include when averaging (matches GrainCable.filter indexing). */ +function valuesForAveraging( + nodeValues: number[], + component: Component, + allNodes?: boolean +): number[] { + if (allNodes) { + return nodeValues.filter(v => !isNaN(v)); + } + + const grainFilledTo = component.settings.grainFilledTo ?? 0; + const excludedNodes = component.settings.excludedNodes ?? []; + const included: number[] = []; + + nodeValues.forEach((v, index) => { + if (isNaN(v)) { + return; + } + if (excludedNodes.includes(index)) { + return; + } + if (grainFilledTo > 0 && index >= grainFilledTo) { + return; + } + included.push(v); + }); + + return included; } interface MeasurementMeta { @@ -54,7 +91,11 @@ function normalizeValue(value: number, min: number, max: number): number { return ((value - min) / (max - min)) * 100; } -function buildChartData(measurements: UnitMeasurement[]): { +function buildChartData( + measurements: UnitMeasurement[], + component: Component, + allNodes?: boolean +): { data: ChartPoint[]; series: SeriesInfo[]; } { @@ -66,7 +107,7 @@ function buildChartData(measurements: UnitMeasurement[]): { if (valArray.error || !um.timestamps[i]) { return; } - const nodeValues = valArray.values.filter(v => !isNaN(v)); + const nodeValues = valuesForAveraging(valArray.values, component, allNodes); if (nodeValues.length === 0) { return; } @@ -137,7 +178,7 @@ function SensorGraphTooltip(props: TooltipProps) { } export default function BinSensorGraph(props: Props) { - const { device, icon, title, tag, component, startDate, endDate, customHeight = 350, onMenuClick } = props; + const { device, icon, title, tag, component, startDate, endDate, customHeight = 350, onMenuClick, allNodes } = props; const [measurements, setMeasurements] = useState([]); const [loading, setLoading] = useState(false); const componentAPI = useComponentAPI(); @@ -148,7 +189,7 @@ export default function BinSensorGraph(props: Props) { useEffect(() => { setLoading(true); componentAPI - .sampleUnitMeasurements(device, component, startDate, endDate, 1000) + .sampleUnitMeasurements(device, component.key(), startDate, endDate, 1000, undefined, undefined, undefined, undefined, true) .then(res => { setMeasurements(res.data.measurements.map(um => UnitMeasurement.any(um, user))); }) @@ -160,7 +201,10 @@ export default function BinSensorGraph(props: Props) { }); }, [device, component, startDate, endDate, componentAPI, user]); - const { data, series } = useMemo(() => buildChartData(measurements), [measurements]); + const { data, series } = useMemo( + () => buildChartData(measurements, component, allNodes), + [measurements, component, allNodes] + ); return ( @@ -170,7 +214,7 @@ export default function BinSensorGraph(props: Props) { } diff --git a/src/models/GrainCable.ts b/src/models/GrainCable.ts index dff0dab..4de1ae4 100644 --- a/src/models/GrainCable.ts +++ b/src/models/GrainCable.ts @@ -132,21 +132,43 @@ export class GrainCable { return describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour(); } - public minTemp(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS) { + public minTemp(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS, inGrain?: boolean) { + if(inGrain){ + let grainMax = Math.min(...this.filteredNodes(true).temps) + if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) + return this.farenheit(grainMax) + return grainMax + } if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) return this.farenheit(Math.min(...this.temperatures)); return Math.min(...this.temperatures); } - public minHumidity() { + public minHumidity(inGrain?: boolean) { + if(inGrain){ + return Math.max(...this.filteredNodes(true).humids) + } return Math.min(...this.humidities); } - public minMoisture() { + public minMoisture(inGrain?: boolean) { + if(inGrain){ + return Math.min(...this.filteredNodes(true).moistures) + } return Math.min(...this.grainMoistures); } - public aveTemp(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS) { + /** + * Averages the temperatures of the grain cable, if filtered is true it will filter the excluded nodes and nodes above the top node before averaging + * @param unit the unit of the temperature + * @param inGrain whether to filter the excluded nodes and nodes above the top node before averaging + * @returns + */ + public aveTemp(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS, inGrain?: boolean) { + if(inGrain){ + let filteredNodes = this.filteredNodes(true) + return filteredNodes.temps.reduce((p: any, c: any) => p + c, 0) / filteredNodes.temps.length + } if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) return this.farenheit( this.temperatures.reduce((p: any, c: any) => p + c, 0) / this.temperatures.length @@ -154,25 +176,45 @@ export class GrainCable { return this.temperatures.reduce((p: any, c: any) => p + c, 0) / this.temperatures.length; } - public aveHumidity() { + public aveHumidity(inGrain?: boolean) { + if(inGrain){ + let filteredNodes = this.filteredNodes(true) + return filteredNodes.humids.reduce((p: any, c: any) => p + c, 0) / filteredNodes.humids.length + } return this.humidities.reduce((p: any, c: any) => p + c, 0) / this.humidities.length; } - public aveMoisture() { + public aveMoisture(inGrain?: boolean) { + if(inGrain){ + let filteredNodes = this.filteredNodes(true) + return filteredNodes.moistures.reduce((p: any, c: any) => p + c, 0) / filteredNodes.moistures.length + } return this.grainMoistures.reduce((p: any, c: any) => p + c, 0) / this.grainMoistures.length; } - public maxTemp(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS) { + public maxTemp(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS, inGrain?: boolean) { + if(inGrain){ + let grainMax = Math.max(...this.filteredNodes(true).temps) + if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) + return this.farenheit(grainMax) + return grainMax + } if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) return this.farenheit(Math.max(...this.temperatures)); return Math.max(...this.temperatures); } - public maxHumidity() { + public maxHumidity(inGrain?: boolean) { + if(inGrain){ + return Math.max(...this.filteredNodes(true).humids) + } return Math.max(...this.humidities); } - public maxMoisture() { + public maxMoisture(inGrain?: boolean) { + if(inGrain){ + return Math.max(...this.filteredNodes(true).moistures) + } return Math.min(...this.grainMoistures); }