diff --git a/package-lock.json b/package-lock.json index bb51eb6..aaa306a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,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#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#co2_bin_activity", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", "react-color": "^2.19.3", @@ -10864,7 +10864,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#8311cb2928f25950700fa0d3aaacf2ac07ea7f7e", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#09dcadae1f892d9960a7fc823f2735d3cccab8e3", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index af353b9..70f5054 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,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#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#co2_bin_activity", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", "react-color": "^2.19.3", diff --git a/src/bin/BinCardV2.tsx b/src/bin/BinCardV2.tsx index 58bff74..8f106ee 100644 --- a/src/bin/BinCardV2.tsx +++ b/src/bin/BinCardV2.tsx @@ -58,9 +58,9 @@ interface GrainDetails { export default function BinCard(props: Props) { const { bin, valDisplay } = props; const [cables, setCables] = useState(); - const [modeDetails, setModeDetails] = useState(""); + //const [modeDetails, setModeDetails] = useState(""); const [lidarPercentage, setLidarPercentage] = useState(); - const [lidarBushels, setLidarBushels] = useState(); + //const [lidarBushels, setLidarBushels] = useState(); const [{ user }] = useGlobalState(); const classes = useStyles(); const [average, setAverage] = useState(); @@ -84,6 +84,7 @@ export default function BinCard(props: Props) { let coldestNode: GrainDetails | undefined; let hottestNode: GrainDetails | undefined; let now = moment(); + //loop through the cables in the bin status to prep for display bin.status.grainCables.forEach(cable => { let c: GrainCable = new GrainCable(); //flip the cables so that for display node 1 is at the bottom @@ -137,8 +138,9 @@ export default function BinCard(props: Props) { }; } - let lastRead = moment(cable.lastRead); - let elapsedMS = now.diff(lastRead); + //check cables missed readings + let lastCableRead = moment(cable.lastRead); + let elapsedMS = now.diff(lastCableRead); if (elapsedMS > cable.measurementInterval) { let missedReadings = Math.floor(elapsedMS / cable.measurementInterval); @@ -147,6 +149,18 @@ export default function BinCard(props: Props) { } } }); + //loop through the CO2's to check for missed readings + bin.status.co2.forEach(co2 => { + let lastRead = moment(co2.lastRead); + let elapsedMS = now.diff(lastRead); + if (elapsedMS > co2.measurementInterval) { + let missedReadings = Math.floor(elapsedMS / co2.measurementInterval); + if (missedReadings > mostMissedReadings) { + mostMissedReadings = missedReadings; + } + } + }) + setCables(newGrainCables); setMostMissed(mostMissedReadings); setColdNode(coldestNode); @@ -165,28 +179,28 @@ export default function BinCard(props: Props) { let ratio = 1 - cm / height; let capacity = or(bin.settings.specs?.bushelCapacity, 0); let lidarEstimate = Math.round(capacity * ratio); - setLidarBushels(lidarEstimate); + //setLidarBushels(lidarEstimate); setLidarPercentage(Math.round((lidarEstimate / capacity) * 100)); } }, [bin]); - useEffect(() => { - let now = moment(); - let duration = moment.duration(moment(bin.status.lastModeChange).diff(now)); - let days = duration.asDays(); - days = Math.abs(days); - duration.subtract(moment.duration(days, "days")); - let hours = duration.hours(); - hours = Math.abs(hours); + // useEffect(() => { + // let now = moment(); + // let duration = moment.duration(moment(bin.status.lastModeChange).diff(now)); + // let days = duration.asDays(); + // days = Math.abs(days); + // duration.subtract(moment.duration(days, "days")); + // let hours = duration.hours(); + // hours = Math.abs(hours); - if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) { - setModeDetails("Calculating..."); - } else if (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) { - setModeDetails("No Mode"); - } else { - setModeDetails(Math.floor(days) + "d " + hours + "h"); - } - }, [bin]); + // if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) { + // setModeDetails("Calculating..."); + // } else if (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) { + // setModeDetails("No Mode"); + // } else { + // setModeDetails(Math.floor(days) + "d " + hours + "h"); + // } + // }, [bin]); const typeDisplay = () => { let grainType = bin.settings.inventory?.grainType ?? pond.Grain.GRAIN_NONE; @@ -440,9 +454,10 @@ export default function BinCard(props: Props) { const componentStateBanner = () => { const hasCables = bin.status.grainCables.length > 0; + const hasCO2 = bin.status.co2.length > 0; return ( - {hasCables ? ( + {hasCables || hasCO2 ? ( {mostMissed < 3 ? "Active" : mostMissed <= warningThreshold ? "Missing" : "Inactive"} diff --git a/src/bin/BinComponentTypes.tsx b/src/bin/BinComponentTypes.tsx index e5f64e3..01562e5 100644 --- a/src/bin/BinComponentTypes.tsx +++ b/src/bin/BinComponentTypes.tsx @@ -32,6 +32,7 @@ import { MoreVert } from "@mui/icons-material"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { Ambient } from "models/Ambient"; import { makeStyles } from "@mui/styles"; +import { CO2 } from "models/CO2"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -123,7 +124,7 @@ export default function BinComponentTypes(props: Props) { const [heaters, setHeaters] = useState([]); const [fans, setFans] = useState([]); const [lidars, setLidars] = useState([]); - const [headspaceCo2s, setHeadspaceCo2s] = useState([]); + const [headspaceCo2s, setHeadspaceCo2s] = useState([]); const [tempUnit, setTempUnit] = useState(); const [pressureUnit, setPressureUnit] = useState(); const [selectedComponentKey, setSelectedComponentKey] = useState(""); @@ -176,7 +177,7 @@ export default function BinComponentTypes(props: Props) { var lidar: Component[] = []; var unassigned: Component[] = []; var ambients: Ambient[] = []; - var headspaceCo2s: Component[] = []; + var headspaceCo2s: CO2[] = []; components.forEach(comp => { let pref = preferences.get(comp.key()); if (pref) { @@ -198,7 +199,8 @@ export default function BinComponentTypes(props: Props) { } else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) { lidar.push(comp); } else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) { - headspaceCo2s.push(comp); + let coComp = CO2.create(comp) + headspaceCo2s.push(coComp); } } } else { @@ -716,9 +718,8 @@ export default function BinComponentTypes(props: Props) { return distanceCM + "cm"; }; - const headspaceList = (sensors: Headspace[], lidars: Component[], headspaceCo2s: Component[]) => { + const headspaceList = (sensors: Headspace[], lidars: Component[], headspaceCo2s: CO2[]) => { if (sensors.length < 1 && lidars.length < 1 && headspaceCo2s.length < 1) return null; - let headspaceComponents = lidars.concat(headspaceCo2s); return ( @@ -778,7 +779,73 @@ export default function BinComponentTypes(props: Props) { ); })} - {headspaceComponents.map((component, index) => { + {headspaceCo2s.map((co2, index) => { + let cIcon = GetComponentIcon( + co2.settings.type, + co2.settings.subtype, + theme.palette.mode + ); + + let colour = "white"; //default colour if there are no measurements to get the type from + + let ppmString = "--"; + if ( + co2.status.measurement[0] && + co2.status.measurement[0].values[0] && + co2.status.measurement[0].values[0].values[0] + ) { + colour = describeMeasurement( + co2.status.measurement[0].type, + co2.type(), + co2.subType() + ).colour(); + ppmString = co2.status.measurement[0].values[0].values[0] + " ppm"; + } + return ( + + + + + {co2.name()} + + + + + +
+ + {ppmString} + +
+
+
+ + ) => { + setComponentUnnassigned(false); + setSelectedComponentType(co2.type()); + setSelectedComponentKey(co2.key()); + setAnchorEl(event.currentTarget); + }}> + + + +
+
+
+ ); + }) + } + {lidars.map((component, index) => { let cIcon = GetComponentIcon( component.settings.type, component.settings.subtype, diff --git a/src/bin/BinStorageConditions.tsx b/src/bin/BinStorageConditions.tsx index 3d857e3..5d5e682 100644 --- a/src/bin/BinStorageConditions.tsx +++ b/src/bin/BinStorageConditions.tsx @@ -27,6 +27,7 @@ import React, { useEffect, useState } from "react"; import { avg, getTemperatureUnit } from "utils"; import { makeStyles } from "@mui/styles"; import { Mark } from "@mui/material/Slider/useSlider.types"; +import { CO2 } from "models/CO2"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -117,7 +118,7 @@ const useStyles = makeStyles((theme: Theme) => { interface Props { bin: Bin; - headspaceCO2: Component[]; + headspaceCO2: CO2[]; cables: GrainCable[]; } diff --git a/src/models/CO2.ts b/src/models/CO2.ts new file mode 100644 index 0000000..62e5d73 --- /dev/null +++ b/src/models/CO2.ts @@ -0,0 +1,110 @@ +import { pond } from "protobuf-ts/pond"; +import { quack } from "protobuf-ts/pond"; +import { or } from "utils/types"; +import { clone, cloneDeep } from "lodash"; +import { Component } from "models"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { extractGrainCable } from "pbHelpers/ComponentTypes"; + +export class CO2 { + public settings: pond.ComponentSettings = pond.ComponentSettings.create(); + public status: pond.ComponentStatus = pond.ComponentStatus.create(); + public lastReading: string = ""; + public ppm: number = 0; + + public static create(comp: Component): CO2 { + let my = new CO2(); + my.settings = comp.settings; + my.status = comp.status; + + if (comp.status.lastGoodMeasurement.length > 0) { + comp.status.lastGoodMeasurement.forEach(um => { + if (um.timestamps[0]) { + my.lastReading = um.timestamps[0]; + } + if (um.values[0] && um.values[0].values.length > 0) { + if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_PPM) { + my.ppm = um.values[0].values[0]; + } + } + }); + } + return my; + } + + public static createPond(comp: pond.Component): CO2 { + let my = new CO2(); + my.settings = comp.settings ? comp.settings : pond.ComponentSettings.create(); + my.status = comp.status ? comp.status : pond.ComponentStatus.create(); + + if (comp.status && comp.status.lastGoodMeasurement.length > 0) { + comp.status.lastGoodMeasurement.forEach(um => { + if (um.timestamps[0]) { + my.lastReading = um.timestamps[0]; + } + if (um.values[0] && um.values[0].values.length > 0) { + if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_PPM) { + my.ppm = um.values[0].values[0]; + } + } + }); + } + return my; + } + + public static any(data: any): CO2 { + let comp = pond.Component.fromObject(cloneDeep(data)); + let my = CO2.createPond(comp); + if (data && data.status && data.status.lastMeasurement) { + my.status.lastMeasurement = data.status.lastMeasurement; + } + return my; + } + + public update(other: CO2) { + this.settings = other.settings; + this.status = other.status; + } + + public name(): string { + return this.settings.name !== "" ? this.settings.name : "Component " + this.key(); + } + + public key(): string { + return this.settings.key; + } + + public location(): quack.ComponentID { + return quack.ComponentID.fromObject({ + type: this.settings.type, + addressType: this.settings.addressType, + address: this.settings.address + }); + } + + public locationString(): string { + return ( + or(this.settings.type, 0).toString() + + "-" + + or(this.settings.addressType, 0).toString() + + "-" + + or(this.settings.address, 0).toString() + ); + } + + public type(): quack.ComponentType { + return this.settings.type; + } + + public subType(): number { + return this.settings.subtype; + } + + //return the CO2 as a component + public asComponent(): Component { + let component = Component.create(); + component.settings = this.settings; + component.status = this.status; + return component; + } +} diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 7d71b07..fc9463f 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -68,6 +68,7 @@ import { Controller } from "models/Controller"; import TaskViewer from "tasks/TaskViewer"; import ButtonGroup from "common/ButtonGroup"; import BinTransactions from "bin/BinTransactions"; +import { CO2 } from "models/CO2"; interface TabPanelProps { children?: React.ReactNode; @@ -192,7 +193,7 @@ export default function Bin(props: Props) { const [ambients, setAmbients] = useState([]); const [grainCables, setGrainCables] = useState([]); const [pressures, setPressures] = useState([]); - const [headspaceCO2, setHeadspaceCO2] = useState([]); + const [headspaceCO2, setHeadspaceCO2] = useState([]); const [heaters, setHeaters] = useState([]); const [fans, setFans] = useState([]); const [detail, setDetail] = useState< @@ -210,63 +211,13 @@ export default function Bin(props: Props) { new Map() ); const [binPresets, setBinPresets] = useState([]); - const [missedCableReadings, setMissedCableReadings] = useState(0); + const [missedReadings, setMissedReadings] = useState(0); const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); }; - // const StyledToggle = styled(ToggleButton)(() => ({ - // root: { - // backgroundColor: "transparent", - // overflow: "visible", - // content: "content-box", - // "&$selected": { - // backgroundColor: "gold", - // color: "black", - // borderRadius: 24, - // fontWeight: "bold" - // }, - // "&$selected:hover": { - // backgroundColor: "rgb(255, 255, 0)", - // color: "black", - // borderRadius: 24 - // } - // }, - // selected: {} - // })) - - // const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({ - // grouped: { - // margin: theme.spacing(-0.5), - // border: "none", - // padding: theme.spacing(1), - // "&:not(:first-child):not(:last-child)": { - // borderRadius: 24, - // marginRight: theme.spacing(0.5), - // marginLeft: theme.spacing(0.5) - // }, - // "&:first-child": { - // borderRadius: 24, - // marginLeft: theme.spacing(0.25) - // }, - // "&:last-child": { - // borderRadius: 24, - // marginRight: theme.spacing(0.25) - // } - // }, - // root: { - // backgroundColor: darken( - // theme.palette.background.paper, - // getThemeType() === "light" ? 0.05 : 0.25 - // ), - // borderRadius: 24, - // content: "border-box" - // } - // })) - const load = useCallback(() => { - console.log(loadRef.current) if (loadRef.current || user.id() === "") return; setBinLoading(true); loadRef.current = true; @@ -365,9 +316,10 @@ export default function Bin(props: Props) { var fans: Controller[] = []; var heaters: Controller[] = []; var pressures: Pressure[] = []; - var headspaceCo2: Component[] = []; + var headspaceCo2: CO2[] = []; var mostMissed: number = 0; let now = moment(); + components.forEach(comp => { let pref = preferences.get(comp.key()); if (pref) { @@ -396,7 +348,17 @@ export default function Bin(props: Props) { pressures.push(Pressure.create(comp)); if (pref.type === pond.BinComponent.BIN_COMPONENT_HEADSPACE) { if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) { - headspaceCo2.push(comp); + //check if there are missed readings from the co2 and compare them to what we already had + let coComp = CO2.create(comp) + let lastRead = moment(coComp.lastReading); + let elapsedMS = now.diff(lastRead); + if (elapsedMS > coComp.settings.measurementPeriodMs) { + let missedReadings = Math.floor(elapsedMS / coComp.settings.measurementPeriodMs); + if (missedReadings > mostMissed) { + mostMissed = missedReadings; + } + } + headspaceCo2.push(coComp); } } } else { @@ -404,7 +366,7 @@ export default function Bin(props: Props) { } } }); - setMissedCableReadings(mostMissed); + setMissedReadings(mostMissed); setPlenums(plenums); setGrainCables(grainCables); setPressures(pressures); @@ -1098,9 +1060,37 @@ export default function Bin(props: Props) { }; const componentState = () => { + const hasCables = grainCables.length > 0 + const hasCO2 = headspaceCO2.length > 0 return ( - + + {missedReadings < 3 + ? "Active" + : missedReadings <= warningThreshold + ? "Missing" + : "Inactive"} + + {missedReadings < 3 ? ( + + ) : missedReadings <= warningThreshold ? ( + + ) : ( + + )} +
+ : + - {missedCableReadings < 3 - ? "Active" - : missedCableReadings <= warningThreshold - ? "Missing" - : "Inactive"} - - {missedCableReadings < 3 ? ( - - ) : missedCableReadings <= warningThreshold ? ( - - ) : ( - - )} + Unmonitored +
+ }
); };