frontend/src/bin/BinVisualizerV2.tsx

2094 lines
70 KiB
TypeScript

import {
Avatar,
Box,
Button,
Card,
CircularProgress,
darken,
DialogActions,
DialogContent,
DialogTitle,
Grid2 as Grid,
IconButton,
lighten,
Link,
Skeleton,
Slider,
Switch,
TextField,
Theme,
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 { round } from "lodash";
import { Bin, Component, Device } 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,
ArrowForwardIos,
CheckCircleOutline,
Error,
InfoOutlined,
TrendingDown,
TrendingFlat,
TrendingUp,
Warning
} from "@mui/icons-material";
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 } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
import ModeChangeDialog from "./conditioning/modeChangeDialog";
import CustomGrainSelector from "grain/CustomGrainSelector";
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 CombinedPlenum {
tempHumidity?: Plenum,
pressure?: Pressure
}
interface Props {
bin: Bin;
loading: boolean;
components: Map<string, Component>;
componentDevices?: Map<string, number>;
devices: Device[];
plenums?: Plenum[];
ambient?: Ambient;
cables?: GrainCable[];
pressures?: Pressure[];
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,
plenums,
ambient,
pressures,
loading,
cables,
components,
devices,
preferences,
componentDevices,
permissions,
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 [targetMoisture, setTargetMoisture] = useState(0);
const [outdoorTemp, setOutdoorTemp] = useState(0);
const [outdoorHumidity, setOutdoorHumidity] = useState(0);
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 [newBinMode, setNewBinMode] = 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">("high");
//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 [activePlenum, setActivePlenum] = useState<CombinedPlenum | undefined>()
const [activePlenumIndex, setActivePlenumIndex] = useState(0)
const [combinedPlenums, setCombinedPlenums] = useState<CombinedPlenum[]>([])
const [openModeChange, setOpenModeChange] = useState(false)
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
const [newGrainSettings, setNewGrainSettings] = useState<pond.GrainSettings>()
useEffect(() => {
setModeTime(moment(bin.status.lastModeChange));
}, [bin.status.lastModeChange]);
useEffect(()=>{
//match the plenums and pressures based on their location (address)
let combinedPlenums: CombinedPlenum[] = []
if (plenums) {
plenums.forEach(plenum => {
let newCombinedPlenum: CombinedPlenum = {
tempHumidity: plenum
}
if (pressures) {
pressures.forEach(pressure => {
if(plenum.location().address === pressure.location().address){
newCombinedPlenum.pressure = pressure
}
})
}
combinedPlenums.push(newCombinedPlenum)
})
}
if (combinedPlenums.length > 0){
setActivePlenum(combinedPlenums[0])
}
setCombinedPlenums(combinedPlenums)
},[plenums, pressures])
useEffect(() => {
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
setStorageType(bin.storage());
// setNewPreset(pond.BinMode.BIN_MODE_NONE);
if (bin.settings.inventory) {
setTargetMoisture(bin.settings.inventory.initialMoisture);
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());
setNewBinMode(bin.settings.mode);
setNewGrainSettings(bin.customGrain());
}
if (bin.settings) {
let t = bin.settings.outdoorTemp;
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
t = CtoF(t);
}
setOutdoorTemp(t);
}
if (bin.settings) setOutdoorHumidity(bin.settings.outdoorHumidity);
}, [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)
//only push to the humidity values if the cable reads humidities
if(cable.subType() !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP){
humids.push(...filteredNodes.humids)
emcs.push(...filteredNodes.moistures)
}
//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(...filteredNodes.temps) < lowNodeConditions.tempC) {
//determine which node is the coldest so that the data displayed is for the same node
let lowTempIndex =
filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps));
lowNodeConditions = {
tempC: filteredNodes.temps[lowTempIndex],
humidity: filteredNodes.humids[lowTempIndex],
emc: filteredNodes.moistures[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 || Math.max(...filteredNodes.temps) > highNodeConditions.tempC) {
let highTempIndex =
filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps));
highNodeConditions = {
tempC: filteredNodes.temps[highTempIndex],
humidity: filteredNodes.humids[highTempIndex],
emc: filteredNodes.moistures[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 grainSubtype = bin.settings.inventory?.grainSubtype;
return (
<Box>
{/* grain display */}
<Box display="flex" justifyContent="space-between">
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 800 }}>
{isEmpty ? "" : bin.grainName()}
{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) {
setTargetMoisture(bin.settings.inventory.initialMoisture);
setCustomTypeName(bin.settings.inventory.customTypeName);
setGrainType(bin.settings.inventory.grainType);
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
setNewGrainSettings(bin.customGrain())
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_UNSUPPORTED_GRAIN);
setGrainType(pond.Grain.GRAIN_CUSTOM);
setNewGrainSettings(bin.customGrain())
} else {
setStorageType(pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
setGrainType(pond.Grain.GRAIN_NONE);
setNewGrainSettings(undefined)
}
}}
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}
/>
)} */}
<CustomGrainSelector initialGrain={bin.customGrain()} onGrainSettingsChange={settings => {setNewGrainSettings(settings)}}/>
</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%" }}>
<Box display="flex" flexDirection="row" alignItems="center">
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Plenum {plenums && plenums?.length > 1 ? activePlenumIndex+1 : ""}
</Typography>
{plenums && plenums.length > 1 &&
<IconButton sx={{background: darken(theme.palette.background.paper, 0.1), marginLeft: 1}} size="small" onClick={()=>{
let newIndex = activePlenumIndex + 1
if(newIndex > combinedPlenums.length - 1){
newIndex = 0
}
setActivePlenum(combinedPlenums[newIndex])
setActivePlenumIndex(newIndex)
}}>
<ArrowForwardIos sx={{ fontSize: 15}}/>
</IconButton>
}
</Box>
<Box
className={classes.displayBoxBinLeft}
style={{ marginRight: isMobile ? -70 : -90 }}
position="relative">
{activePlenum?.tempHumidity ? (
<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
}}>
{activePlenum.tempHumidity.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
}}>
{activePlenum.tempHumidity.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>
{activePlenum?.pressure ? (
<React.Fragment>
<Grid size={{ xs: 5 }}>
<Typography
align="center"
noWrap
style={{
fontSize: isMobile ? "0.85rem" : "1.0rem",
fontWeight: 650,
color: pressColour
}}>
{activePlenum.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()}
customGrain={bin.customGrain()}
/>
</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;
let emc = ExtractMoisture(
bin.grain(),
activePlenum?.tempHumidity?.temperature ?? 0,
activePlenum?.tempHumidity?.humidity ?? 0,
bin.customGrain()
)
return (
<Box>
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Fan Performance
</Typography>
{activePlenum?.pressure && activePlenum.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>
{emc !== activePlenum?.tempHumidity?.humidity &&
<Box className={classes.lightBox}>
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
{emc.toFixed(2)}
%
</Typography>
<Typography align="center" style={{ fontWeight: 650 }}>
Plenum EMC
</Typography>
</Box>
}
</Grid>
</Grid>
</Box>
</Box>
);
};
const ambientDisplay = () => {
let emc = ExtractMoisture(
bin.grain(),
ambient?.temperature ?? 0,
ambient?.humidity ?? 0,
bin.customGrain()
)
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 >
{emc !== ambient?.humidity &&
<Box className={classes.lightBox}>
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
{emc.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>
{modeChangeInProgress &&
<CircularProgress color="primary" size={25}/>
}
<Box id="tour-bin-mode">
<ButtonGroup
disableAll={modeChangeInProgress}
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)
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
}
}
]}
toggledButtons={determineToggle(mode)}
toggle
/>
</Box>
</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);
setNewBinMode(pond.BinMode.BIN_MODE_STORAGE);
setOpenModeChange(true)
};
const setModeCooldown = () => {
if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) {
return;
}
// setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN);
setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN);
setOpenModeChange(true)
};
const setModeConditioning = () => {
if (
bin.settings.inventory &&
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture
) {
// setNewPreset(pond.BinMode.BIN_MODE_HYDRATING);
setNewBinMode(pond.BinMode.BIN_MODE_HYDRATING);
} else {
// setNewPreset(pond.BinMode.BIN_MODE_DRYING);
setNewBinMode(pond.BinMode.BIN_MODE_DRYING);
}
setOpenModeChange(true)
};
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 = targetMoisture;
//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;
b.settings.inventory.customGrain = newGrainSettings
}
if (b.settings.outdoorTemp !== undefined) {
let t = outdoorTemp;
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
t = FtoC(outdoorTemp);
}
b.settings.outdoorTemp = t;
}
if (b.settings.outdoorHumidity !== undefined)
b.settings.outdoorHumidity = outdoorHumidity;
if (b.settings.mode != newBinMode){
if(b.settings.inventory){
b.settings.inventory.initialTimestamp = moment().format();
}
b.settings.mode = newBinMode;
}
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
refresh();
});
};
return (
<Card raised className={classes.cardContent}>
{cfmLowDialog()}
{cfmHighDialog()}
{changeGrain()}
{storageTimeDialog()}
<ModeChangeDialog
binKey={bin.key()}
binMode={newBinMode}
grain={bin.settings.inventory?.grainType}
devices={devices}
open={openModeChange}
binCables={cables}
presets={binPresets}
compDevMap={componentDevices}
preferences={preferences}
onClose={(refresh) => {
setOpenModeChange(false)
if(refresh) updateBin();
}}
defaultTargetMoisture={bin.settings.inventory?.initialMoisture}
// defaultOutdoorTemp={get this from any ambient sensors}
// defaultOutdoorHumidity={get this from any ambient sensors}
changeTargetMoisture={newMoisture => {
setTargetMoisture(newMoisture)
}}
changeOutdoorTemp={newTemp => {
setOutdoorTemp(newTemp)
}}
changeOutdoorHumidity={newHumidity => {
setOutdoorHumidity(newHumidity)
}}
startChange={() => {
setModeChangeInProgress(true)
}}
changeComplete={() => {
setModeChangeInProgress(false)
}}
/>
<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>
{plenums && pressures && 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>
);
}