import { AccessTime, MoreVert } from "@mui/icons-material"; import { Avatar, Box, Button, Card, Checkbox, DialogActions, DialogContent, DialogTitle, 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"; import ResponsiveDialog from "common/ResponsiveDialog"; interface Props { bin: Bin components: Map; componentDevices: Map; preferences?: Map; setPreferences: React.Dispatch< React.SetStateAction | 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([]) const [plenums, setPlenums] = useState([]) const [ambient, setAmbient] = useState([]) const [pressures, setPressures] = useState([]) const [fans, setFans] = useState([]) const [heaters, setHeaters] = useState([]) //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([]) //the components in the headspace that measure temp/humidity const [headspaceCO2, setHeadspaceCO2] = useState([]) //the CO2 components in the headspace const [headspaceLidar, setHeadspaceLidar] = useState([]) //the lidar components in the headspace const [unassignedComponents, setUnassignedComponents] = useState([]) //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); //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(defaultDateRange.start); const [endDate, setEndDate] = useState(defaultDateRange.end); const [filterNodes, setFilterNodes] = useState(true) const [controllerConfirmation, setControllerConfirmation] = useState(false) const isMobile = useMobile() //state variables fo controller updates using the switch on the card const [currentController, setCurrentController] = useState() const [controllersNewMode, setControllersNewMode] = useState(quack.OutputMode.OUTPUT_MODE_OFF) //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( { selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_UNKNOWN); }}> Remove ); } if ( selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT || selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ) { options.push( { selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_AMBIENT); }}> Ambient ); options.push( { selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PLENUM); }}> Plenum ); } if ( selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT || selectedComponentType === quack.ComponentType.COMPONENT_TYPE_LIDAR || selectedComponentType === quack.ComponentType.COMPONENT_TYPE_CO2 ) { options.push( { selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_HEADSPACE); }}> Headspace ); } if ( selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE || selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE ) { options.push( { selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PRESSURE); }}> Plenum Pressure ); } if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) { options.push( { selectType( selectedComponentKey, pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE, selectedComponentTopNode ); }}> Grain Cable ); } if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT) { options.push( { 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 ); } if (options.length === 0) { return No Available Options; } else { return options; } }; const assignmentMenu = () => { return ( setAnchorEl(null)} keepMounted disableAutoFocusItem> {getOptions()} ); }; 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 ( { 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 ( { 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 ( { 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 ( { 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 ( { setComponentUnnassigned(false); setSelectedComponentType(sensor.type()); setSelectedComponentKey(sensor.key()); setAnchorEl(event.currentTarget); }} /> ) } const controllerDialog = () => { return ( {setControllerConfirmation(false)}}> Changing Controller This action will change the state of the controller ) } const updateController = () => { if(!currentController) return const prefType = preferences?.get(currentController.key())?.type //this will update the components settings here to change the mode currentController.settings.defaultOutputState = controllersNewMode let device = componentDevices.get(currentController.key()) if(device){ componentAPI.update(device, currentController.settings).then(resp => { currentController.mode = controllersNewMode if (prefType === pond.BinComponent.BIN_COMPONENT_FAN) { setFans(prev => prev.map(f => f.key() === currentController.key() ? currentController : f)) } else { setHeaters(prev => prev.map(h => h.key() === currentController.key() ? currentController : h)) } }).catch(err => { console.log("there was a problem updating the controller") }).finally(() => { setControllerConfirmation(false) })} } 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 ( {controller.name()} - {tag} ) => { setComponentUnnassigned(false) setSelectedComponentType(controller.type()) setSelectedComponentKey(controller.key()) setAnchorEl(event.currentTarget) }} > Current state {controller.on ? "Running" : isAuto ? "Standby" : "Off"} {[ { 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) => ( { setCurrentController(controller) setControllersNewMode(mode) setControllerConfirmation(true) }} sx={{ ...segStyle(active, label === "Off" ? theme.palette.action.selected : undefined), borderLeft: i > 0 ? `0.5px solid ${theme.palette.divider}` : "none", }} > {label} ))} {moment(controller.lastReading).fromNow()} ) } 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 ( {component.name()} ) => { setComponentUnnassigned(true) setSelectedComponentType(component.type()) setSelectedComponentKey(component.key()) setSelectedComponentSubtype(component.subType()) setSelectedComponentTopNode(0) setAnchorEl(event.currentTarget) }} > {lastReading ? moment(lastReading).fromNow() : "No readings yet"} ) } const updateDateRange = (newStartDate: any, newEndDate: any) => { let range = GetDefaultDateRange(); range.start = newStartDate; range.end = newEndDate; setStartDate(newStartDate); setEndDate(newEndDate); }; const graphControls = () => { return ( {showGraphs && !isMobile && } {cables.length > 0 && { setFilterNodes(!filterNodes) }} /> } label={Show All Nodes } /> } ) } return ( {controllerDialog()} {assignmentMenu()} {sensorCount > 0 && Sensors {graphControls()} {showGraphs && isMobile && } {plenums.map(plenum => { let device = componentDevices.get(plenum.key()) if(showGraphs && device){ let icon = GetComponentIcon(plenum.type(), plenum.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(plenum.type()); setSelectedComponentKey(plenum.key()); setAnchorEl(event.currentTarget); }} /> ) } return( {tempHumidCard(plenum)} ) } )} {pressures.map(pressure => { let device = componentDevices.get(pressure.key()) if(showGraphs && device){ let icon = GetComponentIcon(pressure.type(), pressure.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(pressure.type()); setSelectedComponentKey(pressure.key()); setAnchorEl(event.currentTarget); }} /> ) } return( {pressureCard(pressure)} ) })} {ambient.map(ambient => { let device = componentDevices.get(ambient.key()) if(showGraphs && device){ let icon = GetComponentIcon(ambient.type(), ambient.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(ambient.type()); setSelectedComponentKey(ambient.key()); setAnchorEl(event.currentTarget); }} /> ) } return( {tempHumidCard(ambient)} ) } )} {headspaceTH.map(h => { let device = componentDevices.get(h.key()) if(showGraphs && device){ let icon = GetComponentIcon(h.type(), h.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(h.type()); setSelectedComponentKey(h.key()); setAnchorEl(event.currentTarget); }} /> ) } return( {tempHumidCard(h)} ) })} {headspaceCO2.map(co2 => { let device = componentDevices.get(co2.key()) if(showGraphs && device){ let icon = GetComponentIcon(co2.type(), co2.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(co2.type()); setSelectedComponentKey(co2.key()); setAnchorEl(event.currentTarget); }} /> ) } return( {co2Card(co2)} ) })} {headspaceLidar.map(lidar => { let device = componentDevices.get(lidar.key()) if(showGraphs && device){ let icon = GetComponentIcon(lidar.type(), lidar.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(lidar.type()); setSelectedComponentKey(lidar.key()); setAnchorEl(event.currentTarget); }} /> ) } return ( {lidarCard(lidar)} ) })} {cables.map(cable => { let device = componentDevices.get(cable.key()) if(showGraphs && device){ let icon = GetComponentIcon(cable.type(), cable.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(cable.type()); setSelectedComponentKey(cable.key()); setSelectedComponentTopNode(cable.settings.grainFilledTo); setAnchorEl(event.currentTarget); }} /> ) } return( {cableCard(cable)} ) })} } {controllerCount > 0 && Controllers {heaters.map(heater => { let device = componentDevices.get(heater.key()) if(showGraphs && device){ let icon = GetComponentIcon(heater.type(), heater.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(heater.type()); setSelectedComponentKey(heater.key()); setAnchorEl(event.currentTarget); }} /> ) } return ( {controllerCard(heater)} ) })} {fans.map(fan => { let device = componentDevices.get(fan.key()) if(showGraphs && device){ let icon = GetComponentIcon(fan.type(), fan.subType(), themeType) return( { setComponentUnnassigned(false); setSelectedComponentType(fan.type()); setSelectedComponentKey(fan.key()); setAnchorEl(event.currentTarget); }} /> ) } return ( {controllerCard(fan)} ) })} } {unassignedComponents.length > 0 && Unassigned Components {unassignedComponents.map(comp => ( {unassignedComponentCard(comp)} ))} } ) }