frontend/src/bin/binSensorsDisplay.tsx

1032 lines
No EOL
45 KiB
TypeScript

import { AccessTime, MoreVert } from "@mui/icons-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";
import { useMobile, useThemeType } from "hooks";
import { Bin, Component } from "models";
import { Ambient } from "models/Ambient";
import { CO2 } from "models/CO2";
import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable";
import { Headspace } from "models/Headspace";
import { Plenum } from "models/Plenum";
import { Pressure } from "models/Pressure";
import moment, { Moment } from "moment";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useBinAPI, useComponentAPI, useGlobalState, useSnackbar } from "providers";
import React, { useEffect, useState } from "react";
import { avg } from "utils";
import BinSensorGraph from "./graphs/BinSensorGraph";
import TimeBar from "common/time/TimeBar";
interface Props {
bin: Bin
components: Map<string, Component>;
componentDevices: Map<string, number>;
preferences?: Map<string, pond.BinComponentPreferences>;
setPreferences: React.Dispatch<
React.SetStateAction<Map<string, pond.BinComponentPreferences> | undefined>
>;
}
export default function BinSensorsDisplay(props: Props){
const {bin, components, componentDevices, preferences, setPreferences} = props
const themeType = useThemeType()
const [{user, as}] = useGlobalState()
const binAPI = useBinAPI()
const componentAPI = useComponentAPI()
const theme = useTheme()
const snackbar = useSnackbar()
const [cables, setCables] = useState<GrainCable[]>([])
const [plenums, setPlenums] = useState<Plenum[]>([])
const [ambient, setAmbient] = useState<Ambient[]>([])
const [pressures, setPressures] = useState<Pressure[]>([])
const [fans, setFans] = useState<Controller[]>([])
const [heaters, setHeaters] = useState<Controller[]>([])
//the above component preferences all have only components that are one type of sensor, however headspace has components of three different sensors T/H, CO2, and Lidar
const [headspaceTH, setHeadspaceTH] = useState<Headspace[]>([]) //the components in the headspace that measure temp/humidity
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]) //the CO2 components in the headspace
const [headspaceLidar, setHeadspaceLidar] = useState<Component[]>([]) //the lidar components in the headspace
const [unassignedComponents, setUnassignedComponents] = useState<Component[]>([])
//using counts so i dont have to have to check multiple arrays for sensors and controllers
const [sensorCount, setSensorCount] = useState(0)
const [controllerCount, setControllerCount] = useState(0)
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
//only used to determine what preferences are options for the selected component ie. DHT can be a plenum or a headspace
const [selectedComponentType, setSelectedComponentType] = useState<
quack.ComponentType | undefined
>(undefined);
const [componentUnassigned, setComponentUnnassigned] = useState(false);
const [selectedComponentKey, setSelectedComponentKey] = useState("");
const [selectedComponentSubtype, setSelectedComponentSubtype] = useState(0);
const [selectedComponentTopNode, setSelectedComponentTopNode] = useState(0);
const [showGraphs, setShowGraphs] = useState(false);
const defaultDateRange = GetDefaultDateRange();
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const [filterNodes, setFilterNodes] = useState(true)
const isMobile = useMobile()
//organize the components into the correct 'positions' using the bins preferences
useEffect(()=>{
let unassigned: Component[] = []
let cables: GrainCable[] = []
let plenums: Plenum[] = []
let ambients: Ambient[] = []
let pressures: Pressure[] = []
let fans: Controller[] = []
let heaters: Controller[] = []
let headspaceTH: Headspace[] = []
let headspaceCO2: CO2[] = []
let headspaceLidar: Component[] = []
let sensorCount = 0
let controllerCount = 0
components.forEach(comp => {
if(preferences){
let pref = preferences.get(comp.key())
switch(pref?.type){
case pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE:
cables.push(GrainCable.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_PLENUM:
plenums.push(Plenum.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_AMBIENT:
ambients.push(Ambient.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_PRESSURE:
pressures.push(Pressure.create(comp))
sensorCount++
break;
case pond.BinComponent.BIN_COMPONENT_FAN:
fans.push(Controller.create(comp))
controllerCount++
break;
case pond.BinComponent.BIN_COMPONENT_HEATER:
heaters.push(Controller.create(comp))
controllerCount++
break;
case pond.BinComponent.BIN_COMPONENT_HEADSPACE:
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_DHT) {
headspaceTH.push(Headspace.create(comp));
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) {
headspaceLidar.push(comp);
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) {
let coComp = CO2.create(comp)
headspaceCO2.push(coComp);
}
sensorCount++
break;
default:
unassigned.push(comp)
}
}else{
unassigned.push(comp)
}
})
// console.log(cables)
// console.log(unassigned)
setSensorCount(sensorCount)
setControllerCount(controllerCount)
setCables(cables)
setPlenums(plenums)
setPressures(pressures)
setAmbient(ambients)
setFans(fans)
setHeaters(heaters)
setHeadspaceTH(headspaceTH)
setHeadspaceCO2(headspaceCO2)
setHeadspaceLidar(headspaceLidar)
setUnassignedComponents(unassigned)
},[components, preferences])
const selectType = (component: string, type: pond.BinComponent, topNode?: number) => {
setAnchorEl(null);
if (!preferences) return
let p = preferences.get(component);
if (p) {
p.type = type;
p.node = topNode ?? 0;
binAPI
.updateComponentPreferences(bin.key(), component, p, as)
.then(() => {
snackbar.success("Component preferences updated");
preferences.set(component, p!);
setPreferences(new Map(preferences));
})
.catch(() => {
snackbar.error("Component preference update failed");
});
}
};
const getOptions = () => {
let options: JSX.Element[] = [];
if (!componentUnassigned) {
options.push(
<MenuItem
key="remove"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_UNKNOWN);
}}>
Remove
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE
) {
options.push(
<MenuItem
key="ambientOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_AMBIENT);
}}>
Ambient
</MenuItem>
);
options.push(
<MenuItem
key="plenumOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PLENUM);
}}>
Plenum
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_LIDAR ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_CO2
) {
options.push(
<MenuItem
key="headspaceOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_HEADSPACE);
}}>
Headspace
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE
) {
options.push(
<MenuItem
key="pressureOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PRESSURE);
}}>
Plenum Pressure
</MenuItem>
);
}
if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
options.push(
<MenuItem
key="cableOption"
onClick={() => {
selectType(
selectedComponentKey,
pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE,
selectedComponentTopNode
);
}}>
Grain Cable
</MenuItem>
);
}
if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT) {
options.push(
<MenuItem
key="controlOption"
onClick={() => {
if (
selectedComponentSubtype === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER
) {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_HEATER);
} else if (
selectedComponentSubtype ===
quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN
) {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_FAN);
}
}}>
Heaters & Fans
</MenuItem>
);
}
if (options.length === 0) {
return <MenuItem>No Available Options</MenuItem>;
} else {
return options;
}
};
const assignmentMenu = () => {
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{getOptions()}
</Menu>
);
};
const cableCard = (cable: GrainCable) => {
const component = cable.asComponent()
let icon = GetComponentIcon(component.type(), component.subType(), themeType)
let unit = user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "°F" : "°C"
const hasHumidity = cable.aveHumidity() !== 0
const hasMoisture = cable.grainMoistures.length > 0
const rows: { label: string; min: string; avg: string; max: string }[] = [
{
label: `Temp ${unit}`,
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(filterNodes).toFixed(2),
avg: cable.aveHumidity(filterNodes).toFixed(2),
max: cable.maxHumidity(filterNodes).toFixed(2),
}] : []),
...(hasMoisture ? [{
label: "EMC %",
min: cable.minMoisture(filterNodes).toFixed(2),
avg: cable.aveMoisture(filterNodes).toFixed(2),
max: cable.maxMoisture(filterNodes).toFixed(2),
}] : []),
]
return (
<BinSensorCard
icon={icon}
lastReading={cable.lastReading}
name={cable.name()}
tag={"Cable"}
cableRows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(cable.type());
setSelectedComponentKey(cable.key());
setSelectedComponentTopNode(cable.settings.grainFilledTo);
setAnchorEl(event.currentTarget);
}}/>
)
}
const tempHumidCard = (sensor: Plenum | Ambient | Headspace, showEMC?: boolean) => {
let icon = GetComponentIcon(sensor.settings.type, sensor.settings.subtype, themeType)
let prefDisplay = ""
switch (preferences?.get(sensor.key())?.type){
case pond.BinComponent.BIN_COMPONENT_PLENUM:
prefDisplay = "Plenum"
break;
case pond.BinComponent.BIN_COMPONENT_AMBIENT:
prefDisplay = "Ambient"
break;
case pond.BinComponent.BIN_COMPONENT_HEADSPACE:
prefDisplay = "Headspace"
}
let rows: {label: string, data: string}[] = [
// build temperature row data
{
label: "Temp " + (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "°F" : "°C"),
data: (user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? sensor.temperature * (9 / 5) + 32 : sensor.temperature).toFixed(2)
},
// build humidity row
{
label: "RH %",
data: sensor.humidity.toFixed(2)
}
]
// build moisture row assuming the bin has a grain type to use to calculatee the moisture
if(showEMC && bin.grain() !== pond.Grain.GRAIN_INVALID && bin.grain() !== pond.Grain.GRAIN_NONE){
let emc = ExtractMoisture(
bin.grain(),
sensor.temperature,
sensor.humidity,
bin.customGrain()
)
rows.push(
{
label: "EMC %",
data: emc.toFixed(2)
}
)
}
return (
<BinSensorCard
lastReading={sensor.lastReading}
name={sensor.name()}
tag={prefDisplay}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(sensor.type());
setSelectedComponentKey(sensor.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const pressureCard = (pressure: Pressure) => {
let icon = GetComponentIcon(pressure.settings.type, pressure.settings.subtype, themeType)
let rows: {label: string, data: string}[] = [
{
label: "Pressure " + (user.pressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? "iwg" : "kPa"),
data: (user.pressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? pressure.pascals/249 : pressure.pascals/1000).toFixed(2)
}
]
if(pressure.airflow){
rows.push({
label: "Airflow CFM",
data: pressure.airflow.toFixed(2)
})
}
return (
<BinSensorCard
lastReading={pressure.lastReading}
name={pressure.name()}
tag={"Pressure"}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(pressure.type());
setSelectedComponentKey(pressure.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const co2Card = (sensor: CO2) => {
let icon = GetComponentIcon(sensor.settings.type, sensor.settings.subtype, themeType)
let rows: {label: string, data: string}[] = [
{
label: "CO2 ppm",
data: sensor.ppm.toFixed(2)
}
]
return (
<BinSensorCard
lastReading={sensor.lastReading}
name={sensor.name()}
tag={"Headspace"}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(sensor.type());
setSelectedComponentKey(sensor.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const lidarCard = (sensor: Component) => {
let icon = GetComponentIcon(sensor.settings.type, sensor.settings.subtype, themeType)
let rows: {label: string, data: string}[] = []
let lastReading = moment.now().toString()
sensor.lastMeasurement.forEach(unitMeasurement => {
let label = ""
let value = 0
if(sensor.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR){
if(unitMeasurement.type === quack.MeasurementType.MEASUREMENT_TYPE_DISTANCE_CM){
label = "Lidar " + (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? " ft" : " M")
if(unitMeasurement.timestamps.length > 0){
lastReading = unitMeasurement.timestamps[0]
}
if (unitMeasurement.values.length > 0){
//distance is stored in cm so needs to be converted
value = avg(unitMeasurement.values[0].values)
value = user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? value/30.48 : value/100
}
}
}
rows.push({label: label, data: value.toFixed(2)})
})
return (
<BinSensorCard
lastReading={lastReading}
name={sensor.name()}
tag={"Headspace"}
icon={icon}
rows={rows}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(sensor.type());
setSelectedComponentKey(sensor.key());
setAnchorEl(event.currentTarget);
}}
/>
)
}
const controllerCard = (controller: Controller) => {
const component = controller.asComponent()
let icon = GetComponentIcon(component.type(), component.subType(), themeType)
const isStale = moment().diff(moment(controller.lastReading), "hours") > 6
const isAuto = controller.mode === quack.OutputMode.OUTPUT_MODE_AUTO
const isOn = controller.mode === quack.OutputMode.OUTPUT_MODE_ON
const isOff = controller.mode === quack.OutputMode.OUTPUT_MODE_OFF
const prefType = preferences?.get(controller.key())?.type
const tag = prefType === pond.BinComponent.BIN_COMPONENT_FAN ? "Fan" : "Heater"
const segStyle = (active: boolean, activeColor?: string): React.CSSProperties => ({
flex: 1,
padding: "5px 0",
fontSize: 11,
fontWeight: 500,
textAlign: "center",
cursor: "pointer",
borderRadius: 0,
backgroundColor: active ? (activeColor ?? theme.palette.success.main) : "transparent",
color: active ? "#fff" : theme.palette.text.secondary,
border: "none",
transition: "background-color 0.15s",
})
return (
<Card raised sx={{ height: "100%" }}>
<Box padding={1.5} height="100%" display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1}>
<Box display="flex" alignItems="center" gap={1}>
<Avatar
variant="square"
src={icon}
alt={controller.name() + " icon"}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
<Typography fontWeight={500}>{controller.name()}</Typography>
<Typography variant="caption" color="textSecondary">- {tag}</Typography>
</Box>
<IconButton
size="small"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false)
setSelectedComponentType(controller.type())
setSelectedComponentKey(controller.key())
setAnchorEl(event.currentTarget)
}}
>
<MoreVert fontSize="small" />
</IconButton>
</Box>
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={1.5}>
<Typography variant="caption" color="textSecondary">Current state</Typography>
<Box display="flex" alignItems="center" gap={0.5}>
<Box
sx={{
width: 7,
height: 7,
borderRadius: "50%",
backgroundColor: controller.on
? theme.palette.success.main
: theme.palette.text.disabled,
}}
/>
<Typography
variant="caption"
fontWeight={500}
color={controller.on ? "success.main" : "text.secondary"}
>
{controller.on ? "Running" : isAuto ? "Standby" : "Off"}
</Typography>
</Box>
</Box>
<Box
display="flex"
sx={{
border: `0.5px solid ${theme.palette.divider}`,
borderRadius: 1,
overflow: "hidden",
}}
>
{[
{ label: "Auto", mode: quack.OutputMode.OUTPUT_MODE_AUTO, active: isAuto },
{ label: "On", mode: quack.OutputMode.OUTPUT_MODE_ON, active: isOn },
{ label: "Off", mode: quack.OutputMode.OUTPUT_MODE_OFF, active: isOff },
].map(({ label, mode, active }, i) => (
<Box
key={label}
component="button"
onClick={() => {
//this will update the components settings here to change the mode
console.log("changing mode: " + mode)
controller.settings.defaultOutputState = mode
let device = componentDevices.get(controller.key())
if(device){
componentAPI.update(device, controller.settings).then(resp => {
controller.mode = mode // ← update the field the UI reads
if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) {
setFans(prev => prev.map(f => f.key() === controller.key() ? controller : f))
} else {
setHeaters(prev => prev.map(h => h.key() === controller.key() ? controller : h))
}
}).catch(err => {
console.log("there was a problem updating the controller")
})
}
}}
sx={{
...segStyle(active, label === "Off" ? theme.palette.action.selected : undefined),
borderLeft: i > 0 ? `0.5px solid ${theme.palette.divider}` : "none",
}}
>
{label}
</Box>
))}
</Box>
<Box display="flex" alignItems="center" gap={0.5} marginTop="auto" paddingTop={1}>
<AccessTime sx={{ fontSize: 12, color: isStale ? "warning.main" : "text.disabled" }} />
<Typography variant="caption" color={isStale ? "warning.main" : "text.secondary"}>
{moment(controller.lastReading).fromNow()}
</Typography>
</Box>
</Box>
</Card>
)
}
const unassignedComponentCard = (component: Component) => {
let icon = GetComponentIcon(component.type(), component.subType(), themeType)
const lastReading = component.lastMeasurement.length > 0
? component.lastMeasurement[0].timestamps[0] ?? ""
: ""
const isStale = moment().diff(moment(lastReading), "hours") > 6
return (
<Card raised sx={{ height: "100%" }}>
<Box padding={1.5} height="100%" display="flex" flexDirection="column">
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box display="flex" alignItems="center" gap={1}>
<Avatar
variant="square"
src={icon}
alt={component.name() + " icon"}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
<Typography fontWeight={500}>{component.name()}</Typography>
</Box>
<IconButton
size="small"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(true)
setSelectedComponentType(component.type())
setSelectedComponentKey(component.key())
setSelectedComponentSubtype(component.subType())
setSelectedComponentTopNode(0)
setAnchorEl(event.currentTarget)
}}
>
<MoreVert fontSize="small" />
</IconButton>
</Box>
<Box display="flex" alignItems="center" gap={0.5} marginTop="auto">
<AccessTime sx={{ fontSize: 12, color: isStale ? "warning.main" : "text.disabled" }} />
<Typography variant="caption" color={isStale ? "warning.main" : "text.secondary"}>
{lastReading ? moment(lastReading).fromNow() : "No readings yet"}
</Typography>
</Box>
</Box>
</Card>
)
}
const updateDateRange = (newStartDate: any, newEndDate: any) => {
let range = GetDefaultDateRange();
range.start = newStartDate;
range.end = newEndDate;
setStartDate(newStartDate);
setEndDate(newEndDate);
};
const graphControls = () => {
return (
<Box display="flex" gap={2} alignItems="center">
<Button variant="contained" color="primary" onClick={() => {
setShowGraphs(!showGraphs)
}}>
{showGraphs ? "Show Cards" : "Show Graphs"}
</Button>
{showGraphs && !isMobile &&
<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>
)
}
return (
<Box padding={1}>
{assignmentMenu()}
{sensorCount > 0 &&
<Box>
<Box display="flex" gap={2} alignItems="center" marginBottom={1}>
<Typography sx={{fontWeight: 650, fontSize: 25}}>Sensors</Typography>
{graphControls()}
</Box>
{showGraphs && isMobile &&
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
}
<Grid2 container spacing={2}>
{plenums.map(plenum => {
let device = componentDevices.get(plenum.key())
if(showGraphs && device){
let icon = GetComponentIcon(plenum.type(), plenum.subType(), themeType)
return(
<Grid2 key={plenum.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={plenum.name()}
tag={"Plenum"}
device={device}
component={components.get(plenum.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(plenum.type());
setSelectedComponentKey(plenum.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 key={plenum.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(plenum)}
</Grid2>
)
}
)}
{pressures.map(pressure => {
let device = componentDevices.get(pressure.key())
if(showGraphs && device){
let icon = GetComponentIcon(pressure.type(), pressure.subType(), themeType)
return(
<Grid2 key={pressure.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={pressure.name()}
tag={"Pressure"}
device={device}
component={components.get(pressure.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(pressure.type());
setSelectedComponentKey(pressure.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 key={pressure.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{pressureCard(pressure)}
</Grid2>
)
})}
{ambient.map(ambient => {
let device = componentDevices.get(ambient.key())
if(showGraphs && device){
let icon = GetComponentIcon(ambient.type(), ambient.subType(), themeType)
return(
<Grid2 key={ambient.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={ambient.name()}
tag={"Ambient"}
device={device}
component={components.get(ambient.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(ambient.type());
setSelectedComponentKey(ambient.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 key={ambient.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(ambient)}
</Grid2>
)
}
)}
{headspaceTH.map(h => {
let device = componentDevices.get(h.key())
if(showGraphs && device){
let icon = GetComponentIcon(h.type(), h.subType(), themeType)
return(
<Grid2 key={h.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={h.name()}
tag={"Headspace"}
device={device}
component={components.get(h.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(h.type());
setSelectedComponentKey(h.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 key={h.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{tempHumidCard(h)}
</Grid2>
)
})}
{headspaceCO2.map(co2 => {
let device = componentDevices.get(co2.key())
if(showGraphs && device){
let icon = GetComponentIcon(co2.type(), co2.subType(), themeType)
return(
<Grid2 key={co2.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={co2.name()}
tag={"Headspace"}
device={device}
component={components.get(co2.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(co2.type());
setSelectedComponentKey(co2.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 key={co2.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{co2Card(co2)}
</Grid2>
)
})}
{headspaceLidar.map(lidar => {
let device = componentDevices.get(lidar.key())
if(showGraphs && device){
let icon = GetComponentIcon(lidar.type(), lidar.subType(), themeType)
return(
<Grid2 key={lidar.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={lidar.name()}
tag={"Headspace"}
device={device}
component={components.get(lidar.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(lidar.type());
setSelectedComponentKey(lidar.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return (
<Grid2 key={lidar.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{lidarCard(lidar)}
</Grid2>
)
})}
{cables.map(cable => {
let device = componentDevices.get(cable.key())
if(showGraphs && device){
let icon = GetComponentIcon(cable.type(), cable.subType(), themeType)
return(
<Grid2 key={cable.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
icon={icon}
title={cable.name()}
tag={"Cable Averages"}
device={device}
component={components.get(cable.key())!}
startDate={startDate}
endDate={endDate}
allNodes={!filterNodes}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(cable.type());
setSelectedComponentKey(cable.key());
setSelectedComponentTopNode(cable.settings.grainFilledTo);
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return(
<Grid2 key={cable.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{cableCard(cable)}
</Grid2>
)
})}
</Grid2>
</Box>
}
{controllerCount > 0 &&
<Box>
<Typography sx={{fontWeight: 650, fontSize: 25}}>Controllers</Typography>
<Grid2 container spacing={2}>
{heaters.map(heater => {
let device = componentDevices.get(heater.key())
if(showGraphs && device){
let icon = GetComponentIcon(heater.type(), heater.subType(), themeType)
return(
<Grid2 key={heater.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
device={device}
icon={icon}
title={heater.name()}
tag={"Heater"}
component={components.get(heater.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(heater.type());
setSelectedComponentKey(heater.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return (
<Grid2 key={heater.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{controllerCard(heater)}
</Grid2>
)
})}
{fans.map(fan => {
let device = componentDevices.get(fan.key())
if(showGraphs && device){
let icon = GetComponentIcon(fan.type(), fan.subType(), themeType)
return(
<Grid2 key={fan.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
<BinSensorGraph
device={device}
icon={icon}
title={fan.name()}
tag={"Fan"}
component={components.get(fan.key())!}
startDate={startDate}
endDate={endDate}
onMenuClick={(event) => {
setComponentUnnassigned(false);
setSelectedComponentType(fan.type());
setSelectedComponentKey(fan.key());
setAnchorEl(event.currentTarget);
}}
/>
</Grid2>
)
}
return (
<Grid2 key={fan.key()} size={{xs: 12, sm: 6, md: 4, lg: 3}}>
{controllerCard(fan)}
</Grid2>
)
})}
</Grid2>
</Box>
}
{unassignedComponents.length > 0 &&
<Box>
<Typography sx={{ fontWeight: 650, fontSize: 25 }}>Unassigned Components</Typography>
<Grid2 container spacing={2}>
{unassignedComponents.map(comp => (
<Grid2 key={comp.key()} size={{ xs: 6, sm: 6, md: 4, lg: 3 }}>
{unassignedComponentCard(comp)}
</Grid2>
))}
</Grid2>
</Box>
}
</Box>
)
}