frontend/src/bin/BinVisualizerV2.tsx
csawatzky 23223aa1a8 updated the api to use a new variable for whether to load all nodes, this will still use the show errors boolean for the component page
fixed issue in the filter function in the grain cable model that caused it to return empty when a top node was not set
updated the bin svg to have the disabled nodes greyed out
2025-07-31 13:20:31 -06:00

2169 lines
72 KiB
TypeScript

import {
Avatar,
Box,
Button,
Card,
darken,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Grid2 as Grid,
IconButton,
InputAdornment,
lighten,
Link,
Skeleton,
Slider,
Switch,
TextField,
Theme,
ToggleButton,
ToggleButtonGroup,
Typography,
useTheme,
} from "@mui/material";
import FullscreenIcon from "@mui/icons-material/Fullscreen";
import FullscreenExitIcon from "@mui/icons-material/FullscreenExit";
import RefreshIcon from "@mui/icons-material/Refresh";
import HumidityIcon from "component/HumidityIcon";
import TemperatureIcon from "component/TemperatureIcon";
import GrainDescriber, { GrainOptions, ToGrainOption } from "grain/GrainDescriber";
import useViewport from "hooks/useViewport";
import { cloneDeep, round } from "lodash";
import { Bin, Component, Device, Interaction } from "models";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { useMobile } from "hooks";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState, useSnackbar } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import { FullScreen, useFullScreenHandle } from "react-full-screen";
import moment, { Moment } from "moment";
import ResponsiveDialog from "common/ResponsiveDialog";
import { getThemeType } from "theme";
import { getGrainUnit, getTemperatureUnit, or } from "utils";
import { useBinAPI } from "providers/pond/binAPI";
import BindaptIcon from "products/Bindapt/BindaptIcon";
import {
AccessTime,
CheckCircleOutline,
Error,
InfoOutlined,
TrendingDown,
TrendingFlat,
TrendingUp,
Warning
} from "@mui/icons-material";
import DevicePresetsFromPicker from "device/DevicePresetsFromPicker";
import { Plenum } from "models/Plenum";
import { Pressure } from "models/Pressure";
import { GrainCable } from "models/GrainCable";
import GrainNodeInteractions from "./GrainNodeInteractions";
import GrainTransaction from "grain/GrainTransaction";
import { DevicePreset } from "models/DevicePreset";
import BinSVGV2 from "./BinSVGV2";
import AerationFanIcon from "products/AgIcons/AerationFanIcon";
import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon";
import { Ambient } from "models/Ambient";
import { ExtractMoisture } from "grain";
import SearchSelect, { Option } from "common/SearchSelect";
import Edit from "@mui/icons-material/Edit";
import { makeStyles, styled } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
const useStyles = makeStyles((theme: Theme) => {
return ({
cardContent: {
padding: theme.spacing(1),
paddingRight: theme.spacing(0)
},
bgItem: {
background: darken(theme.palette.background.paper, getThemeType() === "light" ? 0.05 : 0.25),
borderRadius: theme.shape.borderRadius,
width: "auto",
marginRight: theme.spacing(1),
padding: 5
},
displayBoxBinLeft: {
background: darken(theme.palette.background.default, 0.05),
borderRadius: 5,
marginRight: -70, //negative margin to extend the box behind the bin svg
paddingRight: 80, //padding to offset the text and ignore the extension
paddingTop: 5,
paddingBottom: 5,
paddingLeft: 5
},
displayBox: {
background: darken(theme.palette.background.default, 0.05),
borderRadius: 5,
padding: 5
},
controllerDisplay: {
display: "flex",
justifyContent: "space-between",
paddingLeft: 5,
paddingRight: 5
},
days: {
margin: theme.spacing(0.5),
fontSize: "16px"
},
avatarIcon: {
background: "transparent",
width: theme.spacing(3),
height: theme.spacing(3),
margin: "auto",
marginBottom: "auto"
},
selected: {
backgroundColor: "gold",
"&.selected": {
backgroundColor: "gold"
}
},
extraDetails: {
padding: 1,
marginTop: theme.spacing(1),
display: "flex",
width: "100%",
flexGrow: 1,
height: "100%"
},
grainOVerlay: {
position: "absolute",
height: "10%",
width: "100%",
textAlign: "center",
textShadow: "4px 4px 10px black"
},
grainDiff: {
position: "absolute",
top: "10%"
},
lightBox: {
background: lighten(theme.palette.background.default, 0.2),
borderRadius: 10,
padding: 10,
margin: 10
},
bottomSpacing: {
marginBottom: theme.spacing(1)
},
sliderValLabel: {
height: 30,
width: 30,
left: -5,
top: -20,
background: "transparent"
},
});
});
const average = (arr: any) => arr.reduce((p: any, c: any) => p + c, 0) / arr.length;
interface GrainConditions {
tempC: number;
humidity: number;
emc: number;
tempCTrend: number;
humidityTrend: number;
emcTrend: number;
}
interface Props {
bin: Bin;
loading: boolean;
components: Map<string, Component>;
componentDevices?: Map<string, number>;
devices: Device[];
plenum?: Plenum;
ambient?: Ambient;
cables?: GrainCable[];
pressure?: Pressure;
interactions?: Interaction[];
//changeBinMode: (binMode: pond.BinMode) => boolean;
refresh: (showSnack?: boolean) => void;
afterUpdate?(): void;
preferences?: Map<string, pond.BinComponentPreferences>;
permissions?: pond.Permission[];
updateComponentCallback: (componentKey: string) => void;
binPresets: DevicePreset[];
}
export default function BinVisualizer(props: Props) {
const {
bin,
//changeBinMode,
//changeGrainAmount,
plenum,
ambient,
pressure,
loading,
cables,
components,
devices,
preferences,
componentDevices,
permissions,
//interactions,
refresh,
updateComponentCallback,
binPresets
} = props;
const binAPI = useBinAPI();
const [{as}] = useGlobalState()
const isMobile = useMobile();
const classes = useStyles();
const theme = useTheme();
const fullScreenHandler = useFullScreenHandle();
const viewport = useViewport();
const { openSnack } = useSnackbar();
const [fillPercentage, setFillPercentage] = useState<number | null>(0);
const [lidarBushels, setLidarBushels] = useState<number | undefined>();
const [lidarPercentage, setLidarPercentage] = useState<number | undefined>();
const [pendingGrainAmount, setPendingGrainAmount] = useState<number | undefined>();
const tempColour = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
).colour();
const humidColour = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
quack.ComponentType.COMPONENT_TYPE_DHT
).colour();
const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour();
const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour();
const [showInputMoisture, setShowInputMoisture] = useState<boolean>(false);
const [moistureInput, setMoistureInput] = useState<string>("");
const [tempInput, setTempInput] = useState<string>("");
const [outdoorHumidityInput, setOutdoorHumidityInput] = useState<string>("");
const [modeTime, setModeTime] = useState<Moment>(moment());
const [grainUpdate, setGrainUpdate] = useState<boolean>(false);
const [openStorageTime, setOpenStorageTime] = useState(false);
const [sliderColour, setSliderCoulour] = useState("gold");
const [grainDiff, setGrainDiff] = useState<number>(0);
const [grainCables, setGrainCables] = useState<GrainCable[]>([]);
const iOS = typeof window !== "undefined" && /iPad|iPhone|iPod/.test(navigator.userAgent);
const [nodeDetails, setNodeDetails] = useState(false);
const [cfmLowOpen, setCFMLowOpen] = useState(false);
const [cfmHighOpen, setCFMHighOpen] = useState(false);
const [{ user }] = useGlobalState();
const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [selectedCable, setSelectedCable] = useState<GrainCable>();
const [openNodeDialog, setOpenNodeDialog] = useState(false);
const [cableDevice, setCableDevice] = useState<Device | undefined>();
const [selectedNode, setSelectedNode] = useState(0);
const [devMap, setDevMap] = useState<Map<number, Device>>(new Map<number, Device>());
//the cables with the highest and lowest temp nodes
//const [highTempCable, setHighTempCable] = useState<GrainCable>();
//const [lowTempCable, setLowTempCable] = useState<GrainCable>();
//the switch value to determine what to display in the grain condition box
const [valueDisplay, setValueDisplay] = useState<"low" | "avg" | "high">("avg");
//the variables to be able to change the grain type through a unique dialog outside of the settings
const [grainChangeDialog, setGrainChangeDialog] = useState(false);
const grainOptions = GrainOptions();
const [isCustomInventory, setIsCustomInventory] = useState(false);
const [grainOption, setGrainOption] = useState<Option>();
const [customTypeName, setCustomTypeName] = useState("");
const [grainType, setGrainType] = useState<pond.Grain>(pond.Grain.GRAIN_NONE);
const [storageType, setStorageType] = useState<pond.BinStorage>(
pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
);
const [bushPerTonne, setBushPerTonne] = useState("0");
const [grainSubtype, setGrainSubtype] = useState("");
const [loadingTrend, setLoadingTrend] = useState(false);
const [averageConditions, setAverageConditions] = useState<GrainConditions>();
const [highNodeConditions, setHighNodeConditions] = useState<GrainConditions>();
const [lowNodeConditions, setLowNodeConditions] = useState<GrainConditions>();
const [storageDate, setStorageDate] = useState(moment().format("YYYY-MM-DD"));
const [storageTime, setStorageTime] = useState(moment().format("HH:mm"));
// const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({
// 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"
// }
// }));
useEffect(() => {
setModeTime(moment(bin.status.lastModeChange));
}, [bin.status.lastModeChange]);
useEffect(() => {
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
setStorageType(bin.storage());
setNewPreset(pond.BinMode.BIN_MODE_NONE);
if (bin.settings.inventory) {
setMoistureInput(bin.settings.inventory.initialMoisture.toString());
setCustomTypeName(bin.settings.inventory.customTypeName);
setGrainType(bin.settings.inventory.grainType);
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
setGrainSubtype(bin.subtype());
}
if (bin.settings) {
let t = bin.settings.outdoorTemp;
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
t = CtoF(t);
}
setTempInput(t.toString());
}
if (bin.settings) setOutdoorHumidityInput(bin.settings.outdoorHumidity.toString());
}, [bin, user]);
useEffect(() => {
//if(loadingTrend) return
if (cables && cables.length > 0) {
setGrainCables(cables);
let trendCables = cables;
if (bin.key() !== "" && !loadingTrend) {
setLoadingTrend(true);
binAPI
.getBinsTrendData([bin.key()], 7)
.then(resp => {
let binTrend = resp.data.trendData[bin.key()];
//the variables to use to build the average conditions
let temps: number[] = [];
let humids: number[] = [];
let emcs: number[] = [];
let tempTrends: number[] = [];
let humidTrends: number[] = [];
let emcTrends: number[] = [];
let lowNodeConditions: GrainConditions | undefined = undefined;
let highNodeConditions: GrainConditions | undefined = undefined;
let avgConditions: GrainConditions | undefined = undefined;
trendCables.forEach(cable => {
//only use the values below the top node
//because splice mutates the original array make a clone in the cables so the svg still has all of them for display
//also reverse it so that the node 1 (end of the cable) is in the first position
let filteredNodes = cable.filteredNodes(true)
temps.push(...filteredNodes.temps)
humids.push(...filteredNodes.humids)
emcs.push(...filteredNodes.moistures)
// let tempClone = cloneDeep(cable.temperatures).reverse();
// let humClone = cloneDeep(cable.humidities).reverse();
// let emcClone = cloneDeep(cable.grainMoistures).reverse();
//add the cable data to the proper arrays
// if (cable.topNode > 0) {
// temps.push(...tempClone.splice(0, cable.topNode));
// humids.push(...humClone.splice(0, cable.topNode));
// emcs.push(...emcClone.splice(0, cable.topNode));
// } else {
// //if the cable has no fill set (top node) then use all of the nodes
// temps = tempClone;
// humids = humClone;
// emcs = emcClone;
// }
//add the trend data to the proper arrays if the top node is set
let cableTrendData = binTrend.cableTrend[cable.key()];
tempTrends.push(cableTrendData.grainCelciusTrend);
humidTrends.push(cableTrendData.grainHumidityTrend);
emcTrends.push(cableTrendData.grainEmcTrend);
//if the lowest temp for this cable is less than the temp in the low node conditions or the low node conditions are not set
//use this cables lowest node and trend data in the condition
if (!lowNodeConditions || Math.min(...temps) < lowNodeConditions.tempC) {
//determine which node is the coldest so that the data displayed is for the same node
let lowTempIndex =
temps.indexOf(Math.min(...temps)) === -1 ? 0 : temps.indexOf(Math.min(...temps));
lowNodeConditions = {
tempC: temps[lowTempIndex],
humidity: humids[lowTempIndex],
emc: emcs[lowTempIndex],
tempCTrend: cableTrendData.coldNodeTrend?.tempTrend ?? 0,
humidityTrend: cableTrendData.coldNodeTrend?.humidityTrend ?? 0,
emcTrend: cableTrendData.coldNodeTrend?.emcTrend ?? 0
};
}
//do the same for the high node
if (!highNodeConditions || cable.maxTemp() > highNodeConditions.tempC) {
let highTempIndex =
temps.indexOf(Math.max(...temps)) === -1 ? 0 : temps.indexOf(Math.max(...temps));
highNodeConditions = {
tempC: temps[highTempIndex],
humidity: humids[highTempIndex],
emc: emcs[highTempIndex],
tempCTrend: cableTrendData.hotNodeTrend?.tempTrend ?? 0,
humidityTrend: cableTrendData.hotNodeTrend?.humidityTrend ?? 0,
emcTrend: cableTrendData.hotNodeTrend?.emcTrend ?? 0
};
}
});
//set the average conditions
avgConditions = {
tempC: average(temps),
humidity: average(humids),
emc: average(emcs),
tempCTrend: average(tempTrends),
humidityTrend: average(humidTrends),
emcTrend: average(emcTrends)
};
setAverageConditions(avgConditions);
setLowNodeConditions(lowNodeConditions);
setHighNodeConditions(highNodeConditions);
})
.catch(err => {
console.log(err);
openSnack("Failed to retrieve trend data");
})
.finally(() => {
setLoadingTrend(false);
});
}
}
}, [binAPI, bin, cables, openSnack]); //eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
let cm = 0;
//just pull the number from the status
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));
} else {
setLidarBushels(undefined);
setLidarPercentage(undefined);
}
}, [bin]);
useEffect(() => {
let newMap = new Map<number, Device>();
devices.forEach(dev => {
newMap.set(dev.id(), dev);
});
setDevMap(newMap);
}, [devices]);
const resetFillPercentage = useCallback(() => {
if (bin.settings.inventory && bin.settings.specs) {
const isEmpty = bin.settings.inventory?.empty === true;
const capacity = bin.settings.specs.bushelCapacity;
const grainBushels = bin.bushels();
setSliderCoulour("gold");
if (!capacity) {
setFillPercentage(null);
} else if (isEmpty || !grainBushels) {
setFillPercentage(0);
} else {
setFillPercentage(Math.round((grainBushels / capacity) * 100));
}
}
}, [bin.settings.specs, bin.settings.inventory]);
useEffect(() => {
resetFillPercentage();
}, [resetFillPercentage]);
const getVolume = (val: number) => {
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
return Math.round(val * 35.239 * 100) / 100;
} else {
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) {
return Math.round((val / bin.bushelsPerTonne()) * 100) / 100;
}
}
return val;
};
const getCapacityDisplay = (bushelCapacity: number) => {
let c = 1;
if (bushelCapacity !== 0) {
c = bushelCapacity;
}
let capacity = getVolume(c);
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
return " / " + capacity.toLocaleString() + " L";
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) {
return "mT (" + bin.fillPercent() + "%)";
} else {
return " / " + capacity.toLocaleString() + " bu";
}
};
const grainErrorCheck = () => {
if (isNaN(parseFloat(bushPerTonne))) return true;
return false;
};
const inventoryOverview = () => {
const capacity = bin.settings.specs?.bushelCapacity ?? 0;
const grainBushels = bin.bushels();
const isEmpty = bin.settings.inventory?.empty === true || !grainBushels || grainBushels <= 0;
const grainType = bin.settings.inventory?.grainType;
const grainTypeName = isEmpty || !grainType ? "" : GrainDescriber(grainType).name;
const customTypeName = bin.settings.inventory?.customTypeName;
const grainSubtype = bin.settings.inventory?.grainSubtype;
return (
<Box>
{/* grain display */}
<Box display="flex" justifyContent="space-between">
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 800 }}>
{bin.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
? grainTypeName
: customTypeName}
{grainSubtype !== "" ? " - " + grainSubtype : ""}
</Typography>
<Box
style={{ cursor: "pointer" }}
onClick={() => {
setGrainChangeDialog(true);
}}>
<Edit />
</Box>
</Box>
{/* box that display capacity */}
<Box className={classes.bgItem}>
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2" style={{ fontWeight: "bold", marginRight: "4px" }}>
{isEmpty ? "Empty" : (grainBushels ? getVolume(grainBushels) : 0).toLocaleString()}
</Typography>
<Typography variant="body2">{isEmpty ? "" : getCapacityDisplay(capacity)}</Typography>
</div>
{!isEmpty &&
lidarBushels &&
bin.inventoryControl() !==
pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR && (
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2">
{(lidarBushels ? getVolume(lidarBushels) : 0).toLocaleString()} /{" "}
{(capacity ? getVolume(capacity) : 0).toLocaleString()} bu (est)
</Typography>
</div>
)}
</Box>
</Box>
);
};
const CtoF = (celsius: number) => {
return Math.round((celsius * (9 / 5) + 32) * 100) / 100;
};
const FtoC = (fahrenheit: number) => {
return Math.round((fahrenheit - 32) * (5 / 9) * 100) / 100;
};
const displayTemp = () => {
let temp = "--";
let valC: number | undefined;
switch (valueDisplay) {
case "avg":
if (averageConditions && !isNaN(averageConditions.tempC)) {
valC = averageConditions.tempC;
}
break;
case "high":
if (highNodeConditions && !isNaN(highNodeConditions.tempC)) {
valC = highNodeConditions.tempC;
}
break;
case "low":
if (lowNodeConditions && !isNaN(lowNodeConditions.tempC)) {
valC = lowNodeConditions.tempC;
}
break;
}
if (valC) {
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
temp = CtoF(valC).toFixed(1) + "°F";
} else {
temp = valC.toFixed(1) + "°C";
}
}
return (
<Typography
align="center"
noWrap
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: tempColour
}}>
{temp}
</Typography>
);
};
const displayHumidityOrEMC = () => {
let humidity = "--";
let useEMC = false;
let val: number | undefined;
switch (valueDisplay) {
case "avg":
if (averageConditions) {
if (averageConditions.emc) {
useEMC = true;
val = averageConditions.emc;
} else {
val = averageConditions.humidity;
}
}
break;
case "high":
if (highNodeConditions) {
if (highNodeConditions.emc) {
useEMC = true;
val = highNodeConditions.emc;
} else {
val = highNodeConditions.humidity;
}
}
break;
case "low":
if (lowNodeConditions) {
if (lowNodeConditions.emc) {
useEMC = true;
val = lowNodeConditions.emc;
} else {
val = lowNodeConditions.humidity;
}
}
break;
}
if (val && !isNaN(val)) {
humidity = val.toFixed(1) + "%";
}
return (
<Typography
align="center"
noWrap
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: useEMC ? emcColour : humidColour
}}>
{humidity}
</Typography>
);
};
const trendArrow = (val: number) => {
if (val > 0) return <TrendingUp style={{ fill: "green" }} />;
if (val < 0) return <TrendingDown style={{ fill: "red" }} />;
return <TrendingFlat />;
};
const displayTempTrend = () => {
let temp = "--";
let valC: number | undefined;
switch (valueDisplay) {
case "avg":
if (averageConditions && !isNaN(averageConditions.tempCTrend)) {
valC = averageConditions.tempCTrend;
}
break;
case "high":
if (highNodeConditions && !isNaN(highNodeConditions.tempCTrend)) {
valC = highNodeConditions.tempCTrend;
}
break;
case "low":
if (lowNodeConditions && !isNaN(lowNodeConditions.tempCTrend)) {
valC = lowNodeConditions.tempCTrend;
}
break;
}
if (valC) {
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
temp = Math.abs(valC * 1.8).toFixed(1) + "°F"; //since this is a measurement of change a change in 1 degrre celsius is equivalent to 1.8 fahrenheit
} else {
temp = Math.abs(valC).toFixed(1) + "°C";
}
}
return (
<Box height={20} display="flex" justifyContent="center">
{valC !== undefined && !isNaN(valC) && trendArrow(valC)}
<Typography
align="center"
noWrap
style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
{temp}
</Typography>
</Box>
);
};
const displayHumidOrEmcTrend = () => {
let display = "--";
let val: number | undefined;
switch (valueDisplay) {
case "avg":
if (averageConditions) {
if (averageConditions.emcTrend) {
val = averageConditions.emcTrend;
} else {
val = averageConditions.humidityTrend;
}
}
break;
case "high":
if (highNodeConditions) {
if (highNodeConditions.emcTrend) {
val = highNodeConditions.emcTrend;
} else {
val = highNodeConditions.humidityTrend;
}
}
break;
case "low":
if (lowNodeConditions) {
if (lowNodeConditions.emcTrend) {
val = lowNodeConditions.emcTrend;
} else {
val = lowNodeConditions.humidityTrend;
}
}
break;
}
if (val && !isNaN(val)) {
display = Math.abs(val).toFixed(1) + "%";
}
return (
<Box height={20} display="flex" justifyContent="center">
{val !== undefined && !isNaN(val) && trendArrow(val)}
<Typography
align="center"
noWrap
style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
{display}
</Typography>
</Box>
);
};
//this function is used to reset the state variable to what is in the bin whne they close it without saving any changes
const closeGrain = () => {
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
setStorageType(bin.storage());
if (bin.settings.inventory) {
setMoistureInput(bin.settings.inventory.initialMoisture.toString());
setCustomTypeName(bin.settings.inventory.customTypeName);
setGrainType(bin.settings.inventory.grainType);
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
setGrainSubtype(bin.subtype());
}
setGrainChangeDialog(false);
};
const changeGrain = () => {
const canEdit = permissions ? permissions.includes(pond.Permission.PERMISSION_WRITE) : false;
return (
<ResponsiveDialog
open={grainChangeDialog}
onClose={() => {
closeGrain();
}}>
<DialogTitle>Change Grain Type</DialogTitle>
<DialogContent>
<Grid container justifyContent="space-between" alignItems="center">
<Grid>
<Grid container alignItems="center">
<Grid >Grain</Grid>
<Grid >
<Switch
color="default"
value={isCustomInventory}
checked={isCustomInventory}
onChange={(_, checked) => {
setIsCustomInventory(checked);
setBushPerTonne("0");
setGrainSubtype("");
setCustomTypeName("");
setGrainOption(ToGrainOption(pond.Grain.GRAIN_NONE));
if (checked) {
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
setGrainType(pond.Grain.GRAIN_CUSTOM);
} else {
setStorageType(pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN);
setGrainType(pond.Grain.GRAIN_NONE);
}
}}
name="storage"
/>
</Grid>
<Grid >Custom</Grid>
</Grid>
</Grid>
</Grid>
{!isCustomInventory && (
<Box className={classes.bottomSpacing}>
<SearchSelect
label="Type"
selected={grainOption}
changeSelection={option => {
let newGrainType = option
? pond.Grain[option.value as keyof typeof pond.Grain]
: pond.Grain.GRAIN_INVALID;
setGrainOption(ToGrainOption(newGrainType));
setGrainType(newGrainType);
setBushPerTonne(GrainDescriber(newGrainType).bushelsPerTonne.toFixed(2));
}}
group
disabled={!canEdit}
options={grainOptions}
/>
</Box>
)}
{isCustomInventory ? (
<React.Fragment>
<TextField
label="Type"
value={customTypeName}
type="text"
onChange={event => {
setCustomTypeName(event.target.value);
}}
fullWidth
variant="outlined"
className={classes.bottomSpacing}
/>
{storageType !== pond.BinStorage.BIN_STORAGE_FERTILIZER && (
<TextField
label="Bushels Per Tonne"
value={bushPerTonne}
type="number"
error={isNaN(parseFloat(bushPerTonne))}
helperText={isNaN(parseFloat(bushPerTonne)) && "Must be a valid number"}
onChange={event => {
setBushPerTonne(event.target.value);
}}
fullWidth
variant="outlined"
className={classes.bottomSpacing}
/>
)}
</React.Fragment>
) : (
<TextField
label="Grain Variant"
value={grainSubtype}
type="text"
onChange={event => {
setGrainSubtype(event.target.value);
}}
fullWidth
variant="outlined"
disabled={!grainOption}
className={classes.bottomSpacing}
/>
)}
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeGrain();
}}>
Cancel
</Button>
<Button
variant="contained"
color="primary"
disabled={grainErrorCheck()}
onClick={() => {
updateBin();
setGrainChangeDialog(false);
}}>
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
//the box that displays the grain conditions
const grainCableOverview = () => {
return (
<Box>
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Grain Condition
</Typography>
<Box className={classes.displayBoxBinLeft}>
<Grid container alignContent="center" alignItems="center" direction="row">
<Grid size={{ xs: 2 }}>
<TemperatureIcon heightWidth={isMobile ? 20 : 25} />
</Grid>
<Grid size={{ xs: 4 }}>
<Box height={20} display="flex" justifyContent="center">
{displayTemp()}
</Box>
<Box display="flex" justifyContent="center">
<Typography
align="center"
color="textSecondary"
noWrap
style={{ fontSize: "0.6rem" }}>
{valueDisplay === "low" ? "Low" : valueDisplay === "high" ? "High" : "Average"}
</Typography>
</Box>
</Grid>
<Grid size={{ xs: 6 }}>
{displayTempTrend()}
<Box display="flex" justifyContent="center">
<Typography
align="center"
color="textSecondary"
noWrap
style={{ fontSize: "0.6rem" }}>
Past Week
</Typography>
</Box>
</Grid>
</Grid>
<Grid container alignContent="center" alignItems="center" direction="row">
<Grid size={{ xs: 2 }} style={{ paddingLeft: isMobile ? 5 : 8 }}>
<HumidityIcon height={isMobile ? 20 : 25} />
</Grid>
<Grid size={{ xs: 4 }}>
<Box height={20} display="flex" justifyContent="center">
{displayHumidityOrEMC()}
</Box>
<Box display="flex" justifyContent="center">
<Typography
align="center"
color="textSecondary"
noWrap
style={{ fontSize: "0.6rem" }}>
{valueDisplay === "low" ? "Low" : valueDisplay === "high" ? "High" : "Average"}
</Typography>
</Box>
</Grid>
<Grid size={{ xs: 6 }}>
{displayHumidOrEmcTrend()}
<Box display="flex" justifyContent="center">
<Typography
align="center"
color="textSecondary"
noWrap
style={{ fontSize: "0.6rem" }}>
Past Week
</Typography>
</Box>
</Grid>
</Grid>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between"
style={{ marginTop: 10 }}>
<Grid size={{ xs: 4 }}>
<Button
onClick={() => {
setValueDisplay("low");
}}
style={{
width: "100%",
minWidth: 0,
zIndex: valueDisplay === "low" ? 1 : 0,
background:
valueDisplay === "low" ? "gold" : darken(theme.palette.background.paper, 0.2),
borderRadius: 10
}}>
<Typography
align="center"
noWrap
style={{
fontWeight: 650,
fontSize: "0.7rem",
color: valueDisplay === "low" ? "black" : "white"
}}>
Low
</Typography>
</Button>
</Grid>
<Grid size={{ xs: 4 }}>
<Button
onClick={() => {
setValueDisplay("avg");
}}
style={{
width: "100%",
minWidth: 0,
zIndex: valueDisplay === "avg" ? 1 : 0,
background:
valueDisplay === "avg" ? "gold" : darken(theme.palette.background.paper, 0.2),
borderRadius: 10
}}>
<Typography
align="center"
noWrap
style={{
fontWeight: 650,
fontSize: "0.7rem",
color: valueDisplay === "avg" ? "black" : "white"
}}>
Avg
</Typography>
</Button>
</Grid>
<Grid size={{ xs: 4 }}>
<Button
onClick={() => {
setValueDisplay("high");
}}
style={{
width: "100%",
minWidth: 0,
zIndex: valueDisplay === "high" ? 1 : 0,
background:
valueDisplay === "high" ? "gold" : darken(theme.palette.background.paper, 0.2),
borderRadius: 10
}}>
<Typography
align="center"
noWrap
style={{
fontWeight: 650,
fontSize: "0.7rem",
color: valueDisplay === "high" ? "black" : "white"
}}>
High
</Typography>
</Button>
</Grid>
</Grid>
</Box>
</Box>
);
};
const plenumOverview = () => {
return (
<Box style={{ position: "absolute", bottom: 5, width: "100%" }}>
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Plenum
</Typography>
<Box
className={classes.displayBoxBinLeft}
style={{ marginRight: isMobile ? -70 : -90 }}
position="relative">
{plenum ? (
<React.Fragment>
<Grid
container
alignContent="center"
alignItems="center"
direction="row"
style={{ paddingLeft: 5 }}>
<Grid size={{ xs: 2 }}>
<Avatar
variant="square"
src={GetComponentIcon(
quack.ComponentType.COMPONENT_TYPE_DHT,
undefined,
theme.palette.mode
)}
style={{ height: isMobile ? 20 : 25, width: isMobile ? 20 : 25 }}
/>
</Grid>
<Grid size={{ xs: 5 }}>
<Typography
align="center"
noWrap
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: tempColour
}}>
{plenum.getTempString(user.settings.temperatureUnit)}
</Typography>
</Grid>
<Grid size={{ xs: 5 }}>
<Typography
align="center"
noWrap
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: humidColour
}}>
{plenum.getHumidityString()}
</Typography>
</Grid>
</Grid>
{/* <Divider
style={{
marginLeft: theme.spacing(1),
marginTop: 5,
marginBottom: 5,
background: "gold",
marginRight: -40
}}
/> */}
{pressureOverview()}
</React.Fragment>
) : (
<Box flexDirection={"row"} display="flex">
<Avatar variant="square" className={classes.avatarIcon}>
<BindaptIcon />
</Avatar>
<Box textAlign="center" width={"85%"}>
<Link href="https://www.bindapt.com/bins/">View Smart Bin Devices</Link>
</Box>
</Box>
)}
</Box>
</Box>
);
};
const cfmDryWarning = (cfm: number) => {
if (cfm > 2) {
return (
<IconButton onClick={() => setCFMHighOpen(true)}>
<Error style={{ color: "yellow" }} />
</IconButton>
);
} else if (cfm < 1) {
return (
<IconButton onClick={() => setCFMLowOpen(true)}>
<Warning style={{ color: "red" }} />
</IconButton>
);
} else {
return (
<IconButton disabled>
<CheckCircleOutline style={{ color: "green" }} />
</IconButton>
);
}
};
const cfmHighDialog = () => {
return (
<ResponsiveDialog
open={cfmHighOpen}
onClose={() => {
setCFMHighOpen(false);
}}>
<DialogTitle>CFM Warning</DialogTitle>
<DialogContent>
<Typography>Your CFM seems abnormally high</Typography>
Your fan may have a loose connection
</DialogContent>
<DialogActions>
<Button
color="primary"
onClick={() => {
setCFMHighOpen(false);
}}>
Close
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const cfmLowDialog = () => {
return (
<ResponsiveDialog
open={cfmLowOpen}
onClose={() => {
setCFMLowOpen(false);
}}>
<DialogTitle>CFM Warning</DialogTitle>
<DialogContent>
<Typography>Your CFM is low</Typography>
Your bin may be too full for your fan, try reducing inventory.
</DialogContent>
<DialogActions>
<Button
color="primary"
onClick={() => {
setCFMLowOpen(false);
}}>
Close
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const pressureOverview = () => {
return (
<Grid
container
alignContent="center"
alignItems="center"
direction="row"
style={{ paddingLeft: 5, marginTop: 4 }}>
<Grid size={{ xs: 2 }}>
<Avatar
variant="square"
src={GetComponentIcon(
quack.ComponentType.COMPONENT_TYPE_PRESSURE,
undefined,
theme.palette.mode
)}
style={{ height: isMobile ? 20 : 25, width: isMobile ? 20 : 25 }}
/>
</Grid>
{pressure ? (
<React.Fragment>
<Grid size={{ xs: 5 }}>
<Typography
align="center"
noWrap
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: pressColour
}}>
{pressure.getPressureString(user.settings.pressureUnit)}
</Typography>
</Grid>
{/* <Grid item xs={5}>
<Typography
align="center"
noWrap
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: pressColour
}}>
{bin.status.fanCfm.toFixed(0) + "CFM"}
</Typography>
</Grid> */}
</React.Fragment>
) : (
<React.Fragment>
<Grid size={{ xs: 10 }}>
<Typography
color="textSecondary"
style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
N/A
</Typography>
</Grid>
</React.Fragment>
)}
</Grid>
);
};
const overview = () => {
return (
<React.Fragment>
{inventoryOverview()}
{grainCableOverview()}
{plenumOverview()}
</React.Fragment>
);
};
const getBinSVGHeight = () => {
if (isMobile) {
return fullScreenHandler.active ? viewport.height * 0.8 : viewport.width * 0.8;
}
return fullScreenHandler.active ? viewport.height * 0.8 : viewport.height * 0.4;
};
const visual = () => {
let diffDisplay = grainDiff;
let pendingDisplay = pendingGrainAmount;
if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
diffDisplay = diffDisplay * 35.239;
if (pendingDisplay) {
pendingDisplay = pendingDisplay * 35.239;
}
}
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT && bin.bushelsPerTonne() > 1) {
diffDisplay = diffDisplay / bin.bushelsPerTonne();
if (pendingDisplay) {
pendingDisplay = pendingDisplay / bin.bushelsPerTonne();
}
}
return (
<Box display="flex" width={1} justifyContent="flex-end">
<FullScreen handle={fullScreenHandler}>
<Box
position="relative"
height={1}
width={1}
display="flex"
justifyContent="center"
alignItems="center"
alignContent="flex-end">
{fullScreenHandler.active && (
<Box position="absolute" bottom={10} right={10}>
<IconButton onClick={fullScreenHandler.exit}>
<FullscreenExitIcon />
</IconButton>
</Box>
)}
<Box zIndex={5}>
<Box>
{grainDiff > 0 && (
<div
className={classes.grainOVerlay}
style={{
top: bin.binShape() === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? "30%" : "35%",
color: "#5CE422"
}}>
<Typography variant={"h5"} style={{ fontWeight: 750 }}>
+{diffDisplay.toLocaleString()}
</Typography>
</div>
)}
{pendingDisplay !== undefined && (
<div
className={classes.grainOVerlay}
style={{
top: bin.binShape() === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? "40%" : "45%"
}}>
<Typography variant={"h4"} style={{ fontWeight: 750 }}>
{pendingDisplay.toLocaleString()}
</Typography>
</div>
)}
{grainDiff < 0 && (
<div
className={classes.grainOVerlay}
style={{
top: bin.binShape() === pond.BinShape.BIN_SHAPE_HOPPER_BOTTOM ? "50%" : "55%",
color: "#E42222"
}}>
<Typography variant={"h5"} style={{ fontWeight: 750 }}>
{diffDisplay.toLocaleString()}
</Typography>
</div>
)}
</Box>
<BinSVGV2
height={getBinSVGHeight()}
hasMinHeight
binShape={bin.settings.specs?.shape}
fillPercentage={fillPercentage ?? 0}
lidarEstimate={lidarPercentage}
cables={grainCables}
co2Sensors={bin.status.co2}
highTemp={bin.settings.highTemp}
lowTemp={bin.settings.lowTemp}
showTempHum={iOS ? nodeDetails : fullScreenHandler.active}
grainType={bin.settings.inventory?.grainType}
isFullScreen={fullScreenHandler.active}
cableEstimate={bin.settings.autoGrainNode}
hottestNodeTemp={valueDisplay === "high" ? highNodeConditions?.tempC : undefined}
coldestNodeTemp={valueDisplay === "low" ? lowNodeConditions?.tempC : undefined}
cableNodeClicked={(cable, fillNode) => {
if (cable) {
let devId = componentDevices?.get(cable.key());
if (devId) {
let device = devMap.get(devId);
if (device) {
setSelectedCable(cable);
setSelectedNode(fillNode);
setCableDevice(device);
setOpenNodeDialog(true);
}
}
}
}}
inventoryControl={bin.inventoryControl()}
/>
</Box>
</Box>
</FullScreen>
</Box>
);
};
const valueLabel = (value: number) => {
return (
<Box display="flex" sx={{background: sliderColour, height: 30, width: 30, borderRadius: 20}}>
<Typography sx={{fontSize: 12, fontWeight: 650, color: "black", textAlign: "center", margin: "auto"}}>{value}%</Typography>
</Box>
)
}
const controls = () => {
if (bin.settings.inventory?.empty === true) return null;
return (
<Box
height={1}
width={1}
display="flex"
flexDirection="column"
justifyContent="flex-end"
alignContent="flex-end">
{(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_MANUAL ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN) && (
<React.Fragment>
<Box display="flex" height={isMobile ? 35 : 40} marginBottom={1}>
<IconButton
onClick={() => {
setGrainUpdate(true);
}}
style={{
margin: "auto",
backgroundColor: "gold",
color: "black",
height: "100%",
width: isMobile ? 35 : 40
}}>
<Typography sx={{fontSize: isMobile ? 17 : 20, fontWeight: 650}}>+/-</Typography>
</IconButton>
</Box>
<Box
height={0.7}
width={1}
display="flex"
justifyContent="center"
alignContent="flex-end">
{fillPercentage !== null && (
<Slider
orientation="vertical"
value={fillPercentage}
classes={{
// root: classes.sliderRoot,
// rail: classes.sliderRail,
// track: classes.sliderTrack,
// thumb: isMobile ? classes.mobileSliderThumb : classes.sliderThumb,
valueLabel: classes.sliderValLabel,
}}
style={{ height: "100%", color: sliderColour }}
min={0}
max={100}
valueLabelDisplay="auto"
valueLabelFormat={valueLabel}
onChange={(_, value) => {
setFillPercentage(value as number);
const capacity = bin.settings.specs ? bin.settings.specs.bushelCapacity : 0;
const current = bin.bushels()
let grainAmount = ((value as number) / 100) * capacity;
if (grainAmount < current) {
setSliderCoulour("#E42222");
} else if (grainAmount > current) {
setSliderCoulour("#5CE422");
} else {
setSliderCoulour("gold");
}
setGrainDiff(round(grainAmount - current, 0));
setPendingGrainAmount(round(grainAmount, 0));
}}
onChangeCommitted={() => {
setGrainUpdate(true);
}}
aria-labelledby="grain-amount"
/>
)}
</Box>
</React.Fragment>
)}
<Box height={0.1} width={1} />
<Box width={1} textAlign="center">
{iOS ? (
<IconButton size="small" onClick={() => setNodeDetails(!nodeDetails)}>
<InfoOutlined />
</IconButton>
) : (
<IconButton size="small" onClick={fullScreenHandler.enter}>
<FullscreenIcon />
</IconButton>
)}
</Box>
</Box>
);
};
const controllerDisplay = () => {
return (
<Box style={{ marginTop: 5 }}>
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Controllers
</Typography>
<Box className={classes.displayBox}>
{bin.status.fans.map(fan => (
<Box className={classes.controllerDisplay} key={fan.key}>
<Box display="flex">
<AerationFanIcon />
<Typography
align="center"
style={{
paddingLeft: 5,
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650
}}>
{fan.name}
</Typography>
</Box>
<Typography
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: fan.state ? "green" : "orange"
}}>
{fan.state ? "ON" : "OFF"}
</Typography>
</Box>
))}
{bin.status.heaters.map(heater => (
<Box className={classes.controllerDisplay} key={heater.key}>
<Box display="flex">
<ObjectHeaterIcon />
<Typography
align="center"
style={{
paddingLeft: 5,
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650
}}>
{heater.name}
</Typography>
</Box>
<Typography
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: heater.state ? "green" : "orange"
}}>
{heater.state ? "ON" : "OFF"}
</Typography>
</Box>
))}
</Box>
</Box>
);
};
const fanPerformance = () => {
let totalCFM = bin.status.fanCfm;
let bushelCFM = bin.status.cfmPerBushel;
return (
<Box>
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Fan Performance
</Typography>
{pressure && pressure.fanId === 0 && !bin.settings.fan?.type && bin.fanID() === 0 && (
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 600 }}>
No fans found to calculate CFM
</Typography>
)}
<Box className={classes.displayBox}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid >
<Box display="flex">
<Box>
<Typography
align="center"
style={{
marginLeft: 10,
fontWeight: 650,
color: pressColour
}}>
{totalCFM.toFixed(0) + " CFM"}
</Typography>
<Typography
align="center"
style={{
marginLeft: 10,
fontWeight: 650,
color: pressColour
}}>
{bushelCFM.toFixed(2) + " CFM/bu"}
</Typography>
</Box>
{cfmDryWarning(bushelCFM)}
</Box>
</Grid>
<Grid >
<Box className={classes.lightBox}>
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
{ExtractMoisture(
bin.grain(),
plenum?.temperature ?? 0,
plenum?.humidity ?? 0
).toFixed(2)}
%
</Typography>
<Typography align="center" style={{ fontWeight: 650 }}>
Plenum EMC
</Typography>
</Box>
</Grid>
</Grid>
</Box>
</Box>
);
};
const ambientDisplay = () => {
return (
<Box>
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Ambient
</Typography>
<Box className={classes.displayBox}>
<Grid
container
direction="row"
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid >
<Box display="flex" marginY={1}>
<TemperatureIcon />
<Typography
style={{
marginLeft: 10,
fontWeight: 650,
color: tempColour
}}>
{ambient?.getTempString(getTemperatureUnit())}
</Typography>
</Box>
<Box display="flex" marginY={1}>
<Box style={{ width: 24, paddingLeft: 6 }}>
<HumidityIcon height={24} width={15} />
</Box>
<Typography
style={{
marginLeft: 10,
fontWeight: 650,
color: humidColour
}}>
{ambient?.getHumidityString()}
</Typography>
</Box>
</Grid>
<Grid >
<Box className={classes.lightBox}>
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
{ExtractMoisture(
bin.grain(),
ambient?.temperature ?? 0,
ambient?.humidity ?? 0
).toFixed(2)}
%
</Typography>
<Typography align="center" style={{ fontWeight: 650 }}>
Ambient EMC
</Typography>
</Box>
</Grid>
</Grid>
</Box>
</Box>
);
};
const determineToggle = (binMode: pond.BinMode) => {
switch (binMode) {
case pond.BinMode.BIN_MODE_STORAGE:
return [0]
case pond.BinMode.BIN_MODE_COOLDOWN:
return [1]
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_HYDRATING:
return [2]
}
}
const binMode = () => {
const mode = bin.settings.mode;
return (
<Box
display="flex"
marginBottom={1}
flexGrow={1}
height={"100%"}
width={1}
justifyContent="space-between"
alignItems="center"
alignSelf="flex-end">
<Typography variant="subtitle1" style={{ fontWeight: 800 }}>
Bin Mode
</Typography>
<ButtonGroup
buttons={[
{
title: "Storage",
function: () => {
setModeStorage()
}
},
{
title: "Cooldown",
function: () => {
setModeCooldown()
}
},
{
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => {
setShowInputMoisture(true)
}
}
]}
toggledButtons={determineToggle(mode)}
toggle
/>
{/* <StyledToggleButtonGroup
id="tour-bin-mode"
value={mode}
exclusive
size="small"
aria-label="Bin Mode">
<StyledToggle
value={pond.BinMode.BIN_MODE_STORAGE}
aria-label="Storage Mode"
onClick={setModeStorage}>
Storage
</StyledToggle>
<StyledToggle
onClick={setModeCooldown}
value={pond.BinMode.BIN_MODE_COOLDOWN}
aria-label="Off">
Cooldown
</StyledToggle>
{bin.settings.inventory &&
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? (
<StyledToggle
onClick={() => setShowInputMoisture(true)}
value={pond.BinMode.BIN_MODE_HYDRATING}
aria-label="Hydrating Mode">
Hydrating
</StyledToggle>
) : (
<StyledToggle
onClick={() => setShowInputMoisture(true)}
value={pond.BinMode.BIN_MODE_DRYING}
aria-label="Drying Mode">
Drying
</StyledToggle>
)}
</StyledToggleButtonGroup> */}
</Box>
);
};
const showDays = (time: Moment) => {
let now = moment();
let duration = moment.duration(time.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 (bin.settings.mode === pond.BinMode.BIN_MODE_NONE) {
return (
<Typography variant="subtitle2" className={classes.days}>
Select Mode
</Typography>
);
}
if (days > 50 && bin.settings.mode === pond.BinMode.BIN_MODE_DRYING) {
return (
<Typography variant="subtitle2" className={classes.days}>
Calculating...
</Typography>
);
}
return (
<Typography variant="subtitle2" className={classes.days} style={{ fontWeight: 700 }}>
{Math.floor(days)}d {hours}h
</Typography>
);
};
const updateStorageTime = () => {
let status = bin.status;
let newTimestamp = storageDate + " " + storageTime;
setModeTime(moment(newTimestamp));
status.lastModeChange = newTimestamp;
binAPI
.updateBinStatus(bin.key(), status, as)
.then(resp => {
openSnack("Reset the storage time");
})
.catch(err => {
openSnack("There was a problem resetting the storage time");
});
};
const storageTimeDialog = () => {
return (
<ResponsiveDialog open={openStorageTime}>
<DialogTitle>Set Storage Time</DialogTitle>
<DialogContent>
<Typography>Set the date and time that the grain was initially stored</Typography>
<TextField
//style={{ width: "45%" }}
fullWidth
margin="normal"
type="date"
label="Storage Date"
value={storageDate}
onChange={e => setStorageDate(e.target.value)}
/>
<TextField
//style={{ width: "45%", marginLeft: "10%" }}
fullWidth
margin="normal"
type="time"
label="Storage Time"
value={storageTime}
onChange={e => setStorageTime(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Box display="flex" justifyContent="space-between">
<Button
onClick={() => {
setOpenStorageTime(false);
}}>
Close
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
updateStorageTime();
setOpenStorageTime(false);
}}>
Confirm
</Button>
</Box>
</DialogActions>
</ResponsiveDialog>
);
};
const dryingEstimate = () => {
if (
bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING
) {
let time = moment(bin.status.targetMoistureEstimation);
return (
<Box className={classes.extraDetails}>
<Box style={{ position: "relative", transform: "translate(50%, 25%)" }}>
<IconButton style={{ position: "absolute" }} size="small" onClick={() => refresh(true)}>
<RefreshIcon />
</IconButton>
</Box>
<Box padding={0} style={{ flex: 1, textAlign: "center" }}>
<Typography variant="body2" style={{ margin: theme.spacing(0.5) }}>
{bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ? "Drying" : "Hydrating"} Time
Estimate:
</Typography>
{showDays(time)}
</Box>
</Box>
);
}
if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) {
return (
<Box className={classes.extraDetails}>
<Box style={{ position: "relative", transform: "translate(50%, 25%)" }}>
<IconButton style={{ position: "absolute" }} size="small" onClick={() => refresh(true)}>
<RefreshIcon />
</IconButton>
</Box>
<Box padding={0} style={{ flex: 1, textAlign: "center" }}>
<Typography variant="body2" style={{ margin: theme.spacing(0.5) }}>
Cooling:
</Typography>
{showDays(modeTime)}
</Box>
</Box>
);
}
return (
<Box className={classes.extraDetails}>
<Box style={{ position: "relative", transform: "translate(0%, 25%)" }}>
<IconButton style={{ position: "absolute" }} size="small" onClick={() => refresh(true)}>
<RefreshIcon />
</IconButton>
</Box>
<Box padding={0} style={{ flex: 1, textAlign: "center" }}>
<Typography variant="body2" style={{ margin: theme.spacing(0.5) }}>
{bin.settings.mode === pond.BinMode.BIN_MODE_STORAGE ? "In storage for:" : "^"}
</Typography>
{showDays(modeTime)}
</Box>
<Box style={{ position: "relative", transform: "translate(0%, 25%)" }}>
<IconButton
style={{ position: "absolute", right: 1 }}
size="small"
onClick={() => {
setOpenStorageTime(true);
}}>
<AccessTime />
</IconButton>
</Box>
</Box>
);
};
const setModeStorage = () => {
setNewPreset(pond.BinMode.BIN_MODE_STORAGE);
};
const setModeCooldown = () => {
if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) {
return;
}
setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN);
};
const setModeDrying = () => {
if (
bin.settings.inventory &&
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture
) {
setNewPreset(pond.BinMode.BIN_MODE_HYDRATING);
} else {
setNewPreset(pond.BinMode.BIN_MODE_DRYING);
}
};
const closeMoistureDialog = () => {
setNewPreset(0);
setShowInputMoisture(false);
};
const moistureDialog = () => {
return (
<ResponsiveDialog open={showInputMoisture} onClose={closeMoistureDialog}>
<DialogTitle>Input new grain moisture</DialogTitle>
<DialogContent>
<DialogContentText style={{ paddingBottom: theme.spacing(0) }}>
Updating the current grain moisture will calibrate the estimate for more accurate
results. The outdoor temperature will have some effect on the estimate as well; consider
updating with drastic changes in the weather or using a predicted average.
</DialogContentText>
<TextField
label={"Grain Moisture"}
value={moistureInput}
onChange={event => setMoistureInput(event.target.value)}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>
}}
style={{ marginTop: theme.spacing(2) }}
fullWidth
variant="outlined"
/>
<Typography variant="body1" style={{ marginTop: theme.spacing(2) }}>
Weather
</Typography>
<TextField
label={"Outdoor Temperature"}
value={tempInput}
onChange={event => setTempInput(event.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "℃"}
</InputAdornment>
)
}}
style={{ marginTop: theme.spacing(2) }}
fullWidth
variant="outlined"
/>
<TextField
label={"Outdoor Humidity"}
value={outdoorHumidityInput}
onChange={event => setOutdoorHumidityInput(event.target.value)}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>
}}
style={{ marginTop: theme.spacing(2) }}
fullWidth
variant="outlined"
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setNewPreset(0);
setShowInputMoisture(false);
}}
color="primary">
Cancel
</Button>
<Button onClick={setModeDrying} color="primary">
Update
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
if (loading) {
return <Skeleton variant="rectangular" height={460} />;
}
const updateBin = () => {
let b = bin;
b.settings.storage = storageType;
if (b.settings.inventory) {
if (b.settings.inventory.initialMoisture)
b.settings.inventory.initialMoisture = Number(moistureInput);
//update all the grain stuff
b.settings.inventory.grainType = grainType;
b.settings.inventory.customTypeName = customTypeName;
b.settings.inventory.bushelsPerTonne = isNaN(parseFloat(bushPerTonne))
? 0
: parseFloat(bushPerTonne);
b.settings.inventory.grainSubtype = grainSubtype;
}
if (b.settings.outdoorTemp !== undefined) {
let t = Number(tempInput);
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
t = FtoC(Number(tempInput));
}
b.settings.outdoorTemp = t;
}
if (b.settings.outdoorHumidity !== undefined)
b.settings.outdoorHumidity = Number(outdoorHumidityInput);
b.settings.mode = newPreset;
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
refresh();
});
};
return (
<Card raised className={classes.cardContent}>
{moistureDialog()}
{cfmLowDialog()}
{cfmHighDialog()}
{changeGrain()}
{storageTimeDialog()}
<DevicePresetsFromPicker
preferences={preferences}
binKey={bin.key()}
devices={devices}
refreshCallback={success => {
setShowInputMoisture(false);
setNewPreset(0)
if (success) updateBin();
}}
parentPreset={newPreset}
grain={bin.settings.inventory?.grainType}
binCables={cables}
compDevMap={componentDevices}
presets={binPresets}
/>
<Box paddingRight={1}>
{binMode()}
<Grid container justifyContent="flex-start" alignItems="stretch">
<Grid size={5.5} style={{ position: "relative" }}>
{overview()}
</Grid>
<Grid size={5} style={{ position: "relative" }}>
{visual()}
</Grid>
<Grid size={1.5} >{controls()}</Grid>
</Grid>
{plenum && pressure && fanPerformance()}
{ambient && ambientDisplay()}
{(bin.status.fans.length > 0 || bin.status.heaters.length > 0) && controllerDisplay()}
{dryingEstimate()}
</Box>
<GrainTransaction
open={grainUpdate}
mainObject={bin}
grainAdjustment={grainDiff}
close={() => {
setGrainUpdate(false);
setPendingGrainAmount(undefined);
resetFillPercentage();
setGrainDiff(0);
}}
callback={props.afterUpdate}
/>
{selectedCable && cableDevice && (
<GrainNodeInteractions
binKey={bin.key()}
interactionComponents={Array.from(components.values())}
grain={bin.grain()}
cable={selectedCable}
open={openNodeDialog}
device={cableDevice}
selectedNode={selectedNode}
close={() => {
setOpenNodeDialog(false);
}}
permissions={permissions ?? []}
updateComponentCallback={updateComponentCallback}
/>
)}
<div style={{ bottom: "0px" }}></div>
</Card>
);
}