merged tidio

This commit is contained in:
Carter 2025-12-15 13:45:19 -06:00
commit f3ec3b9402
14 changed files with 1565 additions and 248 deletions

View file

@ -6,7 +6,40 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title id="title-id">Adaptive Dashboard</title> <title id="title-id">Adaptive Dashboard</title>
<link rel="manifest" id="manifest-link" /> <link rel="manifest" id="manifest-link" />
<style>
#tidio-chat-iframe {
bottom: 0px !important; /* Adjust this value higher number = moved further up from the bottom */
right: 20px !important; /* Optional: tweak horizontal position if needed */
}
/* Optional: Different position on mobile (to avoid overlaps there) */
@media only screen and (max-width: 980px) {
#tidio-chat-iframe {
bottom: 0px !important; /* Example for mobile test and adjust */
}
}
</style>
</head> </head>
<script src="//code.tidio.co/ojd4ex1awaevuymq9oijcmh7yln2pown.js" async></script>
<script>
function onTidioChatApiReady() {
// Move higher on desktop (increase value to move further up, e.g., 100px, 150px)
tidioChatApi.adjustStyles('#tidio { bottom: 0px !important; }');
// Optional: Separate adjustment for mobile (overrides desktop if needed)
tidioChatApi.adjustStyles(
'#tidio { bottom: 0px !important; }',
'@media only screen and (max-width: 980px)'
);
}
// Wait for the widget to be ready
if (window.tidioChatApi) {
tidioChatApi.on('ready', onTidioChatApiReady);
} else {
document.addEventListener('tidioChat-ready', onTidioChatApiReady);
}
</script>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script> <script type="module" src="/src/app/main.tsx"></script>

View file

@ -6,7 +6,40 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title id="title-id">Adaptive Dashboard</title> <title id="title-id">Adaptive Dashboard</title>
<link rel="manifest" id="manifest-link" /> <link rel="manifest" id="manifest-link" />
<style>
#tidio-chat-iframe {
bottom: 0px !important; /* Adjust this value higher number = moved further up from the bottom */
right: 20px !important; /* Optional: tweak horizontal position if needed */
}
/* Optional: Different position on mobile (to avoid overlaps there) */
@media only screen and (max-width: 980px) {
#tidio-chat-iframe {
bottom: 0px !important; /* Example for mobile test and adjust */
}
}
</style>
</head> </head>
<script src="//code.tidio.co/ojd4ex1awaevuymq9oijcmh7yln2pown.js" async></script>
<script>
function onTidioChatApiReady() {
// Move higher on desktop (increase value to move further up, e.g., 100px, 150px)
tidioChatApi.adjustStyles('#tidio { bottom: 0px !important; }');
// Optional: Separate adjustment for mobile (overrides desktop if needed)
tidioChatApi.adjustStyles(
'#tidio { bottom: 0px !important; }',
'@media only screen and (max-width: 980px)'
);
}
// Wait for the widget to be ready
if (window.tidioChatApi) {
tidioChatApi.on('ready', onTidioChatApiReady);
} else {
document.addEventListener('tidioChat-ready', onTidioChatApiReady);
}
</script>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script> <script type="module" src="/src/app/main.tsx"></script>

16
package-lock.json generated
View file

@ -9727,9 +9727,9 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -10953,7 +10953,11 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "version": "1.0.0",
<<<<<<< HEAD
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#80ad2f8b81afe9e04f10f9b52a8250e6824d2b41", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#80ad2f8b81afe9e04f10f9b52a8250e6824d2b41",
=======
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c08965a296f2cd0799472fd8b163186cf6885d4c",
>>>>>>> tidio
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }
@ -13757,9 +13761,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.6", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View file

@ -45,3 +45,15 @@ html, body, #root {
.MuiPopover-root { .MuiPopover-root {
z-index: 1501 !important; z-index: 1501 !important;
} }
#tidio-chat-iframe {
bottom: 100px !important; /* Adjust this value higher number = moved further up from the bottom */
right: 20px !important; /* Optional: tweak horizontal position if needed */
}
/* Optional: Different position on mobile (to avoid overlaps there) */
@media only screen and (max-width: 980px) {
#tidio-chat-iframe {
bottom: 80px !important; /* Example for mobile test and adjust */
}
}

View file

@ -494,7 +494,6 @@ export default function BinSettings(props: Props) {
//if the users preferences are in feet convert the distance to cm otherwise it was entered as cm so use that //if the users preferences are in feet convert the distance to cm otherwise it was entered as cm so use that
form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance form.inventory.lidarDropCm = getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? lidarDropDistance * 30.48 : lidarDropDistance
} }
console.log(form)
binAPI binAPI
.updateBin(bin.key(), form, as) .updateBin(bin.key(), form, as)
.then(response => { .then(response => {

View file

@ -3,14 +3,13 @@ import {
Box, Box,
Button, Button,
Card, Card,
CircularProgress,
darken, darken,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText,
DialogTitle, DialogTitle,
Grid2 as Grid, Grid2 as Grid,
IconButton, IconButton,
InputAdornment,
lighten, lighten,
Link, Link,
Skeleton, Skeleton,
@ -18,8 +17,6 @@ import {
Switch, Switch,
TextField, TextField,
Theme, Theme,
ToggleButton,
ToggleButtonGroup,
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
@ -30,8 +27,8 @@ import HumidityIcon from "component/HumidityIcon";
import TemperatureIcon from "component/TemperatureIcon"; import TemperatureIcon from "component/TemperatureIcon";
import GrainDescriber, { GrainOptions, ToGrainOption } from "grain/GrainDescriber"; import GrainDescriber, { GrainOptions, ToGrainOption } from "grain/GrainDescriber";
import useViewport from "hooks/useViewport"; import useViewport from "hooks/useViewport";
import { cloneDeep, round } from "lodash"; import { round } from "lodash";
import { Bin, Component, Device, Interaction } from "models"; import { Bin, Component, Device } from "models";
import { GetComponentIcon } from "pbHelpers/ComponentType"; import { GetComponentIcon } from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { useMobile } from "hooks"; import { useMobile } from "hooks";
@ -48,6 +45,7 @@ import { useBinAPI } from "providers/pond/binAPI";
import BindaptIcon from "products/Bindapt/BindaptIcon"; import BindaptIcon from "products/Bindapt/BindaptIcon";
import { import {
AccessTime, AccessTime,
ArrowForwardIos,
CheckCircleOutline, CheckCircleOutline,
Error, Error,
InfoOutlined, InfoOutlined,
@ -56,7 +54,6 @@ import {
TrendingUp, TrendingUp,
Warning Warning
} from "@mui/icons-material"; } from "@mui/icons-material";
import DevicePresetsFromPicker from "device/DevicePresetsFromPicker";
import { Plenum } from "models/Plenum"; import { Plenum } from "models/Plenum";
import { Pressure } from "models/Pressure"; import { Pressure } from "models/Pressure";
import { GrainCable } from "models/GrainCable"; import { GrainCable } from "models/GrainCable";
@ -70,8 +67,9 @@ import { Ambient } from "models/Ambient";
import { ExtractMoisture } from "grain"; import { ExtractMoisture } from "grain";
import SearchSelect, { Option } from "common/SearchSelect"; import SearchSelect, { Option } from "common/SearchSelect";
import Edit from "@mui/icons-material/Edit"; import Edit from "@mui/icons-material/Edit";
import { makeStyles, styled } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup"; import ButtonGroup from "common/ButtonGroup";
import ModeChangeDialog from "./conditioning/modeChangeDialog";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ return ({
@ -172,18 +170,21 @@ interface GrainConditions {
emcTrend: number; emcTrend: number;
} }
interface CombinedPlenum {
tempHumidity?: Plenum,
pressure?: Pressure
}
interface Props { interface Props {
bin: Bin; bin: Bin;
loading: boolean; loading: boolean;
components: Map<string, Component>; components: Map<string, Component>;
componentDevices?: Map<string, number>; componentDevices?: Map<string, number>;
devices: Device[]; devices: Device[];
plenum?: Plenum; plenums?: Plenum[];
ambient?: Ambient; ambient?: Ambient;
cables?: GrainCable[]; cables?: GrainCable[];
pressure?: Pressure; pressures?: Pressure[];
interactions?: Interaction[];
//changeBinMode: (binMode: pond.BinMode) => boolean;
refresh: (showSnack?: boolean) => void; refresh: (showSnack?: boolean) => void;
afterUpdate?(): void; afterUpdate?(): void;
preferences?: Map<string, pond.BinComponentPreferences>; preferences?: Map<string, pond.BinComponentPreferences>;
@ -195,11 +196,9 @@ interface Props {
export default function BinVisualizer(props: Props) { export default function BinVisualizer(props: Props) {
const { const {
bin, bin,
//changeBinMode, plenums,
//changeGrainAmount,
plenum,
ambient, ambient,
pressure, pressures,
loading, loading,
cables, cables,
components, components,
@ -207,7 +206,6 @@ export default function BinVisualizer(props: Props) {
preferences, preferences,
componentDevices, componentDevices,
permissions, permissions,
//interactions,
refresh, refresh,
updateComponentCallback, updateComponentCallback,
binPresets binPresets
@ -233,10 +231,10 @@ export default function BinVisualizer(props: Props) {
).colour(); ).colour();
const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour(); const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour();
const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour(); const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour();
const [showInputMoisture, setShowInputMoisture] = useState<boolean>(false); // const [showInputMoisture, setShowInputMoisture] = useState<boolean>(false);
const [moistureInput, setMoistureInput] = useState<string>(""); const [targetMoisture, setTargetMoisture] = useState(0);
const [tempInput, setTempInput] = useState<string>(""); const [outdoorTemp, setOutdoorTemp] = useState(0);
const [outdoorHumidityInput, setOutdoorHumidityInput] = useState<string>(""); const [outdoorHumidity, setOutdoorHumidity] = useState(0);
const [modeTime, setModeTime] = useState<Moment>(moment()); const [modeTime, setModeTime] = useState<Moment>(moment());
const [grainUpdate, setGrainUpdate] = useState<boolean>(false); const [grainUpdate, setGrainUpdate] = useState<boolean>(false);
const [openStorageTime, setOpenStorageTime] = useState(false); const [openStorageTime, setOpenStorageTime] = useState(false);
@ -249,7 +247,7 @@ export default function BinVisualizer(props: Props) {
const [cfmLowOpen, setCFMLowOpen] = useState(false); const [cfmLowOpen, setCFMLowOpen] = useState(false);
const [cfmHighOpen, setCFMHighOpen] = useState(false); const [cfmHighOpen, setCFMHighOpen] = useState(false);
const [{ user }] = useGlobalState(); const [{ user }] = useGlobalState();
const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE); // const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [newBinMode, setNewBinMode] = 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 [selectedCable, setSelectedCable] = useState<GrainCable>();
const [openNodeDialog, setOpenNodeDialog] = useState(false); const [openNodeDialog, setOpenNodeDialog] = useState(false);
@ -260,7 +258,7 @@ export default function BinVisualizer(props: Props) {
//const [highTempCable, setHighTempCable] = useState<GrainCable>(); //const [highTempCable, setHighTempCable] = useState<GrainCable>();
//const [lowTempCable, setLowTempCable] = useState<GrainCable>(); //const [lowTempCable, setLowTempCable] = useState<GrainCable>();
//the switch value to determine what to display in the grain condition box //the switch value to determine what to display in the grain condition box
const [valueDisplay, setValueDisplay] = useState<"low" | "avg" | "high">("avg"); 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 //the variables to be able to change the grain type through a unique dialog outside of the settings
const [grainChangeDialog, setGrainChangeDialog] = useState(false); const [grainChangeDialog, setGrainChangeDialog] = useState(false);
const grainOptions = GrainOptions(); const grainOptions = GrainOptions();
@ -282,65 +280,47 @@ export default function BinVisualizer(props: Props) {
const [storageDate, setStorageDate] = useState(moment().format("YYYY-MM-DD")); const [storageDate, setStorageDate] = useState(moment().format("YYYY-MM-DD"));
const [storageTime, setStorageTime] = useState(moment().format("HH:mm")); const [storageTime, setStorageTime] = useState(moment().format("HH:mm"));
// const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({ const [activePlenum, setActivePlenum] = useState<CombinedPlenum | undefined>()
// root: { const [activePlenumIndex, setActivePlenumIndex] = useState(0)
// backgroundColor: "transparent", const [combinedPlenums, setCombinedPlenums] = useState<CombinedPlenum[]>([])
// 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 }) => ({ const [openModeChange, setOpenModeChange] = useState(false)
// grouped: { const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
// 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(() => { useEffect(() => {
setModeTime(moment(bin.status.lastModeChange)); setModeTime(moment(bin.status.lastModeChange));
}, [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(() => { useEffect(() => {
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN); setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
setStorageType(bin.storage()); setStorageType(bin.storage());
setNewPreset(pond.BinMode.BIN_MODE_NONE); // setNewPreset(pond.BinMode.BIN_MODE_NONE);
if (bin.settings.inventory) { if (bin.settings.inventory) {
setMoistureInput(bin.settings.inventory.initialMoisture.toString()); setTargetMoisture(bin.settings.inventory.initialMoisture);
setCustomTypeName(bin.settings.inventory.customTypeName); setCustomTypeName(bin.settings.inventory.customTypeName);
setGrainType(bin.settings.inventory.grainType); setGrainType(bin.settings.inventory.grainType);
setGrainOption(ToGrainOption(bin.settings.inventory.grainType)); setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
@ -353,9 +333,9 @@ export default function BinVisualizer(props: Props) {
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
t = CtoF(t); t = CtoF(t);
} }
setTempInput(t.toString()); setOutdoorTemp(t);
} }
if (bin.settings) setOutdoorHumidityInput(bin.settings.outdoorHumidity.toString()); if (bin.settings) setOutdoorHumidity(bin.settings.outdoorHumidity);
}, [bin, user]); }, [bin, user]);
useEffect(() => { useEffect(() => {
@ -391,20 +371,6 @@ export default function BinVisualizer(props: Props) {
humids.push(...filteredNodes.humids) humids.push(...filteredNodes.humids)
emcs.push(...filteredNodes.moistures) 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 //add the trend data to the proper arrays if the top node is set
let cableTrendData = binTrend.cableTrend[cable.key()]; let cableTrendData = binTrend.cableTrend[cable.key()];
@ -418,7 +384,6 @@ export default function BinVisualizer(props: Props) {
//determine which node is the coldest so that the data displayed is for the same node //determine which node is the coldest so that the data displayed is for the same node
let lowTempIndex = let lowTempIndex =
filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)); filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps));
console.log(lowTempIndex)
lowNodeConditions = { lowNodeConditions = {
tempC: filteredNodes.temps[lowTempIndex], tempC: filteredNodes.temps[lowTempIndex],
humidity: filteredNodes.humids[lowTempIndex], humidity: filteredNodes.humids[lowTempIndex],
@ -798,7 +763,7 @@ export default function BinVisualizer(props: Props) {
setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN); setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN);
setStorageType(bin.storage()); setStorageType(bin.storage());
if (bin.settings.inventory) { if (bin.settings.inventory) {
setMoistureInput(bin.settings.inventory.initialMoisture.toString()); setTargetMoisture(bin.settings.inventory.initialMoisture);
setCustomTypeName(bin.settings.inventory.customTypeName); setCustomTypeName(bin.settings.inventory.customTypeName);
setGrainType(bin.settings.inventory.grainType); setGrainType(bin.settings.inventory.grainType);
setGrainOption(ToGrainOption(bin.settings.inventory.grainType)); setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
@ -1094,14 +1059,28 @@ export default function BinVisualizer(props: Props) {
const plenumOverview = () => { const plenumOverview = () => {
return ( return (
<Box style={{ position: "absolute", bottom: 5, width: "100%" }}> <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 }}> <Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Plenum Plenum {plenums && plenums?.length > 1 ? activePlenumIndex+1 : ""}
</Typography> </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 <Box
className={classes.displayBoxBinLeft} className={classes.displayBoxBinLeft}
style={{ marginRight: isMobile ? -70 : -90 }} style={{ marginRight: isMobile ? -70 : -90 }}
position="relative"> position="relative">
{plenum ? ( {activePlenum?.tempHumidity ? (
<React.Fragment> <React.Fragment>
<Grid <Grid
container container
@ -1129,7 +1108,7 @@ export default function BinVisualizer(props: Props) {
fontWeight: 650, fontWeight: 650,
color: tempColour color: tempColour
}}> }}>
{plenum.getTempString(user.settings.temperatureUnit)} {activePlenum.tempHumidity.getTempString(user.settings.temperatureUnit)}
</Typography> </Typography>
</Grid> </Grid>
<Grid size={{ xs: 5 }}> <Grid size={{ xs: 5 }}>
@ -1141,7 +1120,7 @@ export default function BinVisualizer(props: Props) {
fontWeight: 650, fontWeight: 650,
color: humidColour color: humidColour
}}> }}>
{plenum.getHumidityString()} {activePlenum.tempHumidity.getHumidityString()}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -1262,7 +1241,7 @@ export default function BinVisualizer(props: Props) {
style={{ height: isMobile ? 20 : 25, width: isMobile ? 20 : 25 }} style={{ height: isMobile ? 20 : 25, width: isMobile ? 20 : 25 }}
/> />
</Grid> </Grid>
{pressure ? ( {activePlenum?.pressure ? (
<React.Fragment> <React.Fragment>
<Grid size={{ xs: 5 }}> <Grid size={{ xs: 5 }}>
<Typography <Typography
@ -1273,7 +1252,7 @@ export default function BinVisualizer(props: Props) {
fontWeight: 650, fontWeight: 650,
color: pressColour color: pressColour
}}> }}>
{pressure.getPressureString(user.settings.pressureUnit)} {activePlenum.pressure.getPressureString(user.settings.pressureUnit)}
</Typography> </Typography>
</Grid> </Grid>
{/* <Grid item xs={5}> {/* <Grid item xs={5}>
@ -1600,7 +1579,7 @@ export default function BinVisualizer(props: Props) {
<Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}> <Typography style={{ fontSize: isMobile ? "0.85rem" : "1.0rem", fontWeight: 650 }}>
Fan Performance Fan Performance
</Typography> </Typography>
{pressure && pressure.fanId === 0 && !bin.settings.fan?.type && bin.fanID() === 0 && ( {activePlenum?.pressure && activePlenum.pressure.fanId === 0 && !bin.settings.fan?.type && bin.fanID() === 0 && (
<Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 600 }}> <Typography variant="subtitle2" color="textPrimary" style={{ fontWeight: 600 }}>
No fans found to calculate CFM No fans found to calculate CFM
</Typography> </Typography>
@ -1642,8 +1621,8 @@ export default function BinVisualizer(props: Props) {
<Typography align="center" style={{ fontWeight: 650, color: emcColour }}> <Typography align="center" style={{ fontWeight: 650, color: emcColour }}>
{ExtractMoisture( {ExtractMoisture(
bin.grain(), bin.grain(),
plenum?.temperature ?? 0, activePlenum?.tempHumidity?.temperature ?? 0,
plenum?.humidity ?? 0 activePlenum?.tempHumidity?.humidity ?? 0
).toFixed(2)} ).toFixed(2)}
% %
</Typography> </Typography>
@ -1745,7 +1724,11 @@ export default function BinVisualizer(props: Props) {
<Typography variant="subtitle1" style={{ fontWeight: 800 }}> <Typography variant="subtitle1" style={{ fontWeight: 800 }}>
Bin Mode Bin Mode
</Typography> </Typography>
{modeChangeInProgress &&
<CircularProgress color="primary" size={25}/>
}
<ButtonGroup <ButtonGroup
disableAll={modeChangeInProgress}
buttons={[ buttons={[
{ {
title: "Storage", title: "Storage",
@ -1762,48 +1745,14 @@ export default function BinVisualizer(props: Props) {
{ {
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying", title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => { function: () => {
setShowInputMoisture(true) // setShowInputMoisture(true)
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
} }
} }
]} ]}
toggledButtons={determineToggle(mode)} toggledButtons={determineToggle(mode)}
toggle 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> </Box>
); );
}; };
@ -1972,106 +1921,32 @@ export default function BinVisualizer(props: Props) {
}; };
const setModeStorage = () => { const setModeStorage = () => {
setNewPreset(pond.BinMode.BIN_MODE_STORAGE); // setNewPreset(pond.BinMode.BIN_MODE_STORAGE);
setNewBinMode(pond.BinMode.BIN_MODE_STORAGE); setNewBinMode(pond.BinMode.BIN_MODE_STORAGE);
setOpenModeChange(true)
}; };
const setModeCooldown = () => { const setModeCooldown = () => {
if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) { if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) {
return; return;
} }
setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN); // setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN);
setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN); setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN);
setOpenModeChange(true)
}; };
const setModeDrying = () => { const setModeConditioning = () => {
if ( if (
bin.settings.inventory && bin.settings.inventory &&
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture
) { ) {
setNewPreset(pond.BinMode.BIN_MODE_HYDRATING); // setNewPreset(pond.BinMode.BIN_MODE_HYDRATING);
setNewBinMode(pond.BinMode.BIN_MODE_HYDRATING); setNewBinMode(pond.BinMode.BIN_MODE_HYDRATING);
} else { } else {
setNewPreset(pond.BinMode.BIN_MODE_DRYING); // setNewPreset(pond.BinMode.BIN_MODE_DRYING);
setNewBinMode(pond.BinMode.BIN_MODE_DRYING); setNewBinMode(pond.BinMode.BIN_MODE_DRYING);
} }
}; setOpenModeChange(true)
const closeMoistureDialog = () => {
setNewPreset(0);
setNewBinMode(bin.settings.mode);
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);
setNewBinMode(bin.settings.mode);
setShowInputMoisture(false);
}}
color="primary">
Cancel
</Button>
<Button onClick={setModeDrying} color="primary">
Update
</Button>
</DialogActions>
</ResponsiveDialog>
);
}; };
if (loading) { if (loading) {
@ -2083,7 +1958,7 @@ export default function BinVisualizer(props: Props) {
b.settings.storage = storageType; b.settings.storage = storageType;
if (b.settings.inventory) { if (b.settings.inventory) {
if (b.settings.inventory.initialMoisture) if (b.settings.inventory.initialMoisture)
b.settings.inventory.initialMoisture = Number(moistureInput); b.settings.inventory.initialMoisture = targetMoisture;
//update all the grain stuff //update all the grain stuff
b.settings.inventory.grainType = grainType; b.settings.inventory.grainType = grainType;
b.settings.inventory.customTypeName = customTypeName; b.settings.inventory.customTypeName = customTypeName;
@ -2094,14 +1969,14 @@ export default function BinVisualizer(props: Props) {
} }
if (b.settings.outdoorTemp !== undefined) { if (b.settings.outdoorTemp !== undefined) {
let t = Number(tempInput); let t = outdoorTemp;
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
t = FtoC(Number(tempInput)); t = FtoC(outdoorTemp);
} }
b.settings.outdoorTemp = t; b.settings.outdoorTemp = t;
} }
if (b.settings.outdoorHumidity !== undefined) if (b.settings.outdoorHumidity !== undefined)
b.settings.outdoorHumidity = Number(outdoorHumidityInput); b.settings.outdoorHumidity = outdoorHumidity;
if (b.settings.mode != newBinMode){ if (b.settings.mode != newBinMode){
if(b.settings.inventory){ if(b.settings.inventory){
@ -2117,25 +1992,43 @@ export default function BinVisualizer(props: Props) {
return ( return (
<Card raised className={classes.cardContent}> <Card raised className={classes.cardContent}>
{moistureDialog()}
{cfmLowDialog()} {cfmLowDialog()}
{cfmHighDialog()} {cfmHighDialog()}
{changeGrain()} {changeGrain()}
{storageTimeDialog()} {storageTimeDialog()}
<DevicePresetsFromPicker <ModeChangeDialog
preferences={preferences}
binKey={bin.key()} binKey={bin.key()}
devices={devices} binMode={newBinMode}
refreshCallback={success => {
setShowInputMoisture(false);
setNewPreset(0)
if (success) updateBin();
}}
parentPreset={newPreset}
grain={bin.settings.inventory?.grainType} grain={bin.settings.inventory?.grainType}
devices={devices}
open={openModeChange}
binCables={cables} binCables={cables}
compDevMap={componentDevices}
presets={binPresets} 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}> <Box paddingRight={1}>
{binMode()} {binMode()}
@ -2148,7 +2041,7 @@ export default function BinVisualizer(props: Props) {
</Grid> </Grid>
<Grid size={1.5} >{controls()}</Grid> <Grid size={1.5} >{controls()}</Grid>
</Grid> </Grid>
{plenum && pressure && fanPerformance()} {plenums && pressures && fanPerformance()}
{ambient && ambientDisplay()} {ambient && ambientDisplay()}
{(bin.status.fans.length > 0 || bin.status.heaters.length > 0) && controllerDisplay()} {(bin.status.fans.length > 0 || bin.status.heaters.length > 0) && controllerDisplay()}
{dryingEstimate()} {dryingEstimate()}

View file

@ -0,0 +1,90 @@
import { Typography } from "@mui/material"
import { Ambient } from "models/Ambient"
import { Controller } from "models/Controller"
import { Plenum } from "models/Plenum"
import React, { useState } from "react"
import PickComponentSet, { ComponentSet } from "./pickComponentSet"
import { cloneDeep } from "lodash"
import { DevicePreset } from "models/DevicePreset"
import { pond } from "protobuf-ts/pond"
interface Props {
plenums: Plenum[]
ambients: Ambient[]
heaters: Controller[]
fans: Controller[]
binMode: pond.BinMode
presets?: DevicePreset[]
updateSets: (sets: ComponentSet[]) => void
}
export default function ConditioningSelector(props: Props){
const {plenums, ambients, heaters, fans, updateSets, presets, binMode} = props
const [currentSets, setCurrentSets] = useState<ComponentSet[]>([])
const sensorIndex = (key: string) => {
let index = -1
currentSets.forEach((set, i) => {
if(set.sensor.key() === key){
index = i
}
})
return index
}
const setUpdate = (key: string, set?: ComponentSet) => {
let sets = cloneDeep(currentSets)
let index = sensorIndex(key)
//if the set is defined, add it to the existing sets as long as it doesn't already exist
if(set){
//check if there is a set that has a sensor with the key
if(index !== -1){
sets[index] = set //replace it with the new set
}else{//if the set was not found ie is new
sets.push(set)//add the new set
}
}else{ //if the set is undefined
if (index !== -1){
sets.splice(index, 1)//remove it from the sets
}
//no else is needed because if it doesn't exist nothing need be done
}
//once the sets are finished being changed pas that up to the parent via the prop function
setCurrentSets(sets)
updateSets(sets)
}
return (
<React.Fragment>
{(plenums.length > 0 || ambients.length > 0) &&
<Typography>Select the sensors you would like to add an interaction to</Typography>
}
{plenums.map(plenum => {
return (
<PickComponentSet
key={plenum.key()}
sensor={plenum}
heaters={heaters}
fans={fans}
binMode={binMode}
presets={presets}
updateSet={setUpdate}
/>
)})}
{ambients.map(ambient => {
return (
<PickComponentSet
key={ambient.key()}
sensor={ambient}
heaters={heaters}
fans={fans}
binMode={binMode}
presets={presets}
updateSet={setUpdate}
/>
)})}
</React.Fragment>
)
}

View file

@ -0,0 +1,846 @@
import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Stepper, Step, StepLabel, InputAdornment, Typography, useTheme } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useComponentAPI, useInteractionsAPI } from "hooks";
import { Component, Device, Interaction } from "models";
import { Ambient } from "models/Ambient";
import { Controller } from "models/Controller";
import { DevicePreset } from "models/DevicePreset";
import { GrainCable } from "models/GrainCable";
import { Plenum } from "models/Plenum";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState } from "providers";
import React from "react";
import { useEffect, useState } from "react";
import ConditioningSelector from "./conditioningSelector";
import CableTopNodeSummary from "bin/CableTopNodeSummary";
import { ComponentSet } from "./pickComponentSet";
import { sameComponentID } from "pbHelpers/Component";
import moment from "moment";
import { lowerCase } from "lodash";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { fahrenheitToCelsius, getTemperatureUnit } from "utils";
import { GetGrainExtensionMap } from "grain";
import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress";
interface Props {
open: boolean
onClose: (refresh: boolean) => void
binKey: string;
devices: Device[];
binMode: pond.BinMode;
grain?: pond.Grain;
preferences?: Map<string, pond.BinComponentPreferences>;
binCables?: GrainCable[];
compDevMap?: Map<string, number>;
presets?: DevicePreset[];
startChange: () => void;
changeComplete: () => void;
defaultTargetMoisture?: number;
changeTargetMoisture?: (newTarget: number) => void
defaultOutdoorTemp?: number;
changeOutdoorTemp?: (newTemp: number) => void
defaultOutdoorHumidity?: number;
changeOutdoorHumidity?: (newHumidity: number) => void
}
interface Option {
label: string;
device: Device;
icon?: string;
}
interface DialogStep {
label: string;
completed?: boolean;
}
export default function ModeChangeDialog(props: Props){
const {
open,
onClose,
binMode,
devices,
binKey,
preferences,
binCables,
compDevMap,
grain,
startChange,
changeComplete,
defaultTargetMoisture,
defaultOutdoorTemp,
defaultOutdoorHumidity,
changeTargetMoisture,
changeOutdoorTemp,
changeOutdoorHumidity,
presets
} = props
const [{as}] = useGlobalState()
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
const interactionAPI = useInteractionsAPI()
const componentAPI = useComponentAPI();
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
const [loading, setLoading] = useState(false)
const [options, setOptions] = useState<Option[]>([])
const [deviceComponents, setDeviceComponents] = useState<Map<string, Component[]>>(
new Map<string, Component[]>()
);
const [plenums, setPlenums] = useState<Plenum[]>([])
const [ambients, setAmbients] = useState<Ambient[]>([])
const [heaters, setHeaters] = useState<Controller[]>([])
const [fans, setFans] = useState<Controller[]>([])
const grainExtensionMap = GetGrainExtensionMap();
const [conflictingSetInteractions, setConflictingSetInteractions] = useState<Interaction[]>([])
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
const [promiseStages, setPromiseStages] = useState<Stage[]>([])
const [dialogSteps, setDialogSteps] = useState<DialogStep[]>([])
const [currentStep, setCurrentStep] = useState(0)
const theme = useTheme()
//the state variables for the moisture
const [moistureInput, setMoistureInput] = useState<string>("")
const [outdoorTempInput, setOutdoorTempInput] = useState<string>("")
const [outdoorHumidityInput, setOutdoorHumidityInput] = useState<string>("")
const [deviceOption, setDeviceOption] = useState<Option>()
useEffect(()=>{
setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0")
setOutdoorTempInput(defaultOutdoorTemp?.toFixed(2) ?? "0")
setOutdoorHumidityInput(defaultOutdoorHumidity?.toFixed(2) ?? "0")
},[defaultTargetMoisture, defaultOutdoorTemp, defaultOutdoorHumidity])
//get the interactions and components for the device when it is selected from the dropdown
useEffect(()=>{
if (!binKey) return;
if (!selectedDevice) return
let comps = deviceComponents.get(selectedDevice.id().toString());
if (!comps) {
comps = [];
setLoading(true);
let interactionPromise = interactionAPI.listInteractionsByDevice(selectedDevice.id(), undefined, as);
let componentPromise = componentAPI.list(selectedDevice.id(), undefined, [binKey], ["bin"], true, as);
let dComponents = new Map<string, Component[]>();
Promise.all([componentPromise, interactionPromise])
.then(([compResp, interactionResp]) => {
let d = pond.ListComponentsResponse.fromObject(compResp.data);
d.components.forEach(comp => {
let c = Component.create(comp);
let deviceNumber = compResp.data.componentDevices[c.key()];
comps!.push(c);
let newDComponents = dComponents.get(deviceNumber.toString());
if (newDComponents === undefined) newDComponents = [];
newDComponents.push(c);
dComponents.set(deviceNumber.toString(), newDComponents);
});
setDeviceComponents(dComponents);
setExistingInteractions(interactionResp);
})
.finally(() => {
setLoading(false);
});
}
},[selectedDevice, binKey, componentAPI, interactionAPI, deviceComponents, as])
//sort the components according to the bin preferences
useEffect(()=>{
if (loading) return;
if (!selectedDevice) return;
if (!deviceComponents.get(selectedDevice.id().toString())) return;
var plenums: Plenum[] = [];
var ambients: Ambient[] = [];
//var grainCables: GrainCable[] = [];
var heaters: Controller[] = [];
var fans: Controller[] = [];
deviceComponents.get(selectedDevice.id().toString())!.forEach(comp => {
let pref = preferences?.get(comp.key());
if (pref) {
if (pref.type) {
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM)
plenums.push(Plenum.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT)
ambients.push(Ambient.create(comp));
// if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE)
// grainCables.push(GrainCable.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) {
let heater = Controller.create(comp);
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
heaters.push(heater);
}
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
let fan = Controller.create(comp)
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
fans.push(fan);
}
}
}
});
setPlenums(plenums);
setAmbients(ambients);
setHeaters(heaters);
setFans(fans);
}, [loading, setPlenums, setHeaters, deviceComponents, selectedDevice, preferences]);
useEffect(() => {
let o: Option[] = [];
devices.forEach(device => {
let newOption: Option = {device: device, label: device.name()}
if(deviceOption === undefined) {
setDeviceOption(newOption)
setSelectedDevice(device)
}
o.push(newOption);
});
setOptions(o);
}, [devices, setOptions]);
//use effect here to determine the steps based on the bin mode
useEffect(() => {
let steps: DialogStep[] = []
switch(binMode){
case pond.BinMode.BIN_MODE_STORAGE:
steps.push({
label: "Storage Settings"
})
break;
case pond.BinMode.BIN_MODE_COOLDOWN:
steps.push({
label: "Set Components"
})
steps.push({
label: "Progress"
})
break;
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_HYDRATING:
steps.push({
label: "Weather Conditions"
})
steps.push({
label: "Set Components"
})
steps.push({
label: "Progress"
})
}
setDialogSteps(steps)
},[binMode])
const buildHeaterInteraction = (
sensor: Plenum | Ambient,
heater: Controller,
customPreset?: DevicePreset
): pond.InteractionSettings => {
let interaction = pond.InteractionSettings.create({
source: sensor.location(),
sink: heater.location(),
schedule: pond.InteractionSchedule.create({
timeOfDayStart: "00:00",
timeOfDayEnd: "24:00",
timezone: moment.tz.guess(),
weekdays: moment.weekdays().map(d => lowerCase(d))
}),
notifications: pond.InteractionNotifications.create({
reports: true
}),
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
value: 1
})
});
let temp = 0;
let hum = 0;
//if a preset was selected use its temp/hum values
if (customPreset !== undefined) {
temp = customPreset.temp();
hum = customPreset.hum();
} else {
//otherwise use default values
switch(binMode){
case pond.BinMode.BIN_MODE_COOLDOWN:
temp = 10
hum = 45
break;
case pond.BinMode.BIN_MODE_DRYING:
if(grain){
//get the values using the grain type
let ext = grainExtensionMap.get(grain)
if (ext) {
temp = ext.setTempC
hum = ext.targetMC
}
}else{//otherwise use the default drying interaction
temp = 40
hum = 20
}
}
}
let conditions = [];
let humidityCondition = pond.InteractionCondition.create({
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: Math.round(
describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
sensor.settings.type,
sensor.settings.subtype
).toStored(hum)
)
});
conditions.push(humidityCondition);
//since the measurement describers function to stored does a conversion into celsius if the users pref is fahrenheit for temperature we need to convert the preset value into fahrenheit
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
temp = Math.round((temp * (9 / 5) + 32) * 100) / 100;
}
let tempVal = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
sensor.settings.type,
sensor.settings.subtype
).toStored(temp);
let tempCondition = pond.InteractionCondition.create({
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: Math.round(tempVal) //and then round the converted value since interaction conditions wont take floats
});
conditions.push(tempCondition);
interaction.conditions = conditions;
//the the heater output to auto so that when the components are update it uses the new mode
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
return interaction;
};
const buildFanInteraction = (
sensor: Plenum | Ambient,
fan: Controller,
tempComparison: "greater" | "less",
humidityComparison: "greater" | "less",
customPreset?: DevicePreset
) => {
let interaction = pond.InteractionSettings.create({
source: sensor.location(),
sink: fan.location(),
schedule: pond.InteractionSchedule.create({
timeOfDayStart: "00:00",
timeOfDayEnd: "24:00",
timezone: moment.tz.guess(),
weekdays: moment.weekdays().map(d => lowerCase(d))
}),
notifications: pond.InteractionNotifications.create({
reports: true
}),
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
value: 1
})
});
let tempPreset = 0;
let humidityPreset = 0;
//if a custom preset was selected use it
if (customPreset !== undefined) {
tempPreset = customPreset.temp();
humidityPreset = customPreset.hum();
} else {
//otherwise use one of the defaults
switch (binMode) {
case pond.BinMode.BIN_MODE_COOLDOWN:
tempPreset = 15;
humidityPreset = 60;
break;
case pond.BinMode.BIN_MODE_DRYING:
if(grain){
//get the values using the grain type
let ext = grainExtensionMap.get(grain)
if (ext) {
tempPreset = ext.setTempC
humidityPreset = ext.targetMC
}
}else{//otherwise use the default drying interaction
tempPreset = 40
humidityPreset = 20
}
break;
case pond.BinMode.BIN_MODE_HYDRATING:
tempPreset = 25;
humidityPreset = 60;
break;
}
}
let conditions = [];
let fanConditionOne = pond.InteractionCondition.create({
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
comparison:
humidityComparison === "greater"
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
sensor.settings.type,
sensor.settings.subtype
).toStored(humidityPreset)
});
conditions.push(fanConditionOne);
//since the measurement describers function toStored does a conversion into celsius for temperature if the users pref is fahrenheit we need to convert the preset value into fahrenheit
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
tempPreset = Math.round((tempPreset * (9 / 5) + 32) * 100) / 100;
}
let tempVal = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
sensor.settings.type,
sensor.settings.subtype
).toStored(tempPreset);
let fanConditionTwo = pond.InteractionCondition.create({
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
comparison:
tempComparison === "greater"
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
});
conditions.push(fanConditionTwo);
interaction.conditions = conditions;
//set the output mode to auto in the fan so that when the components are updated it uses the new mode
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
return interaction;
};
//this is what will use the conflicting interactions
const buildPromises = () => {
//if there is no device selected or there are no interactions to add, do nothing
if(!selectedDevice || !interactionsToAdd) return
//build the stages to pass into the PromiseProgress
//stage one is to remove conflicting interactions
let stages: Stage[] = []
let stage1: Stage = {
title: "Remove Conflicting Interactions",
steps: []
}
conflictingSetInteractions.forEach(i => {
let newStep: PromiseStep = {
title: "Removing Interaction",
promise: interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as)
}
stage1.steps.push(newStep);
});
stages.push(stage1)
//stage two is to add the new interactions
let stage2: Stage = {
title: "Add New Interactions",
steps: [
{
title: "Adding Interactions",
promise: interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd)
}
]
}
stages.push(stage2)
//stage three is to update the controller components
let stage3: Stage = {
title: "Update Controllers",
steps: []
}
componentSets.forEach(set => {
set.controllers.forEach(controller => {
let newStep: PromiseStep = {
title: "Updating " + controller.name(),
promise: componentAPI.update(selectedDevice.id(), controller.settings)
}
stage3.steps.push(newStep)
})
})
stages.push(stage3)
//set those stages to a state variable
setPromiseStages(stages)
}
//this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
const createInteractions = (sets: ComponentSet[]) => {
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
let conflictingInteractions: Interaction[] = []
let linkedComponents = deviceComponents.get(selectedDevice.id().toString());
//loop through the sets
sets.forEach((set) => {
//loop through the controllers
set.controllers.forEach(controller => {
let customPreset = set.selectedPresets.get(controller.key())
//filter the conflicting interactions for that controller
let c = existingInteractions.filter(i => {
let conflicting = false;
if (linkedComponents) {
linkedComponents.forEach(comp => {
if (sameComponentID(comp.location(), i.settings.sink)) {
conflicting = true;
}
});
}
return conflicting;
});
conflictingInteractions = conflictingInteractions.concat(c)
switch(binMode){
case pond.BinMode.BIN_MODE_COOLDOWN:
// if the controller is a heater create heater interaction
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller, customPreset))
}else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){
// if the controller is a fan create a fan interaction
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "less", customPreset))
}
break;
case pond.BinMode.BIN_MODE_DRYING:
// if the controller is a heater create heater interaction
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller, customPreset))
}else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){// if the controller is a fan
//if it is a combination set using a heater as well just turn the fan on
if(set.combo){
controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON
}else{
//otherwise make a fan interaction
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less", customPreset))
}
}
break;
case pond.BinMode.BIN_MODE_HYDRATING:
//check to make sure the controller is a fan, you cant hydrate with a heater, if the controller is a heater then it will not add an interaction
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){
//create fan interaction
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "greater", customPreset))
}
break;
}
})
})
setComponentSets([...sets])
return {
conflicting: conflictingInteractions,
toAdd: multiInteractionSettings
}
}
const deviceSelector = () => {
return (
<Autocomplete
disablePortal
options={options}
value={deviceOption}
fullWidth
getOptionLabel={option => option.label || ""}
onChange={(_, newValue) => {
if(newValue){
setSelectedDevice(newValue.device);
}
}}
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
/>
)
}
const modeDescription = () => {
switch(binMode){
case pond.BinMode.BIN_MODE_DRYING:
return "Drying"
case pond.BinMode.BIN_MODE_HYDRATING:
return "Hydrating"
case pond.BinMode.BIN_MODE_COOLDOWN:
return "Cooldown"
case pond.BinMode.BIN_MODE_STORAGE:
return "Storage"
default:
return ""
}
}
const close = (refresh: boolean) => {
//clear state data
setCurrentStep(0)
setInteractionsToAdd(undefined)
setPromiseStages([])
onClose(refresh)
}
//this step will only be used for drying and hydrating
const moistureStep = () => {
return (
<Box>
<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}
error={isNaN(parseFloat(moistureInput))}
helperText={isNaN(parseFloat(moistureInput)) ? "Must be a valid number" : ""}
onChange={event => {
setMoistureInput(event.target.value)
if(changeTargetMoisture && !isNaN(parseFloat(event.target.value))){
changeTargetMoisture(parseFloat(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={outdoorTempInput}
error={isNaN(parseFloat(outdoorTempInput))}
helperText={isNaN(parseFloat(outdoorTempInput)) ? "Must be a valid number" : ""}
onChange={event => {
setOutdoorTempInput(event.target.value)
if(changeOutdoorTemp && !isNaN(parseFloat(event.target.value))){
//if the users prefs are F will need to convert it to C when sending it back
let val = parseFloat(event.target.value)
if(getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT){
val = fahrenheitToCelsius(val)
}
changeOutdoorTemp(val)
}
}}
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}
error={isNaN(parseFloat(outdoorHumidityInput))}
helperText={isNaN(parseFloat(outdoorHumidityInput)) ? "Must be a valid number" : ""}
onChange={event => {
setOutdoorHumidityInput(event.target.value)
if(changeOutdoorHumidity && !isNaN(parseFloat(event.target.value))){
changeOutdoorHumidity(parseFloat(event.target.value))
}
}}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>
}}
style={{ marginTop: theme.spacing(2) }}
fullWidth
variant="outlined"
/>
</Box>
)
};
const selectDeviceStep = () => {
switch(binMode){
case pond.BinMode.BIN_MODE_STORAGE:
return (
<Box>
{binCables && binCables?.length > 0 && compDevMap && (
<Box marginBottom={2}>
<CableTopNodeSummary
binKey={binKey}
cables={binCables}
componentDevMap={compDevMap}
/>
</Box>
)}
<DialogContentText>
Select device to turn off controllers and end conditioning. Any interactions will be
left alone. Heaters/Fans will be turned off.
</DialogContentText>
{deviceSelector()}
</Box>
)
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_COOLDOWN:
case pond.BinMode.BIN_MODE_HYDRATING:
return (
<Box>
<DialogContentText>
{modeDescription()} control is handled by interactions between multiple components on the
same device. Please choose which device and components will handle this bin's{" "}
{modeDescription()}.
</DialogContentText>
{deviceSelector()}
<ConditioningSelector
plenums={plenums}
ambients={ambients}
fans={fans}
heaters={heaters}
presets={presets}
binMode={binMode}
updateSets={(sets) => {
//update the component sets that will be used for the interactions
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
let i = createInteractions(sets)
if(i !== undefined){
setConflictingSetInteractions(i.conflicting)
setInteractionsToAdd(i.toAdd)
}
}}
/>
</Box>
)
}
}
const progressStep = () => {
return (
<Box>
<PromiseProgress
stages={promiseStages}
automaticStart
failFast
onStart={() => {
startChange()
}}
onComplete={()=>{
close(true)
setCurrentStep(0)//reset the stepper
changeComplete()//change the boolean preventing more mode changes
}}
/>
</Box>
)
}
const stepper = () => {
return (
<Stepper nonLinear activeStep={currentStep} style={{ width: "100%", marginBottom: theme.spacing(2) }}>
{dialogSteps.map((dialogStep, i) => {
const labelProps: {
optional?: React.ReactNode;
} = {};
return (
<Step key={i} completed={dialogStep.completed}>
<StepLabel {...labelProps}>{dialogStep.label}</StepLabel>
</Step>
);
})}
</Stepper>
);
}
const actions = () => {
let buttons: JSX.Element[] = []
//this button should be on every step of all of them
buttons.push(
<Button key={"close"} onClick={() => {close(false)}}>Close</Button>
)
// this button should appear if the step they are on is not 0 and not the last step
if(currentStep !== 0 && currentStep !== dialogSteps.length-1){
buttons.push(
<Button key={"back"} onClick={()=>{setCurrentStep(currentStep-1)}}>Back</Button>
)
}
// this button should appear if the step they are on is less than the last step -1
if(currentStep < dialogSteps.length-2){
buttons.push(
<Button key={"next"} onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>
)
}
if(((binMode === pond.BinMode.BIN_MODE_STORAGE || binMode === pond.BinMode.BIN_MODE_COOLDOWN) && currentStep === 0) ||
(binMode === pond.BinMode.BIN_MODE_DRYING || binMode === pond.BinMode.BIN_MODE_HYDRATING) && currentStep === 1){
buttons.push(
<Button key={"confirm"} onClick={() => {
//if the bin mode is storage, just update the controllers for the selected device and close
if(binMode === pond.BinMode.BIN_MODE_STORAGE){
close(true)
if (selectedDevice) {
//update the controllers to be off for the selected device
heaters.forEach(heater => {
componentAPI
.update(selectedDevice.id(), heater.settings, undefined, undefined, as)
.then()
.catch();
});
fans.forEach(fan => {
componentAPI
.update(selectedDevice.id(), fan.settings, undefined, undefined, as)
.then()
.catch();
});
}
}else{//if it is cooldown, drying, or hydrating, it will need to go to the progress step and initiate all of the promises
buildPromises()
setCurrentStep(dialogSteps.length-1)//set the stepper to the progress step
}
}}>Confirm</Button>
)
}
return buttons
}
const stepperContent = (step: number) => {
switch(binMode){
case pond.BinMode.BIN_MODE_STORAGE:
return selectDeviceStep();
case pond.BinMode.BIN_MODE_COOLDOWN:
switch (step) {
case 1:
return progressStep();
default:
return selectDeviceStep();
}
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_HYDRATING:
switch (step) {
case 1:
return selectDeviceStep();
case 2:
return progressStep();
default:
return moistureStep();
}
}
};
return (
<React.Fragment>
<ResponsiveDialog open={open} onClose={() => {close(false)}}>
<DialogTitle>
{binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"}
</DialogTitle>
<DialogContent>
{stepper()}
{stepperContent(currentStep)}
</DialogContent>
<DialogActions>
{actions()}
</DialogActions>
</ResponsiveDialog>
</React.Fragment>
)
}

View file

@ -0,0 +1,258 @@
import { CheckBox, ExpandMore } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Checkbox, FormControlLabel, Grid2, TextField, Typography } from "@mui/material";
import { cloneDeep } from "lodash";
import { Ambient } from "models/Ambient";
import { Controller } from "models/Controller";
import { DevicePreset } from "models/DevicePreset";
import { Plenum } from "models/Plenum";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
export interface ComponentSet {
sensor: Plenum | Ambient,
controllers: Controller[]
selectedPresets: Map<string, DevicePreset>
combo?: boolean //if the set contains both a heater and a fan
}
interface Option {
label: string;
id: number;
icon?: string;
}
interface Props {
sensor: Plenum | Ambient
binMode: pond.BinMode
heaters?: Controller[]
fans?: Controller[]
presets?: DevicePreset[]
updateSet: (sensorKey: string, componentSet?: ComponentSet) => void
}
export default function PickComponentSet(props: Props) {
const {sensor, heaters, fans, updateSet, presets, binMode} = props
const [sensorSelected, setSensorSelected] = useState(false)
const [currentSet, setCurrentSet] = useState<ComponentSet | undefined>()
useEffect(()=>{
if (presets){
let presetOptions: Option[] = [];
presets.forEach((devicePreset, i) => {
//add the preset to the possible options
if (devicePreset.favorite()) {
//if it is favorited add it to the front of the array
presetOptions.unshift({
label: devicePreset.name,
id: i
});
} else {
//otherwise add it to the end
presetOptions.push({
label: devicePreset.name,
id: i
});
}
});
}
},[presets])
const getOptionsForController = (controllerType: pond.ControllerType) => {
let validOps: Option[] = []
if(presets){
presets.forEach((devicePreset, i) => {
if(devicePreset.controllerType() === controllerType && devicePreset.compareToBinMode(binMode)){
//add the preset to the possible options
if (devicePreset.favorite()) {
//if it is favorited add it to the front of the array
validOps.unshift({
label: devicePreset.name,
id: i
});
} else {
//otherwise add it to the end
validOps.push({
label: devicePreset.name,
id: i
});
}
}
})
}
return validOps
}
const controllerIndex = (key: string) => {
if(!currentSet) return -1
let index = -1
currentSet.controllers.forEach((cont, i) => {
if(cont.key() === key){
index = i
}
})
return index
}
const comboCheck = (set: ComponentSet) => {
let heaterFound = false
let fanFound = false
set.controllers.forEach(controller => {
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
heaterFound = true
}
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){
fanFound = true
}
})
return heaterFound && fanFound
}
return (
<Box>
<FormControlLabel
control={
<Checkbox
value={sensorSelected}
checked={sensorSelected}
onChange={(_, checked) => {
setSensorSelected(checked)
let newSet: ComponentSet | undefined
if(checked){
newSet = {
sensor: sensor,
controllers: [],
selectedPresets: new Map<string, DevicePreset>()
}
}
setCurrentSet(newSet)
updateSet(sensor.key(), newSet)
}}
/>
}
label={sensor.name()}
/>
{sensorSelected && currentSet &&
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>Controllers Selected: {currentSet.controllers.length}</AccordionSummary>
<AccordionDetails>
{heaters && heaters.length > 0 && binMode !== pond.BinMode.BIN_MODE_HYDRATING &&
<React.Fragment>
<Typography>Heaters</Typography>
{heaters.map(heater => (
<Grid2 key={heater.key()} container justifyContent="space-between" direction="row" alignContent="center" alignItems="center">
<Grid2 size={{xs: 12, sm: 6}}>
<FormControlLabel
control={
<Checkbox
onChange={(_, checked) => {
let set = cloneDeep(currentSet)
if(checked){//if checked need to add it to the controllers
set.controllers.push(heater)
}else{//will need to make sure it is not in the controllers
let index = controllerIndex(heater.key())
if (index !== -1){
set.controllers.splice(index, 1)
}
}
set.combo = comboCheck(set)
setCurrentSet(set)
updateSet(sensor.key(), set)
}}
/>
}
label={heater.name()}
/>
</Grid2>
<Grid2 size={{xs: 12, sm: 6}}>
{presets && getOptionsForController(pond.ControllerType.CONTROLLER_TYPE_HEATER).length > 0 &&
<Autocomplete
disablePortal
options={getOptionsForController(pond.ControllerType.CONTROLLER_TYPE_HEATER)}
fullWidth
getOptionLabel={option => option.label || ""}
onChange={(_, newValue) => {
if (newValue) {
let presetId = newValue.id;
if (presets && presets[presetId]) {
//setSelectedPreset(presets[presetId]);
let set = cloneDeep(currentSet)
set.selectedPresets.set(heater.key(), presets[presetId])
set.combo = comboCheck(set)
setCurrentSet(set)
updateSet(sensor.key(), set)
}
}
}}
renderInput={params => <TextField {...params} variant="outlined" label="Preset" />}
/>
}
</Grid2>
</Grid2>
))}
</React.Fragment>
}
{fans && fans.length > 0 &&
<React.Fragment>
<Typography>Fans</Typography>
{fans.map(fan => (
<Grid2 key={fan.key()} container justifyContent="space-between" direction="row" alignContent="center" alignItems="center">
<Grid2 size={{xs: 12, sm: 6}}>
<FormControlLabel
control={
<Checkbox
onChange={(_, checked) => {
let set = cloneDeep(currentSet)
if(checked){//if checked need to add it to the controllers
set.controllers.push(fan)
}else{//will need to make sure it is not in the controllers
let index = controllerIndex(fan.key())
if (index !== -1){
set.controllers.splice(index, 1)
}
}
set.combo = comboCheck(set)
setCurrentSet(set)
updateSet(sensor.key(), set)
}}
/>
}
label={fan.name()}
/>
</Grid2>
<Grid2 size={{xs: 12, sm: 6}}>
{presets && getOptionsForController(pond.ControllerType.CONTROLLER_TYPE_FAN).length > 0 &&
<Autocomplete
disablePortal
options={getOptionsForController(pond.ControllerType.CONTROLLER_TYPE_FAN)}
fullWidth
getOptionLabel={option => option.label || ""}
onChange={(_, newValue) => {
if (newValue) {
let presetId = newValue.id;
if (presets && presets[presetId]) {
//setSelectedPreset(presets[presetId]);
let set = cloneDeep(currentSet)
set.selectedPresets.set(fan.key(), presets[presetId])
set.combo = comboCheck(set)
setCurrentSet(set)
updateSet(sensor.key(), set)
}
}
}}
renderInput={params => <TextField {...params} variant="outlined" label="Preset" />}
/>
}
</Grid2>
</Grid2>
))}
</React.Fragment>
}
</AccordionDetails>
</Accordion>
}
</Box>
)
}

View file

@ -19,6 +19,10 @@ export interface ButtonData {
* The function to run when the button is clicked or toggled on when toggle is true * The function to run when the button is clicked or toggled on when toggle is true
*/ */
function: () => void function: () => void
/**
* whether the button is disabled or not
*/
disabled?: boolean
} }
interface Props { interface Props {
@ -49,6 +53,10 @@ interface Props {
* Numerical value for the size of the text for buttons that dont use the icon * Numerical value for the size of the text for buttons that dont use the icon
*/ */
textSize?: number textSize?: number
/**
* When true will disable all of the buttons in the group, will be overridded by individual buttons disabled state in the button data
*/
disableAll?: boolean
} }
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
@ -88,7 +96,7 @@ const useStyles = makeStyles((theme: Theme) => {
}) })
export default function ButtonGroup(props: Props){ export default function ButtonGroup(props: Props){
const { buttons, toggle, toggledButtons, multiToggle, textSize } = props const { buttons, toggle, toggledButtons, multiToggle, textSize, disableAll } = props
const classes = useStyles() const classes = useStyles()
const [currentToggle, setCurrentToggle] = useState<number[]>([]) const [currentToggle, setCurrentToggle] = useState<number[]>([])
@ -135,6 +143,7 @@ export default function ButtonGroup(props: Props){
<Box className={classes.container} display="flex" flexDirection="row"> <Box className={classes.container} display="flex" flexDirection="row">
{buttons.map((b, i) => ( {buttons.map((b, i) => (
<Button <Button
disabled={disableAll ?? b.disabled}
key={i} key={i}
className={checkToggled(i) ? classes.buttonToggled : classes.button} className={checkToggled(i) ? classes.buttonToggled : classes.button}
onClick={() => { onClick={() => {

View file

@ -0,0 +1,140 @@
import { AxiosResponse } from "axios"
import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"
import { useEffect, useState } from "react"
import { CheckCircle, Error, Pending } from "@mui/icons-material"
import React from "react"
export interface Stage {
title: string
steps: Step[]
}
export interface Step {
title: string
promise: Promise<AxiosResponse>
onComplete?: () => void
onError?: () => void
}
interface Props {
stages: Stage[]
automaticStart: boolean
failFast?: boolean
onStart?: () => void
onComplete?: () => void
}
enum progress {
Waiting = 1,
InProgress = 2,
Complete = 3,
Failed = 4
}
export function PromiseProgress(props: Props){
const {stages, failFast, onStart, onComplete, automaticStart} = props
const [completion, setCompletion] = useState<Map<string, progress>>(new Map())
const execute = () => {
let completionMap: Map<string, progress> = new Map()
stages.forEach((stage, i) => {
stage.steps.forEach((_, k) => {
completionMap.set(i + "-" + k, progress.Waiting)
})
})
setCompletion(completionMap)
const runStages = async () => {
let progMap = new Map(completionMap)
for (const [i, stage] of stages.entries()) {
// For each stage, map the steps into their own promise chains
const stepPromises = stage.steps.map((step, k) => {
// mark step as in progress
progMap.set(i+"-"+k, progress.InProgress);
setCompletion(new Map(progMap));
return step.promise
.then(() => {
// mark step as completed
progMap.set(i+"-"+k, progress.Complete);
setCompletion(new Map(progMap));
if (step.onComplete) step.onComplete();
})
.catch(err => {
// mark step as failed
progMap.set(i+"-"+k, progress.Failed);
setCompletion(new Map(progMap));
if(failFast){
throw err; // this rejects this step promise
}
})
});
// Wait for all steps in the current stage to finish
// (they run in parallel)
await Promise.all(stepPromises);
}
};
if(onStart){
onStart()
}
runStages().catch(err => {
console.error("Execution stopped due to error:", err);
}).finally(() => {
if(onComplete) onComplete()
});
}
useEffect(() => {
if(automaticStart){
execute()
}
},[automaticStart, execute])
const getCompletion = (completion?: progress) => {
switch(completion){
case progress.InProgress:
return <CircularProgress />
case progress.Complete:
return <CheckCircle />
case progress.Failed:
return <Error />
default://default will run if it is waiting or there was no completion
return <Pending />
}
}
const displayStage = (stage: Stage, number: number) => {
return (
<Box key={number}>
<Typography>Stage {stage.title}</Typography>
{stage.steps.map((step, i) => (
<Box key={number+"-"+i} display="flex" flexDirection="row" justifyContent="space-between" margin={2} alignContent="center" alignItems="center">
<Typography>{step.title}</Typography>
{getCompletion(completion.get(number + "-" + i))}
</Box>
))}
</Box>
)
}
return (
<React.Fragment>
<DialogTitle>
Progress
</DialogTitle>
<DialogContent>
{stages.map((stage, number) => displayStage(stage, number))}
</DialogContent>
<DialogActions>
{!automaticStart &&
<Button onClick={() => {
execute()
}}>
Start
</Button>
}
</DialogActions>
</React.Fragment>
)
}

View file

@ -689,8 +689,8 @@ export default function Bin(props: Props) {
const overview = () => { const overview = () => {
return ( return (
<BinVisualizerV2 <BinVisualizerV2
plenum={plenums[0]} plenums={plenums}
pressure={pressures[0]} pressures={pressures}
ambient={ambients[0]} ambient={ambients[0]}
cables={grainCables} cables={grainCables}
bin={bin} bin={bin}
@ -698,8 +698,6 @@ export default function Bin(props: Props) {
components={components} components={components}
componentDevices={componentDevices} componentDevices={componentDevices}
devices={devices} devices={devices}
interactions={interactions}
//changeBinMode={changeBinMode}
afterUpdate={refresh} afterUpdate={refresh}
refresh={refresh} refresh={refresh}
preferences={preferences} preferences={preferences}

View file

@ -196,7 +196,7 @@ export default function Bins(props: Props) {
const { openSnack } = useSnackbar(); const { openSnack } = useSnackbar();
// const history = useHistory(); // const history = useHistory();
const navigate = useNavigate() const navigate = useNavigate()
const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("average"); const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("high");
const [teamsDialog, setTeamsDialog] = useState(false) const [teamsDialog, setTeamsDialog] = useState(false)

View file

@ -1,7 +1,7 @@
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import UserProvider, { useUserAPI } from "./userAPI"; import UserProvider, { useUserAPI } from "./userAPI";
import TeamProvider, { useTeamAPI } from "./teamAPI"; import TeamProvider, { useTeamAPI } from "./teamAPI";
import { useImagekitAPI } from "./imagekitAPI"; import ImagekitProvider, { useImagekitAPI } from "./imagekitAPI";
import PermissionProvider, { usePermissionAPI } from "./permissionAPI"; import PermissionProvider, { usePermissionAPI } from "./permissionAPI";
import { Scope } from "models"; import { Scope } from "models";
import BinProvider, { useBinAPI } from "./binAPI"; import BinProvider, { useBinAPI } from "./binAPI";
@ -92,7 +92,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<UsageProvider> <UsageProvider>
<KeyManagerProvider> <KeyManagerProvider>
<DevicePresetProvider> <DevicePresetProvider>
{children} <ImagekitProvider>
{children}
</ImagekitProvider>
</DevicePresetProvider> </DevicePresetProvider>
</KeyManagerProvider> </KeyManagerProvider>
</UsageProvider> </UsageProvider>