import { Box, Card, darken, Grid2 as Grid, IconButton, Theme, Typography, } from "@mui/material"; import HumidityIcon from "component/HumidityIcon"; import TemperatureIcon from "component/TemperatureIcon"; import GrainDescriber from "grain/GrainDescriber"; import { cloneDeep } from "lodash"; import { Bin } from "models"; import { GrainCable } from "models/GrainCable"; import moment from "moment"; import { pond } from "protobuf-ts/pond"; import { useGlobalState } from "providers"; import React, { useEffect, useState } from "react"; import { celsiusToFahrenheit, getGrainUnit, or } from "utils"; //import { useHistory } from "react-router"; //import BinModeDot from "./BinModeDot"; import BinSVGV2 from "./BinSVGV2"; import { avg } from "utils"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { quack } from "protobuf-ts/quack"; import AerationFanIcon from "products/AgIcons/AerationFanIcon"; import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon"; import { CheckCircle, Warning } from "@mui/icons-material"; import { makeStyles } from "@mui/styles"; const useStyles = makeStyles((theme: Theme) => { return ({ displayBox: { background: darken(theme.palette.background.default, 0.05), borderRadius: 5, marginRight: -30, //negative margin to extend the box behind the bin svg paddingRight: 30, //padding to offset the text and ignore the extension marginTop: 5, cursor: "pointer", } }); }); interface Props { bin: Bin; duplicateBin: (bin: Bin) => void; dupHovered: (hovered: boolean) => void; valDisplay?: "high" | "low" | "average"; } interface GrainDetails { temp: number; humid: number; emc: number | undefined; } export default function BinCard(props: Props) { const { bin, valDisplay } = props; const [cables, setCables] = useState(); //const [modeDetails, setModeDetails] = useState(""); const [lidarPercentage, setLidarPercentage] = useState(); //const [lidarBushels, setLidarBushels] = useState(); const [{ user }] = useGlobalState(); const classes = useStyles(); const [average, setAverage] = useState(); const [coldNode, setColdNode] = useState(); const [hotNode, setHotNode] = useState(); const [mostMissed, setMostMissed] = useState(0); const warningThreshold = 6; const tempColour = describeMeasurement( quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE ).colour(); const humColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PERCENT).colour(); const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour(); useEffect(() => { let newGrainCables: GrainCable[] = []; if (bin.status.grainCables === undefined) return; let mostMissedReadings = 0; let allTemps: number[] = []; let allHums: number[] = []; let allMoistures: number[] = []; 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, let cableTemps = cloneDeep(cable.celcius); let cableHums = cloneDeep(cable.relativeHumidity); let cableEMC = cloneDeep(cable.moisture); c.temperatures = cableTemps.reverse(); c.humidities = cableHums.reverse(); c.grainMoistures = cableEMC.reverse(); c.topNode = cable.topNode; c.excludedNodes = cable.excludedNodes; newGrainCables.push(c); //filter out the excluded nodes and any nodes above the top node let filteredNodes = c.filteredNodes(true) allTemps.push(...filteredNodes.temps); if(avg(c.humidities) !== 0){ //if the average of the humidities is 0 that means that all the readings are 0 and it is a temp only cable allHums.push(...filteredNodes.humids); allMoistures.push(...filteredNodes.moistures); } //set the hottest and coldest nodes if (coldestNode === undefined || Math.min(...filteredNodes.temps) < coldestNode.temp) { let lowTempIndex = filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)); coldestNode = { temp: filteredNodes.temps[lowTempIndex], humid: filteredNodes.humids[lowTempIndex], emc: filteredNodes.moistures[lowTempIndex] }; } if (hottestNode === undefined || Math.max(...filteredNodes.temps) > hottestNode.temp) { let highTempIndex = filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps)); hottestNode = { temp: filteredNodes.temps[highTempIndex], humid: filteredNodes.humids[highTempIndex], emc: filteredNodes.moistures[highTempIndex] }; } //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); if (missedReadings > mostMissedReadings) { mostMissedReadings = missedReadings; } } }); //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); setHotNode(hottestNode); setAverage({ temp: avg(allTemps), humid: avg(allHums), emc: avg(allMoistures) }); let cm = 0; if (bin.status.distance && bin.status.distance > 0) { //note that no conversions are happening as the unit measurement model is not being used so the value will be in cm cm = bin.status.distance; let height = or(bin.settings.specs?.heightCm, 0); let ratio = 1 - cm / height; let capacity = or(bin.settings.specs?.bushelCapacity, 0); let lidarEstimate = Math.round(capacity * ratio); //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); // 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; if (bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN) { return ( {GrainDescriber(grainType).name} {bin.settings.inventory?.grainSubtype} ); } return ( {bin.settings.inventory?.customTypeName} ); }; const tempDisplay = () => { let display = "--"; let val; switch (valDisplay) { case "average": val = average?.temp; break; case "low": val = coldNode?.temp; break; case "high": val = hotNode?.temp; break; } if (val !== undefined && !isNaN(val)) { if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { val = celsiusToFahrenheit(val); } display = val.toFixed(2); } return ( {display + (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT ? "°F" : "°C")} ); }; const percentDisplay = () => { let display = "--"; let val; let useEMC = false; switch (valDisplay) { case "average": if (average?.emc) { useEMC = true; val = average.emc; } else { val = average?.humid; } break; case "low": if (coldNode?.emc) { useEMC = true; val = coldNode.emc; } else { val = coldNode?.humid; } break; case "high": if (hotNode?.emc) { useEMC = true; val = hotNode.emc; } else { val = hotNode?.humid; } break; } if (val !== undefined && !isNaN(val)) { display = val.toFixed(2); } return ( {display}% ); }; const cableBox = () => { return ( {tempDisplay()} {valDisplay === "high" ? "High" : valDisplay === "low" ? "Low" : "Average"} {percentDisplay()} {valDisplay === "high" ? "High" : valDisplay === "low" ? "Low" : "Average"} ); }; const fanBox = (controller: pond.BinFan) => { return ( {controller.state ? "ON" : "OFF"} ); }; const heaterBox = (controller: pond.BinHeater) => { return ( {/* */} {controller.state ? "ON" : "OFF"} ); }; const binGraphic = () => { return ( {typeDisplay()} {cableBox()} {bin.status.fans.map(controller => fanBox(controller))} {bin.status.heaters.map(controller => heaterBox(controller))} ); }; const inventoryDisplay = (current: number, capacity: number) => { if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) { return ( Math.round(current * 35.239).toLocaleString() + "/" + Math.round(capacity * 35.239).toLocaleString() + " L" ); } if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) { return ( bin.grainTonnes().toLocaleString() + " mT " + bin.fillPercent() + "%" ); } return ( current.toLocaleString() + "/" + capacity.toLocaleString() + " bu" ); }; const componentStateBanner = () => { const hasCables = bin.status.grainCables.length > 0; const hasCO2 = bin.status.co2.length > 0; return ( {hasCables || hasCO2 ? ( {mostMissed < 3 ? "Active" : mostMissed <= warningThreshold ? "Missing" : "Inactive"} {mostMissed < 3 ? ( ) : mostMissed <= warningThreshold ? ( ) : ( )} ) : ( Unmonitored )} ); }; const info = () => { const inv = bin.settings.inventory; let bushelAmount = bin.bushels(); let bushelCapacity = bin.settings.specs?.bushelCapacity; const empty = !inv || inv.empty || !bushelCapacity || bushelCapacity <= 0 || bushelAmount <= 0; return ( {empty || !bushelAmount || !bushelCapacity ? "empty" : inventoryDisplay(bushelAmount, bushelCapacity)} ); }; return ( {user.hasFeature("installer") && ( props.duplicateBin(bin)} onMouseEnter={() => props.dupHovered(true)} onMouseLeave={() => props.dupHovered(false)}> + )} {bin.name()} {binGraphic()} {info()} {componentStateBanner()} ); }