added and all nodes option for the cards and graphs

This commit is contained in:
csawatzky 2026-05-29 12:03:56 -06:00
parent 99f37f7bda
commit c3ee4a518c
3 changed files with 141 additions and 39 deletions

View file

@ -1,5 +1,5 @@
import { AccessTime, MoreVert } from "@mui/icons-material"; 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 BinSensorCard from "bin/BinSensorCard";
import { GetDefaultDateRange } from "common/time/DateRange"; import { GetDefaultDateRange } from "common/time/DateRange";
import { ExtractMoisture } from "grain"; import { ExtractMoisture } from "grain";
@ -71,6 +71,7 @@ export default function BinSensorsDisplay(props: Props){
const defaultDateRange = GetDefaultDateRange(); const defaultDateRange = GetDefaultDateRange();
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start); const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end); const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const [filterNodes, setFilterNodes] = useState(true)
//organize the components into the correct 'positions' using the bins preferences //organize the components into the correct 'positions' using the bins preferences
useEffect(()=>{ useEffect(()=>{
@ -303,21 +304,21 @@ export default function BinSensorsDisplay(props: Props){
const rows: { label: string; min: string; avg: string; max: string }[] = [ const rows: { label: string; min: string; avg: string; max: string }[] = [
{ {
label: `Temp ${unit}`, label: `Temp ${unit}`,
min: cable.minTemp(user.tempUnit()).toFixed(2), min: cable.minTemp(user.tempUnit(), filterNodes).toFixed(2),
avg: cable.aveTemp(user.tempUnit()).toFixed(2), avg: cable.aveTemp(user.tempUnit(), filterNodes).toFixed(2),
max: cable.maxTemp(user.tempUnit()).toFixed(2), max: cable.maxTemp(user.tempUnit(), filterNodes).toFixed(2),
}, },
...(hasHumidity ? [{ ...(hasHumidity ? [{
label: "RH %", label: "RH %",
min: cable.minHumidity().toFixed(2), min: cable.minHumidity(filterNodes).toFixed(2),
avg: cable.aveHumidity().toFixed(2), avg: cable.aveHumidity(filterNodes).toFixed(2),
max: cable.maxHumidity().toFixed(2), max: cable.maxHumidity(filterNodes).toFixed(2),
}] : []), }] : []),
...(hasMoisture ? [{ ...(hasMoisture ? [{
label: "EMC %", label: "EMC %",
min: cable.minMoisture().toFixed(2), min: cable.minMoisture(filterNodes).toFixed(2),
avg: cable.aveMoisture().toFixed(2), avg: cable.aveMoisture(filterNodes).toFixed(2),
max: cable.maxMoisture().toFixed(2), max: cable.maxMoisture(filterNodes).toFixed(2),
}] : []), }] : []),
] ]
@ -695,6 +696,20 @@ export default function BinSensorsDisplay(props: Props){
{showGraphs && {showGraphs &&
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} /> <TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
} }
{cables.length > 0 &&
<FormControlLabel
control={
<Checkbox
checked={!filterNodes}
onChange={(e, checked) => {
setFilterNodes(!filterNodes)
}}
/>
}
label={<Typography>Show All Nodes
</Typography>}
/>
}
</Box> </Box>
) )
} }
@ -718,11 +733,12 @@ export default function BinSensorsDisplay(props: Props){
<BinSensorGraph <BinSensorGraph
icon={icon} icon={icon}
title={cable.name()} title={cable.name()}
tag={"Cable"} tag={"Cable Averages"}
device={device} device={device}
component={cable.key()} component={components.get(cable.key())!}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
allNodes={!filterNodes}
onMenuClick={(event) => { onMenuClick={(event) => {
setComponentUnnassigned(false); setComponentUnnassigned(false);
setSelectedComponentType(cable.type()); setSelectedComponentType(cable.type());
@ -752,7 +768,7 @@ export default function BinSensorsDisplay(props: Props){
title={plenum.name()} title={plenum.name()}
tag={"Plenum"} tag={"Plenum"}
device={device} device={device}
component={plenum.key()} component={components.get(plenum.key())!}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
onMenuClick={(event) => { onMenuClick={(event) => {
@ -783,7 +799,7 @@ export default function BinSensorsDisplay(props: Props){
title={pressure.name()} title={pressure.name()}
tag={"Pressure"} tag={"Pressure"}
device={device} device={device}
component={pressure.key()} component={components.get(pressure.key())!}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
onMenuClick={(event) => { onMenuClick={(event) => {
@ -813,7 +829,7 @@ export default function BinSensorsDisplay(props: Props){
title={ambient.name()} title={ambient.name()}
tag={"Ambient"} tag={"Ambient"}
device={device} device={device}
component={ambient.key()} component={components.get(ambient.key())!}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
onMenuClick={(event) => { onMenuClick={(event) => {
@ -844,7 +860,7 @@ export default function BinSensorsDisplay(props: Props){
title={h.name()} title={h.name()}
tag={"Headspace"} tag={"Headspace"}
device={device} device={device}
component={h.key()} component={components.get(h.key())!}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
onMenuClick={(event) => { onMenuClick={(event) => {
@ -870,7 +886,7 @@ export default function BinSensorsDisplay(props: Props){
let icon = GetComponentIcon(co2.type(), co2.subType(), themeType) let icon = GetComponentIcon(co2.type(), co2.subType(), themeType)
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={co2.key()} startDate={startDate} endDate={endDate} /> <BinSensorGraph device={device} component={components.get(co2.key())!} startDate={startDate} endDate={endDate} />
</Grid2> </Grid2>
) )
} }
@ -885,7 +901,7 @@ export default function BinSensorsDisplay(props: Props){
if(showGraphs && device){ if(showGraphs && device){
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={lidar.key()} startDate={startDate} endDate={endDate} /> <BinSensorGraph device={device} component={components.get(lidar.key())!} startDate={startDate} endDate={endDate} />
</Grid2> </Grid2>
) )
} }
@ -908,7 +924,7 @@ export default function BinSensorsDisplay(props: Props){
if(showGraphs && device){ if(showGraphs && device){
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={heater.key()} startDate={startDate} endDate={endDate} /> <BinSensorGraph device={device} component={components.get(heater.key())!} startDate={startDate} endDate={endDate} />
</Grid2> </Grid2>
) )
} }
@ -923,7 +939,7 @@ export default function BinSensorsDisplay(props: Props){
if(showGraphs && device){ if(showGraphs && device){
return( return(
<Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}> <Grid2 size={{xs: 6, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph device={device} component={fan.key()} startDate={startDate} endDate={endDate} /> <BinSensorGraph device={device} component={components.get(fan.key())!} startDate={startDate} endDate={endDate} />
</Grid2> </Grid2>
) )
} }

View file

@ -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 moment, { Moment } from "moment";
import { Component } from "models";
import { useComponentAPI, useGlobalState } from "providers"; import { useComponentAPI, useGlobalState } from "providers";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { UnitMeasurement } from "models/UnitMeasurement"; import { UnitMeasurement } from "models/UnitMeasurement";
import { quack } from "protobuf-ts/quack";
import { import {
Legend, Legend,
Line, Line,
@ -20,12 +22,47 @@ interface Props {
device: number; device: number;
icon?: string; icon?: string;
title?: string; title?: string;
tag?: string; tag?: string;
onMenuClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onMenuClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
component: string; component: Component;
startDate: Moment; startDate: Moment;
endDate: Moment; endDate: Moment;
customHeight?: string | number; 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 { interface MeasurementMeta {
@ -54,7 +91,11 @@ function normalizeValue(value: number, min: number, max: number): number {
return ((value - min) / (max - min)) * 100; return ((value - min) / (max - min)) * 100;
} }
function buildChartData(measurements: UnitMeasurement[]): { function buildChartData(
measurements: UnitMeasurement[],
component: Component,
allNodes?: boolean
): {
data: ChartPoint[]; data: ChartPoint[];
series: SeriesInfo[]; series: SeriesInfo[];
} { } {
@ -66,7 +107,7 @@ function buildChartData(measurements: UnitMeasurement[]): {
if (valArray.error || !um.timestamps[i]) { if (valArray.error || !um.timestamps[i]) {
return; return;
} }
const nodeValues = valArray.values.filter(v => !isNaN(v)); const nodeValues = valuesForAveraging(valArray.values, component, allNodes);
if (nodeValues.length === 0) { if (nodeValues.length === 0) {
return; return;
} }
@ -137,7 +178,7 @@ function SensorGraphTooltip(props: TooltipProps<number, string>) {
} }
export default function BinSensorGraph(props: Props) { 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<UnitMeasurement[]>([]); const [measurements, setMeasurements] = useState<UnitMeasurement[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const componentAPI = useComponentAPI(); const componentAPI = useComponentAPI();
@ -148,7 +189,7 @@ export default function BinSensorGraph(props: Props) {
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
componentAPI componentAPI
.sampleUnitMeasurements(device, component, startDate, endDate, 1000) .sampleUnitMeasurements(device, component.key(), startDate, endDate, 1000, undefined, undefined, undefined, undefined, true)
.then(res => { .then(res => {
setMeasurements(res.data.measurements.map(um => UnitMeasurement.any(um, user))); 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]); }, [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 ( return (
<Card raised sx={{ height: "100%", padding: 1.5 }}> <Card raised sx={{ height: "100%", padding: 1.5 }}>
@ -170,7 +214,7 @@ export default function BinSensorGraph(props: Props) {
<Avatar <Avatar
variant="square" variant="square"
src={icon} src={icon}
alt={name + " icon"} alt={(title ?? "sensor") + " icon"}
style={{ width: theme.spacing(3), height: theme.spacing(3) }} style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/> />
} }

View file

@ -132,21 +132,43 @@ export class GrainCable {
return describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour(); 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) if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT)
return this.farenheit(Math.min(...this.temperatures)); return this.farenheit(Math.min(...this.temperatures));
return 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); 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); 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) if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT)
return this.farenheit( return this.farenheit(
this.temperatures.reduce((p: any, c: any) => p + c, 0) / this.temperatures.length 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; 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; 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; 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) if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT)
return this.farenheit(Math.max(...this.temperatures)); return this.farenheit(Math.max(...this.temperatures));
return 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); 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); return Math.min(...this.grainMoistures);
} }