finished the refactoring of the bin mode change to allow for more components to be selected

This commit is contained in:
csawatzky 2025-11-20 15:39:29 -06:00
parent 70bd91bbe7
commit 3df6db8a01
6 changed files with 350 additions and 272 deletions

View file

@ -7,11 +7,9 @@ import {
darken, darken,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText,
DialogTitle, DialogTitle,
Grid2 as Grid, Grid2 as Grid,
IconButton, IconButton,
InputAdornment,
lighten, lighten,
Link, Link,
Skeleton, Skeleton,
@ -19,8 +17,6 @@ import {
Switch, Switch,
TextField, TextField,
Theme, Theme,
ToggleButton,
ToggleButtonGroup,
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
@ -31,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";
@ -49,7 +45,6 @@ import { useBinAPI } from "providers/pond/binAPI";
import BindaptIcon from "products/Bindapt/BindaptIcon"; import BindaptIcon from "products/Bindapt/BindaptIcon";
import { import {
AccessTime, AccessTime,
ArrowForward,
ArrowForwardIos, ArrowForwardIos,
CheckCircleOutline, CheckCircleOutline,
Error, Error,
@ -59,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";
@ -73,7 +67,7 @@ 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"; import ModeChangeDialog from "./conditioning/modeChangeDialog";
@ -191,8 +185,6 @@ interface Props {
ambient?: Ambient; ambient?: Ambient;
cables?: GrainCable[]; cables?: GrainCable[];
pressures?: 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>;
@ -204,8 +196,6 @@ interface Props {
export default function BinVisualizer(props: Props) { export default function BinVisualizer(props: Props) {
const { const {
bin, bin,
//changeBinMode,
//changeGrainAmount,
plenums, plenums,
ambient, ambient,
pressures, pressures,
@ -216,7 +206,6 @@ export default function BinVisualizer(props: Props) {
preferences, preferences,
componentDevices, componentDevices,
permissions, permissions,
//interactions,
refresh, refresh,
updateComponentCallback, updateComponentCallback,
binPresets binPresets
@ -243,9 +232,9 @@ export default function BinVisualizer(props: Props) {
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);
@ -331,7 +320,7 @@ export default function BinVisualizer(props: Props) {
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));
@ -344,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(() => {
@ -774,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));
@ -1960,83 +1949,6 @@ export default function BinVisualizer(props: Props) {
setOpenModeChange(true) 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={setModeConditioning} color="primary">
// Update
// </Button>
// </DialogActions>
// </ResponsiveDialog>
// );
// };
if (loading) { if (loading) {
return <Skeleton variant="rectangular" height={460} />; return <Skeleton variant="rectangular" height={460} />;
} }
@ -2046,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;
@ -2057,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){
@ -2080,26 +1992,10 @@ 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
preferences={preferences}
binKey={bin.key()}
devices={devices}
refreshCallback={success => {
setShowInputMoisture(false);
setNewPreset(0)
if (success) updateBin();
}}
parentPreset={newPreset}
grain={bin.settings.inventory?.grainType}
binCables={cables}
compDevMap={componentDevices}
presets={binPresets}
/> */}
<ModeChangeDialog <ModeChangeDialog
binKey={bin.key()} binKey={bin.key()}
binMode={newBinMode} binMode={newBinMode}
@ -2107,18 +2003,32 @@ export default function BinVisualizer(props: Props) {
devices={devices} devices={devices}
open={openModeChange} open={openModeChange}
binCables={cables} binCables={cables}
presets={binPresets}
compDevMap={componentDevices} compDevMap={componentDevices}
preferences={preferences} preferences={preferences}
onClose={(refresh) => { onClose={(refresh) => {
setOpenModeChange(false) setOpenModeChange(false)
if(refresh) updateBin(); 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={() => { startChange={() => {
setModeChangeInProgress(true) setModeChangeInProgress(true)
}} }}
changeComplete={() => { changeComplete={() => {
setModeChangeInProgress(false) setModeChangeInProgress(false)
}} }}
/> />
<Box paddingRight={1}> <Box paddingRight={1}>
{binMode()} {binMode()}

View file

@ -1,10 +1,12 @@
import { Box, Checkbox, DialogActions, DialogContent, FormControlLabel, Typography } from "@mui/material" import { Typography } from "@mui/material"
import { Ambient } from "models/Ambient" import { Ambient } from "models/Ambient"
import { Controller } from "models/Controller" import { Controller } from "models/Controller"
import { Plenum } from "models/Plenum" import { Plenum } from "models/Plenum"
import React, { useEffect, useState } from "react" import React, { useState } from "react"
import PickComponentSet, { ComponentSet } from "./pickComponentSet" import PickComponentSet, { ComponentSet } from "./pickComponentSet"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { DevicePreset } from "models/DevicePreset"
import { pond } from "protobuf-ts/pond"
interface Props { interface Props {
@ -12,12 +14,14 @@ interface Props {
ambients: Ambient[] ambients: Ambient[]
heaters: Controller[] heaters: Controller[]
fans: Controller[] fans: Controller[]
binMode: pond.BinMode
presets?: DevicePreset[]
updateSets: (sets: ComponentSet[]) => void updateSets: (sets: ComponentSet[]) => void
} }
export default function ConditioningSelector(props: Props){ export default function ConditioningSelector(props: Props){
const {plenums, ambients, heaters, fans, updateSets} = props const {plenums, ambients, heaters, fans, updateSets, presets, binMode} = props
const [currentSets, setCurrentSets] = useState<ComponentSet[]>([]) const [currentSets, setCurrentSets] = useState<ComponentSet[]>([])
const sensorIndex = (key: string) => { const sensorIndex = (key: string) => {
@ -64,6 +68,8 @@ export default function ConditioningSelector(props: Props){
sensor={plenum} sensor={plenum}
heaters={heaters} heaters={heaters}
fans={fans} fans={fans}
binMode={binMode}
presets={presets}
updateSet={setUpdate} updateSet={setUpdate}
/> />
)})} )})}
@ -74,6 +80,8 @@ export default function ConditioningSelector(props: Props){
sensor={ambient} sensor={ambient}
heaters={heaters} heaters={heaters}
fans={fans} fans={fans}
binMode={binMode}
presets={presets}
updateSet={setUpdate} updateSet={setUpdate}
/> />
)})} )})}

View file

@ -1,4 +1,4 @@
import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Stepper, Step, StepLabel, InputAdornment, Typography } from "@mui/material"; import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Stepper, Step, StepLabel, InputAdornment, Typography, useTheme } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { useComponentAPI, useInteractionsAPI } from "hooks"; import { useComponentAPI, useInteractionsAPI } from "hooks";
import { Component, Device, Interaction } from "models"; import { Component, Device, Interaction } from "models";
@ -19,7 +19,7 @@ import { sameComponentID } from "pbHelpers/Component";
import moment from "moment"; import moment from "moment";
import { lowerCase } from "lodash"; import { lowerCase } from "lodash";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { getTemperatureUnit } from "utils"; import { fahrenheitToCelsius, getTemperatureUnit } from "utils";
import { GetGrainExtensionMap } from "grain"; import { GetGrainExtensionMap } from "grain";
import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress"; import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress";
@ -37,10 +37,11 @@ interface Props {
startChange: () => void; startChange: () => void;
changeComplete: () => void; changeComplete: () => void;
defaultTargetMoisture?: number; defaultTargetMoisture?: number;
changeTargetMoisture?: (newTarget: number) => {} changeTargetMoisture?: (newTarget: number) => void
defaultOutdoorTemp?: number; defaultOutdoorTemp?: number;
changeOutdoorTemp?: (newTemp: number) => {} changeOutdoorTemp?: (newTemp: number) => void
defaultOutdoorHumidity?: number; defaultOutdoorHumidity?: number;
changeOutdoorHumidity?: (newHumidity: number) => void
} }
interface Option { interface Option {
@ -67,7 +68,13 @@ export default function ModeChangeDialog(props: Props){
grain, grain,
startChange, startChange,
changeComplete, changeComplete,
defaultTargetMoisture defaultTargetMoisture,
defaultOutdoorTemp,
defaultOutdoorHumidity,
changeTargetMoisture,
changeOutdoorTemp,
changeOutdoorHumidity,
presets
} = props } = props
const [{as}] = useGlobalState() const [{as}] = useGlobalState()
const [componentSets, setComponentSets] = useState<ComponentSet[]>([]) const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
@ -85,20 +92,24 @@ export default function ModeChangeDialog(props: Props){
const [ambients, setAmbients] = useState<Ambient[]>([]) const [ambients, setAmbients] = useState<Ambient[]>([])
const [heaters, setHeaters] = useState<Controller[]>([]) const [heaters, setHeaters] = useState<Controller[]>([])
const [fans, setFans] = useState<Controller[]>([]) const [fans, setFans] = useState<Controller[]>([])
const [selectedPreset, setSelectedPreset] = useState<DevicePreset | undefined>(undefined);
const grainExtensionMap = GetGrainExtensionMap(); const grainExtensionMap = GetGrainExtensionMap();
const [conflictingSetInteractions, setConflictingSetInteractions] = useState<Interaction[]>([]) const [conflictingSetInteractions, setConflictingSetInteractions] = useState<Interaction[]>([])
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>() const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
const [promiseStages, setPromiseStages] = useState<Stage[]>([]) const [promiseStages, setPromiseStages] = useState<Stage[]>([])
const [dialogSteps, setDialogSteps] = useState<DialogStep[]>([]) const [dialogSteps, setDialogSteps] = useState<DialogStep[]>([])
const [currentStep, setCurrentStep] = useState(0) const [currentStep, setCurrentStep] = useState(0)
const theme = useTheme()
//the state variables for the moisture //the state variables for the moisture
const [moistureInput, setMoistureInput] = useState<string>("") const [moistureInput, setMoistureInput] = useState<string>("")
const [outdoorTempInput, setOutdoorTempInput] = useState<string>("")
const [outdoorHumidityInput, setOutdoorHumidityInput] = useState<string>("")
useEffect(()=>{ useEffect(()=>{
setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0") setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0")
},[defaultTargetMoisture]) 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 //get the interactions and components for the device when it is selected from the dropdown
useEffect(()=>{ useEffect(()=>{
@ -214,7 +225,8 @@ if (!selectedDevice) return;
const buildHeaterInteraction = ( const buildHeaterInteraction = (
sensor: Plenum | Ambient, sensor: Plenum | Ambient,
heater: Controller heater: Controller,
customPreset?: DevicePreset
): pond.InteractionSettings => { ): pond.InteractionSettings => {
let interaction = pond.InteractionSettings.create({ let interaction = pond.InteractionSettings.create({
source: sensor.location(), source: sensor.location(),
@ -236,11 +248,10 @@ if (!selectedDevice) return;
let temp = 0; let temp = 0;
let hum = 0; let hum = 0;
//if a preset was selected use its temp/hum values //if a preset was selected use its temp/hum values
if (selectedPreset !== undefined) { if (customPreset !== undefined) {
temp = selectedPreset.temp(); temp = customPreset.temp();
hum = selectedPreset.hum(); hum = customPreset.hum();
} else { } else {
//otherwise use default values //otherwise use default values
switch(binMode){ switch(binMode){
@ -303,7 +314,8 @@ if (!selectedDevice) return;
sensor: Plenum | Ambient, sensor: Plenum | Ambient,
fan: Controller, fan: Controller,
tempComparison: "greater" | "less", tempComparison: "greater" | "less",
humidityComparison: "greater" | "less" humidityComparison: "greater" | "less",
customPreset?: DevicePreset
) => { ) => {
let interaction = pond.InteractionSettings.create({ let interaction = pond.InteractionSettings.create({
source: sensor.location(), source: sensor.location(),
@ -327,9 +339,9 @@ if (!selectedDevice) return;
let humidityPreset = 0; let humidityPreset = 0;
//if a custom preset was selected use it //if a custom preset was selected use it
if (selectedPreset !== undefined) { if (customPreset !== undefined) {
tempPreset = selectedPreset.temp(); tempPreset = customPreset.temp();
humidityPreset = selectedPreset.hum(); humidityPreset = customPreset.hum();
} else { } else {
//otherwise use one of the defaults //otherwise use one of the defaults
switch (binMode) { switch (binMode) {
@ -437,7 +449,6 @@ if (!selectedDevice) return;
} }
componentSets.forEach(set => { componentSets.forEach(set => {
set.controllers.forEach(controller => { set.controllers.forEach(controller => {
console.log(controller)
let newStep: PromiseStep = { let newStep: PromiseStep = {
title: "Updating " + controller.name(), title: "Updating " + controller.name(),
promise: componentAPI.update(selectedDevice.id(), controller.settings) promise: componentAPI.update(selectedDevice.id(), controller.settings)
@ -461,6 +472,7 @@ if (!selectedDevice) return;
sets.forEach((set) => { sets.forEach((set) => {
//loop through the controllers //loop through the controllers
set.controllers.forEach(controller => { set.controllers.forEach(controller => {
let customPreset = set.selectedPresets.get(controller.key())
//filter the conflicting interactions for that controller //filter the conflicting interactions for that controller
let c = existingInteractions.filter(i => { let c = existingInteractions.filter(i => {
let conflicting = false; let conflicting = false;
@ -478,17 +490,17 @@ if (!selectedDevice) return;
case pond.BinMode.BIN_MODE_COOLDOWN: case pond.BinMode.BIN_MODE_COOLDOWN:
// if the controller is a heater create heater interaction // if the controller is a heater create heater interaction
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller)) multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller, customPreset))
}else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN || }else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){ controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){
// if the controller is a fan create a fan interaction // if the controller is a fan create a fan interaction
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "less")) multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "less", customPreset))
} }
break; break;
case pond.BinMode.BIN_MODE_DRYING: case pond.BinMode.BIN_MODE_DRYING:
// if the controller is a heater create heater interaction // if the controller is a heater create heater interaction
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller)) multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller, customPreset))
}else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN || }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 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 it is a combination set using a heater as well just turn the fan on
@ -496,7 +508,7 @@ if (!selectedDevice) return;
controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON
}else{ }else{
//otherwise make a fan interaction //otherwise make a fan interaction
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less")) multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less", customPreset))
} }
} }
break; break;
@ -505,7 +517,7 @@ if (!selectedDevice) return;
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN || if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){ controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){
//create fan interaction //create fan interaction
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "greater")) multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "greater", customPreset))
} }
break; break;
} }
@ -572,7 +584,14 @@ if (!selectedDevice) return;
<TextField <TextField
label={"Grain Moisture"} label={"Grain Moisture"}
value={moistureInput} value={moistureInput}
onChange={event => setMoistureInput(event.target.value)} 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={{ InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment> endAdornment: <InputAdornment position="end">%</InputAdornment>
}} }}
@ -585,8 +604,20 @@ if (!selectedDevice) return;
</Typography> </Typography>
<TextField <TextField
label={"Outdoor Temperature"} label={"Outdoor Temperature"}
value={tempInput} value={outdoorTempInput}
onChange={event => setTempInput(event.target.value)} 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={{ InputProps={{
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
@ -603,7 +634,14 @@ if (!selectedDevice) return;
<TextField <TextField
label={"Outdoor Humidity"} label={"Outdoor Humidity"}
value={outdoorHumidityInput} value={outdoorHumidityInput}
onChange={event => setOutdoorHumidityInput(event.target.value)} 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={{ InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment> endAdornment: <InputAdornment position="end">%</InputAdornment>
}} }}
@ -652,6 +690,8 @@ if (!selectedDevice) return;
ambients={ambients} ambients={ambients}
fans={fans} fans={fans}
heaters={heaters} heaters={heaters}
presets={presets}
binMode={binMode}
updateSets={(sets) => { updateSets={(sets) => {
//update the component sets that will be used for the interactions //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 //when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
@ -673,9 +713,13 @@ if (!selectedDevice) return;
<Box> <Box>
<PromiseProgress <PromiseProgress
stages={promiseStages} stages={promiseStages}
automaticStart
failFast failFast
onStart={() => {
startChange()
}}
onComplete={()=>{ onComplete={()=>{
console.log("COMPLETED PROMISES") close(true)
setCurrentStep(0)//reset the stepper setCurrentStep(0)//reset the stepper
changeComplete()//change the boolean preventing more mode changes changeComplete()//change the boolean preventing more mode changes
}} }}
@ -686,13 +730,13 @@ if (!selectedDevice) return;
const stepper = () => { const stepper = () => {
return ( return (
<Stepper nonLinear activeStep={currentStep} style={{ width: "100%" }}> <Stepper nonLinear activeStep={currentStep} style={{ width: "100%", marginBottom: theme.spacing(2) }}>
{dialogSteps.map(dialogStep => { {dialogSteps.map((dialogStep, i) => {
const labelProps: { const labelProps: {
optional?: React.ReactNode; optional?: React.ReactNode;
} = {}; } = {};
return ( return (
<Step key={dialogStep.label} completed={dialogStep.completed}> <Step key={i} completed={dialogStep.completed}>
<StepLabel {...labelProps}>{dialogStep.label}</StepLabel> <StepLabel {...labelProps}>{dialogStep.label}</StepLabel>
</Step> </Step>
); );
@ -705,24 +749,24 @@ if (!selectedDevice) return;
let buttons: JSX.Element[] = [] let buttons: JSX.Element[] = []
//this button should be on every step of all of them //this button should be on every step of all of them
buttons.push( buttons.push(
<Button onClick={() => {close(false)}}>Close</Button> <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 // 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){ if(currentStep !== 0 && currentStep !== dialogSteps.length-1){
buttons.push( buttons.push(
<Button onClick={()=>{setCurrentStep(currentStep-1)}}>Back</Button> <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 // this button should appear if the step they are on is less than the last step -1
if(currentStep < dialogSteps.length-2){ if(currentStep < dialogSteps.length-2){
buttons.push( buttons.push(
<Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button> <Button key={"next"} onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>
) )
} }
if((binMode === pond.BinMode.BIN_MODE_STORAGE || binMode === pond.BinMode.BIN_MODE_COOLDOWN && currentStep === 0) || 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){ (binMode === pond.BinMode.BIN_MODE_DRYING || binMode === pond.BinMode.BIN_MODE_HYDRATING) && currentStep === 1){
buttons.push( buttons.push(
<Button onClick={() => { <Button key={"confirm"} onClick={() => {
//if the bin mode is storage, just update the controllers for the selected device and close //if the bin mode is storage, just update the controllers for the selected device and close
if(binMode === pond.BinMode.BIN_MODE_STORAGE){ if(binMode === pond.BinMode.BIN_MODE_STORAGE){
close(true) close(true)
@ -741,11 +785,10 @@ if (!selectedDevice) return;
.catch(); .catch();
}); });
} }
}else{ }else{//if it is cooldown, drying, or hydrating, it will need to go to the progress step and initiate all of the promises
setCurrentStep(dialogSteps.length-1)//set the stepper to the progress step
buildPromises() buildPromises()
setCurrentStep(dialogSteps.length-1)//set the stepper to the progress step
} }
//if it is cooldown, drying, or hydrating, it will need to go to the progress step and initiate all of the promises
}}>Confirm</Button> }}>Confirm</Button>
) )
} }
@ -778,13 +821,6 @@ if (!selectedDevice) return;
return ( return (
<React.Fragment> <React.Fragment>
{/* <PromiseProgress
open={openProgress}
stages={promiseStages}
onClose={()=>{
setOpenProgress(false)
close(refresh)
}} /> */}
<ResponsiveDialog open={open} onClose={() => {close(false)}}> <ResponsiveDialog open={open} onClose={() => {close(false)}}>
<DialogTitle> <DialogTitle>
{binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"} {binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"}

View file

@ -1,30 +1,89 @@
import { CheckBox, ExpandMore } from "@mui/icons-material"; import { CheckBox, ExpandMore } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Box, Checkbox, FormControlLabel, Typography } from "@mui/material"; import { Accordion, AccordionDetails, AccordionSummary, Autocomplete, Box, Checkbox, FormControlLabel, Grid2, TextField, Typography } from "@mui/material";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Ambient } from "models/Ambient"; import { Ambient } from "models/Ambient";
import { Controller } from "models/Controller"; import { Controller } from "models/Controller";
import { DevicePreset } from "models/DevicePreset";
import { Plenum } from "models/Plenum"; import { Plenum } from "models/Plenum";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack"; import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
export interface ComponentSet { export interface ComponentSet {
sensor: Plenum | Ambient, sensor: Plenum | Ambient,
controllers: Controller[] controllers: Controller[]
selectedPresets: Map<string, DevicePreset>
combo?: boolean //if the set contains both a heater and a fan combo?: boolean //if the set contains both a heater and a fan
} }
interface Option {
label: string;
id: number;
icon?: string;
}
interface Props { interface Props {
sensor: Plenum | Ambient sensor: Plenum | Ambient
binMode: pond.BinMode
heaters?: Controller[] heaters?: Controller[]
fans?: Controller[] fans?: Controller[]
presets?: DevicePreset[]
updateSet: (sensorKey: string, componentSet?: ComponentSet) => void updateSet: (sensorKey: string, componentSet?: ComponentSet) => void
} }
export default function PickComponentSet(props: Props) { export default function PickComponentSet(props: Props) {
const {sensor, heaters, fans, updateSet} = props const {sensor, heaters, fans, updateSet, presets, binMode} = props
const [sensorSelected, setSensorSelected] = useState(false) const [sensorSelected, setSensorSelected] = useState(false)
const [currentSet, setCurrentSet] = useState<ComponentSet | undefined>() 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) => { const controllerIndex = (key: string) => {
if(!currentSet) return -1 if(!currentSet) return -1
let index = -1 let index = -1
@ -64,7 +123,8 @@ export default function PickComponentSet(props: Props) {
if(checked){ if(checked){
newSet = { newSet = {
sensor: sensor, sensor: sensor,
controllers: [] controllers: [],
selectedPresets: new Map<string, DevicePreset>()
} }
} }
setCurrentSet(newSet) setCurrentSet(newSet)
@ -78,12 +138,13 @@ export default function PickComponentSet(props: Props) {
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>Controllers Selected: {currentSet.controllers.length}</AccordionSummary> <AccordionSummary expandIcon={<ExpandMore />}>Controllers Selected: {currentSet.controllers.length}</AccordionSummary>
<AccordionDetails> <AccordionDetails>
{heaters && heaters.length > 0 && {heaters && heaters.length > 0 && binMode !== pond.BinMode.BIN_MODE_HYDRATING &&
<React.Fragment> <React.Fragment>
<Typography>Heaters</Typography> <Typography>Heaters</Typography>
{heaters.map(heater => ( {heaters.map(heater => (
<Grid2 key={heater.key()} container justifyContent="space-between" direction="row" alignContent="center" alignItems="center">
<Grid2 size={{xs: 12, sm: 6}}>
<FormControlLabel <FormControlLabel
key={heater.key()}
control={ control={
<Checkbox <Checkbox
onChange={(_, checked) => { onChange={(_, checked) => {
@ -104,6 +165,32 @@ export default function PickComponentSet(props: Props) {
} }
label={heater.name()} 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> </React.Fragment>
} }
@ -111,8 +198,9 @@ export default function PickComponentSet(props: Props) {
<React.Fragment> <React.Fragment>
<Typography>Fans</Typography> <Typography>Fans</Typography>
{fans.map(fan => ( {fans.map(fan => (
<Grid2 key={fan.key()} container justifyContent="space-between" direction="row" alignContent="center" alignItems="center">
<Grid2 size={{xs: 12, sm: 6}}>
<FormControlLabel <FormControlLabel
key={fan.key()}
control={ control={
<Checkbox <Checkbox
onChange={(_, checked) => { onChange={(_, checked) => {
@ -133,6 +221,32 @@ export default function PickComponentSet(props: Props) {
} }
label={fan.name()} 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> </React.Fragment>
} }

View file

@ -1,6 +1,5 @@
import { AxiosResponse } from "axios" import { AxiosResponse } from "axios"
import ResponsiveDialog from "./ResponsiveDialog" import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"
import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Icon, Typography } from "@mui/material"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { CheckCircle, Error, Pending } from "@mui/icons-material" import { CheckCircle, Error, Pending } from "@mui/icons-material"
import React from "react" import React from "react"
@ -19,7 +18,9 @@ export interface Step {
interface Props { interface Props {
stages: Stage[] stages: Stage[]
automaticStart: boolean
failFast?: boolean failFast?: boolean
onStart?: () => void
onComplete?: () => void onComplete?: () => void
} }
@ -31,7 +32,7 @@ enum progress {
} }
export function PromiseProgress(props: Props){ export function PromiseProgress(props: Props){
const {stages, failFast, onComplete} = props const {stages, failFast, onStart, onComplete, automaticStart} = props
const [completion, setCompletion] = useState<Map<string, progress>>(new Map()) const [completion, setCompletion] = useState<Map<string, progress>>(new Map())
const execute = () => { const execute = () => {
@ -52,7 +53,7 @@ export function PromiseProgress(props: Props){
progMap.set(i+"-"+k, progress.InProgress); progMap.set(i+"-"+k, progress.InProgress);
setCompletion(new Map(progMap)); setCompletion(new Map(progMap));
return step.promise return step.promise
.then(resp => { .then(() => {
// mark step as completed // mark step as completed
progMap.set(i+"-"+k, progress.Complete); progMap.set(i+"-"+k, progress.Complete);
setCompletion(new Map(progMap)); setCompletion(new Map(progMap));
@ -73,6 +74,9 @@ export function PromiseProgress(props: Props){
await Promise.all(stepPromises); await Promise.all(stepPromises);
} }
}; };
if(onStart){
onStart()
}
runStages().catch(err => { runStages().catch(err => {
console.error("Execution stopped due to error:", err); console.error("Execution stopped due to error:", err);
}).finally(() => { }).finally(() => {
@ -80,6 +84,12 @@ export function PromiseProgress(props: Props){
}); });
} }
useEffect(() => {
if(automaticStart){
execute()
}
},[automaticStart, execute])
const getCompletion = (completion?: progress) => { const getCompletion = (completion?: progress) => {
switch(completion){ switch(completion){
case progress.InProgress: case progress.InProgress:
@ -116,11 +126,13 @@ export function PromiseProgress(props: Props){
{stages.map((stage, number) => displayStage(stage, number))} {stages.map((stage, number) => displayStage(stage, number))}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{!automaticStart &&
<Button onClick={() => { <Button onClick={() => {
execute() execute()
}}> }}>
Start Start
</Button> </Button>
}
</DialogActions> </DialogActions>
</React.Fragment> </React.Fragment>
) )

View file

@ -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}