Merge branch 'staging_environment'
This commit is contained in:
commit
bdddcfc103
10 changed files with 1504 additions and 262 deletions
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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()}
|
||||||
|
|
|
||||||
90
src/bin/conditioning/conditioningSelector.tsx
Normal file
90
src/bin/conditioning/conditioningSelector.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
846
src/bin/conditioning/modeChangeDialog.tsx
Normal file
846
src/bin/conditioning/modeChangeDialog.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
258
src/bin/conditioning/pickComponentSet.tsx
Normal file
258
src/bin/conditioning/pickComponentSet.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
140
src/common/PromiseProgress.tsx
Normal file
140
src/common/PromiseProgress.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -357,7 +357,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
const addCondition = () => {
|
const addCondition = () => {
|
||||||
let updatedInteraction = Interaction.clone(interaction);
|
let updatedInteraction = Interaction.clone(interaction);
|
||||||
let updatedRawConditionValues = rawConditionValues;
|
let updatedRawConditionValues = rawConditionValues;
|
||||||
if (numConditions < 2 && interaction.settings.source) {
|
if (numConditions < device.maxConditions() && interaction.settings.source) {
|
||||||
let condition = pond.InteractionCondition.create();
|
let condition = pond.InteractionCondition.create();
|
||||||
condition.measurementType = getMeasurements(
|
condition.measurementType = getMeasurements(
|
||||||
interaction.settings.source.type
|
interaction.settings.source.type
|
||||||
|
|
@ -957,29 +957,16 @@ export default function InteractionSettings(props: Props) {
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 2, sm: 1 }}>
|
<Grid size={{ xs: 2, sm: 1 }}>
|
||||||
{index === 0 ? (
|
<Tooltip title="Remove this condition">
|
||||||
<Tooltip title="Add another condition">
|
<IconButton
|
||||||
<IconButton
|
color="default"
|
||||||
color="primary"
|
disabled={numConditions === 1 ||!canEdit}
|
||||||
disabled={numConditions > 1 || !canEdit}
|
aria-label="Remove condition"
|
||||||
aria-label="Add another condition"
|
onClick={() => removeCondition(index)}
|
||||||
onClick={addCondition}
|
className={classNames(classes.redButton, classes.noPadding)}>
|
||||||
className={classNames(classes.greenButton, classes.noPadding)}>
|
<RemoveIcon />
|
||||||
<AddIcon />
|
</IconButton>
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip title="Remove this condition">
|
|
||||||
<IconButton
|
|
||||||
color="default"
|
|
||||||
disabled={!canEdit}
|
|
||||||
aria-label="Remove condition"
|
|
||||||
onClick={() => removeCondition(index)}
|
|
||||||
className={classNames(classes.redButton, classes.noPadding)}>
|
|
||||||
<RemoveIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
{multiNodeSource() && !sensor && (
|
{multiNodeSource() && !sensor && (
|
||||||
|
|
@ -1076,6 +1063,16 @@ export default function InteractionSettings(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
<FormHelperText>You must select a source before adding conditions</FormHelperText>
|
<FormHelperText>You must select a source before adding conditions</FormHelperText>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip title="Add another condition">
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
disabled={numConditions >= device.maxConditions() || !canEdit}
|
||||||
|
aria-label="Add another condition"
|
||||||
|
onClick={addCondition}
|
||||||
|
className={classNames(classes.greenButton, classes.noPadding)}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,18 @@ export class Device {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public maxConditions(): number {
|
||||||
|
let max = 2
|
||||||
|
switch (this.settings.platform) {
|
||||||
|
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK:
|
||||||
|
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
|
||||||
|
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
|
||||||
|
case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE:
|
||||||
|
max = this.status.firmwareVersion >= "2.1.9" ? 4 : 2;
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
public featureSupported(feature: string): boolean {
|
public featureSupported(feature: string): boolean {
|
||||||
let versions = featureVersions.get(feature);
|
let versions = featureVersions.get(feature);
|
||||||
if (!versions) {
|
if (!versions) {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue