From 67b251d865ab8a72d08063af138ebef4a4f82432 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 22 May 2026 13:12:54 -0600 Subject: [PATCH] added graphs to the bin details section on the new page and started working on the sensors --- package-lock.json | 2 +- src/bin/binSummary/BinSummary.tsx | 24 +- .../binSummary/components/bin3dVisualizer.tsx | 3 +- src/bin/binSummary/components/binAlerts.tsx | 126 +++--- .../components/binAnalysisGraphs.tsx | 410 ++++++++++++++++++ src/bin/binSummary/components/binControls.tsx | 186 ++++++++ src/bin/binSummary/components/binDetails.tsx | 39 +- src/bin/binSummary/components/binSensors.tsx | 156 +++++++ .../binSummary/components/binTableView.tsx | 2 +- src/bin/graphs/BinGraphsVPD.tsx | 1 + .../NewObjectInteraction.tsx | 3 - .../objectInteractions/ObjectInteractions.tsx | 7 +- src/pages/BinV2.tsx | 31 +- 13 files changed, 897 insertions(+), 93 deletions(-) create mode 100644 src/bin/binSummary/components/binAnalysisGraphs.tsx create mode 100644 src/bin/binSummary/components/binControls.tsx create mode 100644 src/bin/binSummary/components/binSensors.tsx diff --git a/package-lock.json b/package-lock.json index 9561e14..2454671 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/bin/binSummary/BinSummary.tsx b/src/bin/binSummary/BinSummary.tsx index 092db1a..fcc0672 100644 --- a/src/bin/binSummary/BinSummary.tsx +++ b/src/bin/binSummary/BinSummary.tsx @@ -12,21 +12,28 @@ import { Controller } from "models/Controller"; import { GrainCable } from "models/GrainCable"; import { pond } from "protobuf-ts/pond"; import BinDetails from "./components/binDetails"; +import { Plenum } from "models/Plenum"; +import BinSensors from "./components/binSensors"; interface Props { bin: Bin devices: Device[] cables?: GrainCable[] + plenums?: Plenum[] fans?: Controller[] heaters?: Controller[] permissions: pond.Permission[] componentDevices: Map componentMap: Map binPrefs?: Map + setPreferences: React.Dispatch< + React.SetStateAction | undefined> + >; updateBinCallback?: (bin: Bin) => void + } export default function BinSummary(props: Props){ - const {bin, devices, fans, heaters, permissions, componentDevices, componentMap, binPrefs, updateBinCallback} = props + const {bin, devices, fans, heaters, permissions, componentDevices, componentMap, binPrefs, updateBinCallback, cables = [], plenums = [], setPreferences} = props //const [currentDevice, setCurrentDevice] = useState(undefined) const isMobile = useMobile() @@ -38,11 +45,9 @@ export default function BinSummary(props: Props){ const activity = (reading: string) => { //if the device hasnt checked in in 6 hours = missing 12 hours = inactive within 6 hours = live - let status = "Live" let colour = "#4caf50" let diff = moment().diff(moment(reading), "hour") - console.log(diff) if(diff > 12){ status = "Inactive" colour = red[700] @@ -172,24 +177,21 @@ export default function BinSummary(props: Props){ - + + permissions={permissions} + cables={cables} + plenums={plenums}/> - {sensorData()} - - - - - {controllerStatus()} + diff --git a/src/bin/binSummary/components/bin3dVisualizer.tsx b/src/bin/binSummary/components/bin3dVisualizer.tsx index 67c4a67..996e1aa 100644 --- a/src/bin/binSummary/components/bin3dVisualizer.tsx +++ b/src/bin/binSummary/components/bin3dVisualizer.tsx @@ -86,6 +86,7 @@ export default function bin3dVisualizer(props: Props){ setOpenModeChange(true) }} > + Select Mode.. Storage Drying Hydrating @@ -161,6 +162,7 @@ export default function bin3dVisualizer(props: Props){ Controllers + {fans && fans.map(fan => { let isOn = controllerState(fan) return ( @@ -219,7 +221,6 @@ export default function bin3dVisualizer(props: Props){ )})} - // loop through controllers displaying each one diff --git a/src/bin/binSummary/components/binAlerts.tsx b/src/bin/binSummary/components/binAlerts.tsx index 1cd7fcf..f8c1bac 100644 --- a/src/bin/binSummary/components/binAlerts.tsx +++ b/src/bin/binSummary/components/binAlerts.tsx @@ -10,8 +10,6 @@ import React, { useCallback, useEffect, useState } from "react"; interface Props { linkedComponents: Map; //the component key to the component object componentDevices: Map; - bin: Bin - devices: Device[]; permissions: pond.Permission[] } @@ -45,68 +43,70 @@ export default function BinAlerts(props: Props){ }; const load = useCallback(()=>{ -//load the interactions for the linked components -setLoading(true) -const newInteractions: Interaction[] = []; -const interactionSourceMap = new Map(); -const sinkOptions: Component[] = []; -const run = async () => { - // Run all component API fetches in parallel - await Promise.all( - [...linkedComponents].map(async ([key, comp]) => { - const device = componentDevices.get(key); - - if (device) { - const resp = await interactionsAPI.listInteractionsByComponent( - device, - comp.location(), - undefined, - as - ); - - resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key)); - newInteractions.push(...resp); - } - - // Collect controller components - if (extension(comp.type()).isController) { - sinkOptions.push(comp); - } - }) - ); -}; - -run().then(() => { - newInteractions.forEach(interaction => { - //alert and control are not mutally exclusive, it is possible to be both if the control is sending notifications - if (interaction.isAlert()) { - // Find matching alert - let similarAlertIndex = alerts.findIndex(alert => - matchConditions(interaction, alert) - ); - - const compKey = interactionSourceMap.get(interaction.key()); - const component = compKey ? linkedComponents.get(compKey) : undefined; - - if (component) { - if (similarAlertIndex === -1) { - alerts.push({ - conditions: interaction.settings.conditions, - sourceComponents: [component], - }); - } else { - alerts[similarAlertIndex].sourceComponents.push(component); + if(!loading){ + //load the interactions for the linked components + setLoading(true) + const newInteractions: Interaction[] = []; + const interactionSourceMap = new Map(); + const sinkOptions: Component[] = []; + const run = async () => { + // Run all component API fetches in parallel + await Promise.all( + [...linkedComponents].map(async ([key, comp]) => { + const device = componentDevices.get(key); + + if (device) { + const resp = await interactionsAPI.listInteractionsByComponent( + device, + comp.location(), + undefined, + as + ); + + resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key)); + newInteractions.push(...resp); + } + + // Collect controller components + if (extension(comp.type()).isController) { + sinkOptions.push(comp); + } + }) + ); + }; + + run().then(() => { + newInteractions.forEach(interaction => { + //alert and control are not mutally exclusive, it is possible to be both if the control is sending notifications + if (interaction.isAlert()) { + // Find matching alert + let similarAlertIndex = alerts.findIndex(alert => + matchConditions(interaction, alert) + ); + + const compKey = interactionSourceMap.get(interaction.key()); + const component = compKey ? linkedComponents.get(compKey) : undefined; + + if (component) { + if (similarAlertIndex === -1) { + alerts.push({ + conditions: interaction.settings.conditions, + sourceComponents: [component], + }); + } else { + alerts[similarAlertIndex].sourceComponents.push(component); + } + } + + } + }); + setAlerts(alerts); + }).catch(err => { + console.error("Interaction fetch error:", err) + }).finally(() => { + setLoading(false) + }) } - } - - } - }); - setAlerts(alerts); - }).catch(err => { - console.error("Interaction fetch error:", err) - }).finally(() => { - setLoading(false) - }) },[linkedComponents, interactionsAPI, componentDevices, as]) useEffect(()=>{ diff --git a/src/bin/binSummary/components/binAnalysisGraphs.tsx b/src/bin/binSummary/components/binAnalysisGraphs.tsx new file mode 100644 index 0000000..cfb4a12 --- /dev/null +++ b/src/bin/binSummary/components/binAnalysisGraphs.tsx @@ -0,0 +1,410 @@ +import { Avatar, Box, Button, CardHeader, Checkbox, FormControlLabel, Grid2, Theme, Tooltip, Typography } from "@mui/material"; +import { useMobile, useThemeType } from "hooks"; +import VPDDark from "assets/products/Ag/dryingDark.png"; +import VPDLight from "assets/products/Ag/dryingLight.png"; +import TrendLight from "assets/products/Ag/trendingLight.png"; +import TrendDark from "assets/products/Ag/trendingDark.png"; +import { useEffect, useState } from "react"; +import { makeStyles } from "@mui/styles"; +import moment, { Moment } from "moment"; +import { blue, orange, teal } from "@mui/material/colors"; +import { GrainDryingPoint } from "charts/GrainDryingChart"; +import { GrainCable } from "models/GrainCable"; +import { Plenum } from "models/Plenum"; +import BinGraphsVPD from "bin/graphs/BinGraphsVPD"; +import { UnitMeasurement } from "models/UnitMeasurement"; +import { Bin } from "models"; +import { useBinAPI, useGlobalState } from "providers"; +import TimeBar from "common/time/TimeBar"; +import { GetDefaultDateRange } from "common/time/DateRange"; +import { ZoomOut } from "@mui/icons-material"; +import BinGraphsTrending from "bin/graphs/BinGraphsTrending"; +import { DataPoint, TrendPoint } from "charts/TrendingChart"; +import WaterLight from "assets/products/Ag/waterContentLight.png"; +import WaterDark from "assets/products/Ag/waterContentDark.png"; +import { Pressure } from "models/Pressure"; +import { DataPoint as WaterPoint } from "charts/WaterLevelChart"; +import BinWaterLevel from "bin/graphs/BinWaterLevel"; +import { Controller } from "models/Controller"; +import { pond } from "protobuf-ts/pond"; + +interface Props { + cables: GrainCable[] + plenums: Plenum[] + componentDevices: Map + bin: Bin +} + +const useStyles = makeStyles((theme: Theme) =>{ + return ({ + root: { + display: "flex", + flexDirection: "column", + height: "100%", + overflow: "hidden", + }, + toolbar: { + flexShrink: 0, + }, + scrollContent: { + flex: 1, + minHeight: 0, + overflowY: "auto", + }, + card: { + position: "relative", + display: "flex", + height: "100%", + flexDirection: "column", + overflow: "visible" + }, + cardHeader: { + padding: theme.spacing(1), + paddingLeft: theme.spacing(2), + marginRight: 10 + }, + avatarIcon: { + width: 33, + height: 33 + } + }) + }); + +export default function BinAnalysisGraphs(props: Props){ + const {cables, plenums, componentDevices, bin} = props + const themeType = useThemeType() + const classes = useStyles() + const [{user, as, showErrors}, dispatch] = useGlobalState() + const isMobile = useMobile() + const [xDomain, setXDomain] = useState(["dataMin", "dataMax"]); + const [zoomed, setZoomed] = useState(false); + const [recentVPD, setRecentVPD] = useState(); + const [loading, setLoading] = useState(false) + // map using the component key and the unitmeasurements for that component + const [compMeasurements, setCompMeasurements] = useState>( + new Map() + ); + const binAPI = useBinAPI() + const defaultDateRange = GetDefaultDateRange() + const [startDate, setStartDate] = useState(defaultDateRange.start); + const [endDate, setEndDate] = useState(defaultDateRange.end); + const [recentPlenumTrend, setRecentPlenumTrend] = useState(); + const [recentCableTrend, setRecentCableTrend] = useState(); + const [recentWaterContent, setRecentWaterContent] = useState(); + + useEffect(() => { + let measurementMap: Map = new Map(); + if (!loading) { + setLoading(true); + //check if the bin has a fan to use the cfm in the drying graph + // if (bin.settings.fan?.type !== pond.FanType.FAN_TYPE_UNKNOWN) { + // setIncludeCFM(true); + // } + binAPI + .listBinComponentsMeasurements( + bin.key(), + startDate.toISOString(), + endDate.toISOString(), + showErrors, + showErrors, + as + ) + .then(resp => { + resp.data.measurements.forEach((um: any) => { + let unitMeasurement = UnitMeasurement.any(um, user); + let entry = measurementMap.get(unitMeasurement.componentId); + if (entry) { + entry.push(unitMeasurement); + } else { + measurementMap.set(unitMeasurement.componentId, [unitMeasurement]); + } + }); + setCompMeasurements(measurementMap); + setLoading(false); + }); + } + }, [bin, binAPI, startDate, endDate, user, as]); // eslint-disable-line react-hooks/exhaustive-deps + + const zoomOut = () => { + setXDomain(["dataMin", "dataMax"]); + setZoomed(false); + }; + + const zoomIn = (domain: string[] | number[]) => { + if (!isMobile) { + setXDomain(domain); + setZoomed(true); + } + }; + + const updateDateRange = (newStartDate: any, newEndDate: any) => { + let range = GetDefaultDateRange(); + range.start = newStartDate; + range.end = newEndDate; + setStartDate(newStartDate); + setEndDate(newEndDate); + }; + + //this uses the components found in the bins status + const waterGraph = () => { + let moistureCables: pond.GrainCable[] = []; + bin.status.grainCables.forEach(cable => { + if (cable.relativeHumidity.length > 0) { + moistureCables.push(cable); + } + }); + if (bin.status.plenums[0] && moistureCables[0] && bin.status.pressures[0] && bin.supportedGrain()) { + return ( + + 0 + ? componentDevices.get(bin.status.fans[0].key) + ":" + bin.status.fans[0].key + : undefined + } + newXDomain={xDomain} + multiGraphZoom={zoomIn} + multiGraphZoomOut + returnLast={lastWater => { + setRecentWaterContent(lastWater); + }} + /> + + ); + } else { + return ( + + + Plenum with pressure and moisture cable as well as an initial moisture and supported + grain type set in the bin settings required to calculate Water Content + + + ); + } + }; + + return ( + + + + + + + {zoomed && ( + + + + )} + { + //setShowErrors(!showErrors); + dispatch({ key: "showErrors", value: !showErrors }); + }} + /> + } + /> + + + + + } + title={ + + + + Drying vs. Hydrating + + + + } + subheader={ + recentVPD ? ( + + + + 0 ? orange[500] : blue[500] }}> + {recentVPD.dryScore > 0 ? "drying" : "hydrating"} + + {" : "} + 0 ? orange[500] : blue[500], + fontWeight: 500 + }}> + {recentVPD.dryScore.toFixed(2)} + +
+
+
+ + + {moment(recentVPD.timestamp).fromNow()} + + +
+ ) : ( + + No Data + + ) + } + /> + + {cables.length > 0 && plenums.length > 0 ? ( + { + setRecentVPD(lastVPD); + }} + /> + ) : ( + + + Plenum and moisture cable required to calculate VPD + + + )} + + + } + title={ + Moisture Trending + } + subheader={ + recentPlenumTrend && recentCableTrend ? ( + + + + {"Plenum Moisture: "} + + {recentPlenumTrend.trend.toFixed(2)} + +
+
+ + {"Grain Moisture: "} + + {recentCableTrend.moisture.toString()} + +
+
+
+ + + {recentPlenumTrend.timestamp > recentCableTrend.timestamp + ? moment(recentPlenumTrend.timestamp).fromNow() + : moment(recentCableTrend.timestamp).fromNow()} + + +
+ ) : ( + + No Data + + ) + } + /> + + {cables.length > 0 && plenums.length > 0 ? ( + { + setRecentCableTrend(lastData); + setRecentPlenumTrend(lastTrend); + }} + /> + ) : ( + + + Plenum and moisture cable required to calculate trending data + + + )} + + + } + title={ + Grain Water Content + } + subheader={ + recentWaterContent && + bin.settings.inventory && + bin.settings.inventory.initialMoisture > 0 ? ( + + + + {"Water Content: "} + + {recentWaterContent.liters && !isNaN(recentWaterContent.liters) + ? recentWaterContent.liters.toFixed(2) + : ""} + +
+
+
+ + + {moment(recentWaterContent.timestamp).fromNow()} + + +
+ ) : ( + + No Data + + ) + } + /> + + {waterGraph()} + +
+
+ ) +} \ No newline at end of file diff --git a/src/bin/binSummary/components/binControls.tsx b/src/bin/binSummary/components/binControls.tsx new file mode 100644 index 0000000..5d400c2 --- /dev/null +++ b/src/bin/binSummary/components/binControls.tsx @@ -0,0 +1,186 @@ +import { LinearProgress } from "@mui/material"; +import { Bin, Component, Device, Interaction } from "models"; +import Controls, { Control } from "objects/objectInteractions/Controls"; +import NewObjectInteraction from "objects/objectInteractions/NewObjectInteraction"; +import { sameComponentID } from "pbHelpers/Component"; +import { extension } from "pbHelpers/ComponentType"; +import { pond } from "protobuf-ts/pond"; +import { quack } from "protobuf-ts/quack"; +import { useGlobalState } from "providers"; +import interactionsAPI, { useInteractionsAPI } from "providers/pond/interactionsAPI"; +import React, { useCallback, useEffect, useState } from "react"; + +interface Props { + linkedComponents: Map; //the component key to the component object + componentDevices: Map; + devices: Device[]; + permissions: pond.Permission[] +} + +export default function BinControls(props: Props) { + const {linkedComponents, componentDevices, devices, permissions} = props + const [{as}] = useGlobalState() + const interactionsAPI = useInteractionsAPI() + const [controls, setControls] = useState([]); + const [selectedDevice, setSelectedDevice] = useState(undefined) + const [openNewInteraction, setOpenNewInteraction] = useState(false) + const [loading, setLoading] = useState(false) + + + const findDevice = (deviceID: number) => { + for (let device of devices) { + if (device.id() === deviceID) return device + } + } + const findSinkComponent = (sinks: Component[], interactionSink: quack.ComponentID, sourceDevice: number) => { + for (let component of sinks) { + if (sameComponentID(component.location(), interactionSink) && sourceDevice === componentDevices.get(component.key())){ + return component + } + } + } + + const matchConditions = (interaction: Interaction, set: Control) => { + //if the number of conditions are not the same they are different + if (interaction.settings.conditions.length !== set.conditions.length) { + return false; + } + //continue with the comparison + let matching = true; + interaction.settings.conditions.forEach((condition, i) => { + if ( + condition.comparison !== set.conditions[i].comparison || + condition.measurementType !== set.conditions[i].measurementType || + condition.value !== set.conditions[i].value + ) { + matching = false; + } + }); + return matching; + }; + + const matchResult = (interaction: Interaction, set: Control) => { + let interactionResult = interaction.settings.result + let setResult = set.result + let matching = true + // if anything in the result for the interaction is different from the set make matching false + if(interactionResult?.type !== setResult?.type || + interactionResult?.mode !== setResult?.mode || + interactionResult?.value !== setResult?.value || + interactionResult?.dutyCycle !== setResult?.dutyCycle + ){ + matching = false + } + return matching + } + const load = useCallback(()=>{ + if(!loading){ + setLoading(true) + const newInteractions: Interaction[] = []; + const interactionSourceMap = new Map(); + const sinkOptions: Component[] = []; + const controls: Control[] = []; + + const run = async () => { + // Run all component API fetches in parallel + await Promise.all( + [...linkedComponents].map(async ([key, comp]) => { + const device = componentDevices.get(key); + + if (device) { + const resp = await interactionsAPI.listInteractionsByComponent( + device, + comp.location(), + undefined, + as + ); + + resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key)); + newInteractions.push(...resp); + } + + // Collect controller components + if (extension(comp.type()).isController) { + sinkOptions.push(comp); + } + }) + ); + }; + + run().then(() => { + newInteractions.forEach(interaction => { + if (interaction.isControl()) { + if (!interaction.settings.sink) return //if there is no sink it cant be a control so move on to the next + const compKey = interactionSourceMap.get(interaction.key()); + if (!compKey) return //we have to have a valid component key + const component = linkedComponents.get(compKey); + const deviceID = componentDevices.get(compKey); + if (!deviceID) return //we have to know which device it is for + const controlDevice = findDevice(deviceID) + if (!controlDevice) return //failed to get the device from the array + const sinkComponent = findSinkComponent(sinkOptions, interaction.settings.sink, deviceID) + if (!sinkComponent) return //failed to get the correct sink component + + let similarControlIndex = controls.findIndex(control => + matchConditions(interaction, control) && + matchResult(interaction, control) && + sameComponentID(interaction.settings.sink, control.sinkComponent.location()) && //if the sink for the interaction is the same address as the existing control + deviceID === control.device.id() //if the device id for source component is the same as the device id in the control + ); + + if (component && interaction.settings.result) { + if (similarControlIndex === -1) { + controls.push({ + conditions: interaction.settings.conditions, + result: interaction.settings.result, + sourceComponents: [component], + sinkComponent: sinkComponent, + device: controlDevice + }); + } else { + controls[similarControlIndex].sourceComponents.push(component); + } + } + } + }); + setControls(controls); + }).catch(err => { + console.error("Interaction fetch error:", err) + }) + } + },[linkedComponents, interactionsAPI, componentDevices, as]) + + useEffect(() => { + load() + }, [load]); + + return ( + + { + setOpenNewInteraction(false) + if(refresh){ + load() + } + }} + linkedComponents={linkedComponents} + componentDevices={componentDevices} + device={selectedDevice}/> + {loading ? + + : + { + console.log("new control") + setSelectedDevice(device) + setOpenNewInteraction(true) + }} + /> + } + + ) +} \ No newline at end of file diff --git a/src/bin/binSummary/components/binDetails.tsx b/src/bin/binSummary/components/binDetails.tsx index 519d2ce..aa21d84 100644 --- a/src/bin/binSummary/components/binDetails.tsx +++ b/src/bin/binSummary/components/binDetails.tsx @@ -6,6 +6,11 @@ import { Bin, Component, Device } from "models"; import Alerts from "objects/objectInteractions/Alerts"; import BinAlerts from "./binAlerts"; import { pond } from "protobuf-ts/pond"; +import BinControls from "./binControls"; +import BinAnalysisGraphs from "./binAnalysisGraphs"; +import { GrainCable } from "models/GrainCable"; +import { Plenum } from "models/Plenum"; +import { Controller } from "models/Controller"; interface Props { bin: Bin @@ -14,6 +19,8 @@ interface Props { componentDevices: Map; devices: Device[]; permissions: pond.Permission[] + cables: GrainCable[] + plenums: Plenum[] } interface TabPanelProps { @@ -26,47 +33,57 @@ function TabPanelMine(props: TabPanelProps) { const { children, value, index, ...other } = props; return ( - + {value === index && children} + ); } export default function BinDetails (props: Props) { - const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions} = props + const {bin, updateBinCallback, linkedComponents, componentDevices, devices, permissions, cables, plenums} = props const [currentTab, setCurrentTab] = useState(0) - //this is where we will have the tabs abd tab panels for the table view, alerts, interactions, and analysis + //this is where we will have the tabs and tab panels for the table view, alerts, controls, and analysis return ( - + setCurrentTab(value)} indicatorColor="primary" textColor="primary" - variant="fullWidth"> + variant="fullWidth" + sx={{ flexShrink: 0 }}> + - + - + - + - + + ) } \ No newline at end of file diff --git a/src/bin/binSummary/components/binSensors.tsx b/src/bin/binSummary/components/binSensors.tsx new file mode 100644 index 0000000..602f622 --- /dev/null +++ b/src/bin/binSummary/components/binSensors.tsx @@ -0,0 +1,156 @@ +import { Box, Grid2, Typography } from "@mui/material"; +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 { pond } from "protobuf-ts/pond"; +import { quack } from "protobuf-ts/quack"; +import React, { useEffect, useState } from "react"; + +interface Props { + bin: Bin + components: Map; + preferences?: Map; + setPreferences: React.Dispatch< + React.SetStateAction | undefined> + >; +} + +export default function BinSensors(props: Props){ + const {bin, components, preferences, setPreferences} = props + 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) + + + //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) + setCables(cables) + setPlenums(plenums) + setPressures(pressures) + setAmbient(ambients) + setFans(fans) + setHeaters(heaters) + setHeadspaceTH(headspaceTH) + setHeadspaceCO2(headspaceCO2) + setHeadspaceLidar(headspaceLidar) + setUnassignedComponents(unassigned) + },[components, preferences]) + + const cableDisplay = (cable: GrainCable) => { + return ( + + + + ) + } + + return ( + + {sensorCount > 0 && + + Sensors + {cables.length > 0 && + + Cables + + {cables.map(cable => { + return( + + {cableDisplay(cable)} + + ) + })} + + + } + {plenums.length } + + } + + Controllers + {/* do all of the fans and heaters here */} + + Unassigned Components + {/* list the components that have no assignment on the bin */} + + ) +} \ No newline at end of file diff --git a/src/bin/binSummary/components/binTableView.tsx b/src/bin/binSummary/components/binTableView.tsx index 72d49c3..c5ebc5a 100644 --- a/src/bin/binSummary/components/binTableView.tsx +++ b/src/bin/binSummary/components/binTableView.tsx @@ -335,7 +335,7 @@ export default function BinTableView(props: Props) { {binLevels.map((level, i) => ( - + {binLevels.length - i} {levelDisplay(level)} diff --git a/src/bin/graphs/BinGraphsVPD.tsx b/src/bin/graphs/BinGraphsVPD.tsx index 037bf2d..f4fe68c 100644 --- a/src/bin/graphs/BinGraphsVPD.tsx +++ b/src/bin/graphs/BinGraphsVPD.tsx @@ -4,6 +4,7 @@ import { GrainCable } from "models/GrainCable"; import { Plenum } from "models/Plenum"; import { UnitMeasurement } from "models/UnitMeasurement"; import moment from "moment"; +import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; import React, { useEffect, useState } from "react"; import { avg } from "utils"; diff --git a/src/objects/objectInteractions/NewObjectInteraction.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx index c28cf91..9fb8887 100644 --- a/src/objects/objectInteractions/NewObjectInteraction.tsx +++ b/src/objects/objectInteractions/NewObjectInteraction.tsx @@ -213,15 +213,12 @@ export default function NewObjectInteraction(props: Props){ //display the components that match the type that was selected for the new alert const listMatchingComponents = () => { - console.log("listing") let matching: Component[] = []; - console.log(validComponents) validComponents.forEach(comp => { if (comp.type() === newAlertComponentType) { matching.push(comp); } }); - console.log(matching) return ( {matching.map(comp => ( diff --git a/src/objects/objectInteractions/ObjectInteractions.tsx b/src/objects/objectInteractions/ObjectInteractions.tsx index d50adb9..da9ba77 100644 --- a/src/objects/objectInteractions/ObjectInteractions.tsx +++ b/src/objects/objectInteractions/ObjectInteractions.tsx @@ -23,6 +23,11 @@ interface Props { permissions: pond.Permission[] } +/** + * combines the alerts and the controls together to display them simultaneously, if you just want one or the other just use the specific component you want + * @param props + * @returns + */ export default function ObjectInteractions(props: Props) { const { linkedComponents, componentDevices, devices, objectKey, objectType, permissions } = props const [{as}] = useGlobalState() @@ -86,7 +91,7 @@ export default function ObjectInteractions(props: Props) { } const load = useCallback(()=>{ -const newInteractions: Interaction[] = []; + const newInteractions: Interaction[] = []; const interactionSourceMap = new Map(); const sinkOptions: Component[] = []; const alerts: Alert[] = []; diff --git a/src/pages/BinV2.tsx b/src/pages/BinV2.tsx index 101352e..22abf2b 100644 --- a/src/pages/BinV2.tsx +++ b/src/pages/BinV2.tsx @@ -14,7 +14,7 @@ import { Pressure } from "models/Pressure"; import { CO2 } from "models/CO2"; import moment from "moment"; import { quack } from "protobuf-ts/quack"; -import { Box, Drawer, Grid2 as Grid, IconButton, Theme, Tooltip } from "@mui/material"; +import { Box, Drawer, Grid2 as Grid, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Theme, Tooltip } from "@mui/material"; import { makeStyles } from "@mui/styles"; import NotesIcon from "@mui/icons-material/Notes"; import TasksIcon from "products/Construction/TasksIcon"; @@ -43,6 +43,9 @@ const useStyles = makeStyles((theme: Theme) => { border: "1px solid", borderColor: theme.palette.divider }, + menuPaper: { + borderRadius: "20px" + }, }) }) @@ -298,6 +301,28 @@ export default function BinV2(){ * UI STUFF STARTS HERE */ + const deviceMenu = () => { + return ( + setAnchorEl(null)} + keepMounted + classes={{ paper: classes.menuPaper }} + disableAutoFocusItem> + {devices.map(dev => ( + goToDevice(dev)}> + + + + + + ))} + + ); + }; + /** * Header - consists of the actions and things that are always visible like * opening the note/history and tasks drawers, navigating to the device and map pages, opening binsensors and settings etc. @@ -433,16 +458,20 @@ export default function BinV2(){ return ( + {deviceMenu()} {pageHeader()} { setBin(bin) binAPI.updateBin(bin.key(), bin.settings).then(resp => {