From 842f9bee6a3cc91571eb2e8c7f6edf90b9080f75 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 5 Nov 2025 15:07:34 -0600 Subject: [PATCH 01/11] passing multiple plenums and pressure to the visualizer and added a button to cycle them --- src/bin/BinVisualizerV2.tsx | 79 ++++++++++++++++++++++++++++++------- src/pages/Bin.tsx | 4 +- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index 8cd41e0..c00072c 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -48,6 +48,8 @@ import { useBinAPI } from "providers/pond/binAPI"; import BindaptIcon from "products/Bindapt/BindaptIcon"; import { AccessTime, + ArrowForward, + ArrowForwardIos, CheckCircleOutline, Error, InfoOutlined, @@ -172,16 +174,21 @@ interface GrainConditions { emcTrend: number; } +interface CombinedPlenum { + tempHumidity?: Plenum, + pressure?: Pressure +} + interface Props { bin: Bin; loading: boolean; components: Map; componentDevices?: Map; devices: Device[]; - plenum?: Plenum; + plenums?: Plenum[]; ambient?: Ambient; cables?: GrainCable[]; - pressure?: Pressure; + pressures?: Pressure[]; interactions?: Interaction[]; //changeBinMode: (binMode: pond.BinMode) => boolean; refresh: (showSnack?: boolean) => void; @@ -197,9 +204,9 @@ export default function BinVisualizer(props: Props) { bin, //changeBinMode, //changeGrainAmount, - plenum, + plenums, ambient, - pressure, + pressures, loading, cables, components, @@ -282,6 +289,10 @@ export default function BinVisualizer(props: Props) { const [storageDate, setStorageDate] = useState(moment().format("YYYY-MM-DD")); const [storageTime, setStorageTime] = useState(moment().format("HH:mm")); + const [activePlenum, setActivePlenum] = useState() + const [activePlenumIndex, setActivePlenumIndex] = useState(0) + const [combinedPlenums, setCombinedPlenums] = useState([]) + // const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({ // root: { // backgroundColor: "transparent", @@ -335,6 +346,31 @@ export default function BinVisualizer(props: Props) { setModeTime(moment(bin.status.lastModeChange)); }, [bin.status.lastModeChange]); + useEffect(()=>{ + //match the plenums and pressures based on their location (address) + let combinedPlenums: CombinedPlenum[] = [] + if (plenums) { + plenums.forEach(plenum => { + let newCombinedPlenum: CombinedPlenum = { + tempHumidity: plenum + } + if (pressures) { + pressures.forEach(pressure => { + if(plenum.location().address === pressure.location().address){ + console.log("found match") + newCombinedPlenum.pressure = pressure + } + }) + } + combinedPlenums.push(newCombinedPlenum) + }) + } + if (combinedPlenums.length > 0){ + setActivePlenum(combinedPlenums[0]) + } + setCombinedPlenums(combinedPlenums) + },[plenums, pressures]) + useEffect(() => { setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN); setStorageType(bin.storage()); @@ -418,7 +454,6 @@ export default function BinVisualizer(props: Props) { //determine which node is the coldest so that the data displayed is for the same node let lowTempIndex = filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)); - console.log(lowTempIndex) lowNodeConditions = { tempC: filteredNodes.temps[lowTempIndex], humidity: filteredNodes.humids[lowTempIndex], @@ -1094,14 +1129,28 @@ export default function BinVisualizer(props: Props) { const plenumOverview = () => { return ( + - Plenum + Plenum {plenums && plenums?.length > 1 ? activePlenumIndex+1 : ""} + {plenums && plenums.length > 1 && + { + let newIndex = activePlenumIndex + 1 + if(newIndex > combinedPlenums.length - 1){ + newIndex = 0 + } + setActivePlenum(combinedPlenums[newIndex]) + setActivePlenumIndex(newIndex) + }}> + + + } + - {plenum ? ( + {activePlenum?.tempHumidity ? ( - {plenum.getTempString(user.settings.temperatureUnit)} + {activePlenum.tempHumidity.getTempString(user.settings.temperatureUnit)} @@ -1141,7 +1190,7 @@ export default function BinVisualizer(props: Props) { fontWeight: 650, color: humidColour }}> - {plenum.getHumidityString()} + {activePlenum.tempHumidity.getHumidityString()} @@ -1262,7 +1311,7 @@ export default function BinVisualizer(props: Props) { style={{ height: isMobile ? 20 : 25, width: isMobile ? 20 : 25 }} /> - {pressure ? ( + {activePlenum?.pressure ? ( - {pressure.getPressureString(user.settings.pressureUnit)} + {activePlenum.pressure.getPressureString(user.settings.pressureUnit)} {/* @@ -1600,7 +1649,7 @@ export default function BinVisualizer(props: Props) { Fan Performance - {pressure && pressure.fanId === 0 && !bin.settings.fan?.type && bin.fanID() === 0 && ( + {activePlenum?.pressure && activePlenum.pressure.fanId === 0 && !bin.settings.fan?.type && bin.fanID() === 0 && ( No fans found to calculate CFM @@ -1642,8 +1691,8 @@ export default function BinVisualizer(props: Props) { {ExtractMoisture( bin.grain(), - plenum?.temperature ?? 0, - plenum?.humidity ?? 0 + activePlenum?.tempHumidity?.temperature ?? 0, + activePlenum?.tempHumidity?.humidity ?? 0 ).toFixed(2)} % @@ -2148,7 +2197,7 @@ export default function BinVisualizer(props: Props) { {controls()} - {plenum && pressure && fanPerformance()} + {plenums && pressures && fanPerformance()} {ambient && ambientDisplay()} {(bin.status.fans.length > 0 || bin.status.heaters.length > 0) && controllerDisplay()} {dryingEstimate()} diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 082a2c9..c352634 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -689,8 +689,8 @@ export default function Bin(props: Props) { const overview = () => { return ( Date: Wed, 5 Nov 2025 15:35:36 -0600 Subject: [PATCH 02/11] some button styling --- src/bin/BinVisualizerV2.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index c00072c..3eb19a8 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -1134,7 +1134,7 @@ export default function BinVisualizer(props: Props) { Plenum {plenums && plenums?.length > 1 ? activePlenumIndex+1 : ""} {plenums && plenums.length > 1 && - { + { let newIndex = activePlenumIndex + 1 if(newIndex > combinedPlenums.length - 1){ newIndex = 0 @@ -1142,7 +1142,7 @@ export default function BinVisualizer(props: Props) { setActivePlenum(combinedPlenums[newIndex]) setActivePlenumIndex(newIndex) }}> - + } From ea81610635e6a4154f0e9c07ff40851e6876e758 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 5 Nov 2025 16:07:33 -0600 Subject: [PATCH 03/11] removed log --- src/bin/BinVisualizerV2.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index 3eb19a8..be71eb8 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -357,7 +357,6 @@ export default function BinVisualizer(props: Props) { if (pressures) { pressures.forEach(pressure => { if(plenum.location().address === pressure.location().address){ - console.log("found match") newCombinedPlenum.pressure = pressure } }) From 393a2f77dca37831764b21b5b8d0913271512557 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 10 Nov 2025 15:21:43 -0600 Subject: [PATCH 04/11] the component sets are being properly constructed and passed up to the mode dialog that will control adding the interactions to them --- src/bin/BinVisualizerV2.tsx | 39 ++- src/bin/conditioning/conditioningSelector.tsx | 82 ++++++ src/bin/conditioning/modeChangeDialog.tsx | 275 ++++++++++++++++++ src/bin/conditioning/pickComponentSet.tsx | 125 ++++++++ 4 files changed, 512 insertions(+), 9 deletions(-) create mode 100644 src/bin/conditioning/conditioningSelector.tsx create mode 100644 src/bin/conditioning/modeChangeDialog.tsx create mode 100644 src/bin/conditioning/pickComponentSet.tsx diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index be71eb8..767eaed 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -74,6 +74,7 @@ import SearchSelect, { Option } from "common/SearchSelect"; import Edit from "@mui/icons-material/Edit"; import { makeStyles, styled } from "@mui/styles"; import ButtonGroup from "common/ButtonGroup"; +import ModeChangeDialog from "./conditioning/modeChangeDialog"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -256,7 +257,7 @@ export default function BinVisualizer(props: Props) { const [cfmLowOpen, setCFMLowOpen] = useState(false); const [cfmHighOpen, setCFMHighOpen] = useState(false); const [{ user }] = useGlobalState(); - const [newPreset, setNewPreset] = useState(pond.BinMode.BIN_MODE_NONE); + // const [newPreset, setNewPreset] = useState(pond.BinMode.BIN_MODE_NONE); const [newBinMode, setNewBinMode] = useState(pond.BinMode.BIN_MODE_NONE); const [selectedCable, setSelectedCable] = useState(); const [openNodeDialog, setOpenNodeDialog] = useState(false); @@ -293,6 +294,8 @@ export default function BinVisualizer(props: Props) { const [activePlenumIndex, setActivePlenumIndex] = useState(0) const [combinedPlenums, setCombinedPlenums] = useState([]) + const [openModeChange, setOpenModeChange] = useState(false) + // const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({ // root: { // backgroundColor: "transparent", @@ -373,7 +376,7 @@ export default function BinVisualizer(props: Props) { useEffect(() => { setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN); setStorageType(bin.storage()); - setNewPreset(pond.BinMode.BIN_MODE_NONE); + // setNewPreset(pond.BinMode.BIN_MODE_NONE); if (bin.settings.inventory) { setMoistureInput(bin.settings.inventory.initialMoisture.toString()); setCustomTypeName(bin.settings.inventory.customTypeName); @@ -2020,16 +2023,18 @@ export default function BinVisualizer(props: Props) { }; const setModeStorage = () => { - setNewPreset(pond.BinMode.BIN_MODE_STORAGE); + // setNewPreset(pond.BinMode.BIN_MODE_STORAGE); setNewBinMode(pond.BinMode.BIN_MODE_STORAGE); + setOpenModeChange(true) }; const setModeCooldown = () => { if (bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) { return; } - setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN); + // setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN); setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN); + setOpenModeChange(true) }; const setModeDrying = () => { @@ -2037,16 +2042,17 @@ export default function BinVisualizer(props: Props) { bin.settings.inventory && 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); } else { - setNewPreset(pond.BinMode.BIN_MODE_DRYING); + // setNewPreset(pond.BinMode.BIN_MODE_DRYING); setNewBinMode(pond.BinMode.BIN_MODE_DRYING); } + setOpenModeChange(true) }; const closeMoistureDialog = () => { - setNewPreset(0); + // setNewPreset(0); setNewBinMode(bin.settings.mode); setShowInputMoisture(false); }; @@ -2107,7 +2113,7 @@ export default function BinVisualizer(props: Props) { + {/* Confirm - button to enter storage and end conditioning */} + + + + ) + case pond.BinMode.BIN_MODE_COOLDOWN: + case pond.BinMode.BIN_MODE_DRYING: + case pond.BinMode.BIN_MODE_HYDRATING: + return ( + + Choose Device for {modeDescription()} Method + + + {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()}. + + {deviceSelector()} + { + //update the component sets that will be used for the interactions + console.log(sets) + }}/> + + + + + + + ) + } + } + + return ( + {close(false)}}> + {content()} + + ) +} \ No newline at end of file diff --git a/src/bin/conditioning/pickComponentSet.tsx b/src/bin/conditioning/pickComponentSet.tsx new file mode 100644 index 0000000..e2c9acf --- /dev/null +++ b/src/bin/conditioning/pickComponentSet.tsx @@ -0,0 +1,125 @@ +import { CheckBox, ExpandMore } from "@mui/icons-material"; +import { Accordion, AccordionDetails, AccordionSummary, Box, Checkbox, FormControlLabel, Typography } from "@mui/material"; +import { cloneDeep } from "lodash"; +import { Ambient } from "models/Ambient"; +import { Controller } from "models/Controller"; +import { Plenum } from "models/Plenum"; +import React, { useEffect, useState } from "react"; + +export interface ComponentSet { + sensor: Plenum | Ambient, + controllers: Controller[] +} + +interface Props { + sensor: Plenum | Ambient + heaters?: Controller[] + fans?: Controller[] + updateSet: (sensorKey: string, componentSet?: ComponentSet) => void +} + +export default function PickComponentSet(props: Props) { + const {sensor, heaters, fans, updateSet} = props + const [sensorSelected, setSensorSelected] = useState(false) + const [currentSet, setCurrentSet] = useState() + + const controllerIndex = (key: string) => { + if(!currentSet) return -1 + let index = -1 + currentSet.controllers.forEach((cont, i) => { + if(cont.key() === key){ + index = i + } + }) + return index + } + + return ( + + { + setSensorSelected(checked) + let newSet: ComponentSet | undefined + if(checked){ + newSet = { + sensor: sensor, + controllers: [] + } + } + setCurrentSet(newSet) + updateSet(sensor.key(), newSet) + }} + /> + } + label={sensor.name()} + /> + {sensorSelected && currentSet && + + }>Controllers Selected: {currentSet.controllers.length} + + {heaters && heaters.length > 0 && + + Heaters + {heaters.map(heater => ( + { + 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) + } + } + setCurrentSet(set) + updateSet(sensor.key(), set) + }} + /> + } + label={heater.name()} + /> + ))} + + } + {fans && fans.length > 0 && + + Fans + {fans.map(fan => ( + { + 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) + } + } + setCurrentSet(set) + updateSet(sensor.key(), set) + }} + /> + } + label={fan.name()} + /> + ))} + + } + + + } + + ) +} \ No newline at end of file From e70c971a88a801c52dc1df5f166aeab6f7cf2832 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 14 Nov 2025 16:21:18 -0600 Subject: [PATCH 05/11] built out the functions to create the interactions --- src/bin/BinSettings.tsx | 1 - src/bin/BinVisualizerV2.tsx | 1 + src/bin/conditioning/modeChangeDialog.tsx | 273 +++++++++++++++++++++- src/bin/conditioning/pickComponentSet.tsx | 19 ++ 4 files changed, 291 insertions(+), 3 deletions(-) diff --git a/src/bin/BinSettings.tsx b/src/bin/BinSettings.tsx index c65bd31..237a540 100644 --- a/src/bin/BinSettings.tsx +++ b/src/bin/BinSettings.tsx @@ -485,7 +485,6 @@ export default function BinSettings(props: Props) { form.inventory.inventoryControl = inventoryControl form.inventory.autoThreshold = autoFillThreshold } - console.log(form) binAPI .updateBin(bin.key(), form, as) .then(response => { diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index 767eaed..af4184b 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -2194,6 +2194,7 @@ export default function BinVisualizer(props: Props) { ([]) @@ -62,6 +69,9 @@ export default function ModeChangeDialog(props: Props){ const [ambients, setAmbients] = useState([]) const [heaters, setHeaters] = useState([]) const [fans, setFans] = useState([]) + const [selectedPreset, setSelectedPreset] = useState(undefined); + const grainExtensionMap = GetGrainExtensionMap(); + //get the interactions and components for the device when it is selected from the dropdown useEffect(()=>{ if (!binKey) return; @@ -85,6 +95,7 @@ export default function ModeChangeDialog(props: Props){ newDComponents.push(c); dComponents.set(deviceNumber.toString(), newDComponents); }); + console.log(dComponents) setDeviceComponents(dComponents); setExistingInteractions(interactionResp); }) @@ -141,9 +152,266 @@ if (!selectedDevice) return; setOptions(o); }, [devices, setOptions]); + const buildHeaterInteraction = ( + sensor: Plenum | Ambient, + heater: Controller + ): 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 (selectedPreset !== undefined) { + temp = selectedPreset.temp(); + hum = selectedPreset.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; + + return interaction; + }; + + const buildFanInteraction = ( + sensor: Plenum | Ambient, + fan: Controller, + tempComparison: "greater" | "less", + humidityComparison: "greater" | "less" + ) => { + 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 (selectedPreset !== undefined) { + tempPreset = selectedPreset.temp(); + humidityPreset = selectedPreset.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; + return interaction; + }; + + //function to loop through the interaction sets building the settings const createInteractions = () => { + if(!selectedDevice) return //if there is no device that was selected, do nothing + let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() + let linkedComponents = deviceComponents.get(selectedDevice.id().toString()); + //loop through the sets + componentSets.forEach(async (set) => { + //loop through the controllers + set.controllers.forEach(controller => { + //filter the conflicting interactions for that controller + let conflictingInteractions = existingInteractions.filter(i => { + let conflicting = false; + if (linkedComponents) { + linkedComponents.forEach(comp => { + if (sameComponentID(comp.location(), i.settings.sink)) { + conflicting = true; + } + }); + } + return conflicting; + }); + //create an array of promises to delete them + let deleteConflictingInteractions = conflictingInteractions.map(i => { + return interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as); + }); + //execute the promises + Promise.all([...deleteConflictingInteractions]).finally(() => { + //and one they are done make the new interaction to be added for the sensor and controller pair + 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)) + }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")) + } + 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)) + }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")) + } + } + 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")) + } + break; + } + console.log(multiInteractionSettings) + }) + + }) + }) + //add all of the interactions that were created (multiInteraction) + interactionAPI.addMultiInteractions(selectedDevice.id(), multiInteractionSettings).then(resp => { + //then once the interactions are added update the controllers to the proper states + + }).catch(err => { + //there was a problem adding the interactions + }) } const deviceSelector = () => { @@ -246,6 +514,7 @@ if (!selectedDevice) return; updateSets={(sets) => { //update the component sets that will be used for the interactions console.log(sets) + setComponentSets(sets) }}/> @@ -255,8 +524,8 @@ if (!selectedDevice) return; Cancel - - - - ); - }; + // const moistureDialog = () => { + // return ( + // + // Input new grain moisture + // + // + // 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. + // + // setMoistureInput(event.target.value)} + // InputProps={{ + // endAdornment: % + // }} + // style={{ marginTop: theme.spacing(2) }} + // fullWidth + // variant="outlined" + // /> + // + // Weather + // + // setTempInput(event.target.value)} + // InputProps={{ + // endAdornment: ( + // + // {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + // ? "°F" + // : "℃"} + // + // ) + // }} + // style={{ marginTop: theme.spacing(2) }} + // fullWidth + // variant="outlined" + // /> + // setOutdoorHumidityInput(event.target.value)} + // InputProps={{ + // endAdornment: % + // }} + // style={{ marginTop: theme.spacing(2) }} + // fullWidth + // variant="outlined" + // /> + // + // + // + // + // + // + // ); + // }; if (loading) { return ; @@ -2171,7 +2080,7 @@ export default function BinVisualizer(props: Props) { return ( - {moistureDialog()} + {/* {moistureDialog()} */} {cfmLowDialog()} {cfmHighDialog()} {changeGrain()} @@ -2201,11 +2110,15 @@ export default function BinVisualizer(props: Props) { compDevMap={componentDevices} preferences={preferences} onClose={(refresh) => { - setShowInputMoisture(false) setOpenModeChange(false) if(refresh) updateBin(); }} - + startChange={() => { + setModeChangeInProgress(true) + }} + changeComplete={() => { + setModeChangeInProgress(false) + }} /> {binMode()} diff --git a/src/bin/conditioning/modeChangeDialog.tsx b/src/bin/conditioning/modeChangeDialog.tsx index 06e1a8f..4d420e2 100644 --- a/src/bin/conditioning/modeChangeDialog.tsx +++ b/src/bin/conditioning/modeChangeDialog.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material"; +import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Stepper, Step, StepLabel, InputAdornment, Typography } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; import { useComponentAPI, useInteractionsAPI } from "hooks"; import { Component, Device, Interaction } from "models"; @@ -21,6 +21,7 @@ import { lowerCase } from "lodash"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { getTemperatureUnit } from "utils"; import { GetGrainExtensionMap } from "grain"; +import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress"; interface Props { open: boolean @@ -33,6 +34,13 @@ interface Props { binCables?: GrainCable[]; compDevMap?: Map; presets?: DevicePreset[]; + startChange: () => void; + changeComplete: () => void; + defaultTargetMoisture?: number; + changeTargetMoisture?: (newTarget: number) => {} + defaultOutdoorTemp?: number; + changeOutdoorTemp?: (newTemp: number) => {} + defaultOutdoorHumidity?: number; } interface Option { @@ -41,6 +49,11 @@ interface Option { icon?: string; } +interface DialogStep { + label: string; + completed?: boolean; +} + export default function ModeChangeDialog(props: Props){ const { open, @@ -51,10 +64,13 @@ export default function ModeChangeDialog(props: Props){ preferences, binCables, compDevMap, - grain + grain, + startChange, + changeComplete, + defaultTargetMoisture } = props const [{as}] = useGlobalState() - // const [componentSets, setComponentSets] = useState([]) + const [componentSets, setComponentSets] = useState([]) const interactionAPI = useInteractionsAPI() const componentAPI = useComponentAPI(); const [selectedDevice, setSelectedDevice] = useState() @@ -73,6 +89,16 @@ export default function ModeChangeDialog(props: Props){ const grainExtensionMap = GetGrainExtensionMap(); const [conflictingSetInteractions, setConflictingSetInteractions] = useState([]) const [interactionsToAdd, setInteractionsToAdd] = useState() + const [promiseStages, setPromiseStages] = useState([]) + const [dialogSteps, setDialogSteps] = useState([]) + const [currentStep, setCurrentStep] = useState(0) + + //the state variables for the moisture + const [moistureInput, setMoistureInput] = useState("") + + useEffect(()=>{ + setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0") + },[defaultTargetMoisture]) //get the interactions and components for the device when it is selected from the dropdown useEffect(()=>{ @@ -153,6 +179,39 @@ if (!selectedDevice) return; 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 @@ -235,7 +294,8 @@ if (!selectedDevice) return; }); 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; }; @@ -333,40 +393,72 @@ if (!selectedDevice) return; 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 execute = () => { - if(!selectedDevice || !interactionsToAdd) return //if there is no device selected or there are no interactions to add, do nothing - //create an array of promises to delete the conflicting interactions - let conflictingInteractionsPromise = conflictingSetInteractions.map(i => { - return interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as); + 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); }); - Promise.all([...conflictingInteractionsPromise]).then(() => { - //the conflicting interactions were removed now add the new ones - interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) - }).catch(err => { - //there was a problem removing any conflicting interactions + 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 => { + console.log(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 takes in the sets that are passed in and returns the promises needed for removing conflicting interactions and adding the new ones - * IT DOES NOT EXECUTE THEM ONLY CREATES THE PROMISES - * returns an object containing two values, - * one is an array of promises for conflicting interactions to remove and the other is the promise for adding the new interactions - * @param sets - * @returns - */ + //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(async (set) => { + sets.forEach((set) => { //loop through the controllers set.controllers.forEach(controller => { //filter the conflicting interactions for that controller @@ -419,6 +511,7 @@ if (!selectedDevice) return; } }) }) + setComponentSets([...sets]) return { conflicting: conflictingInteractions, toAdd: multiInteractionSettings @@ -460,102 +553,250 @@ if (!selectedDevice) return; const close = (refresh: boolean) => { + //clear state data + setCurrentStep(0) + setInteractionsToAdd(undefined) + setPromiseStages([]) onClose(refresh) } - const content = () => { + //this step will only be used for drying and hydrating + const moistureStep = () => { + return ( + + + 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. + + setMoistureInput(event.target.value)} + InputProps={{ + endAdornment: % + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + + Weather + + setTempInput(event.target.value)} + InputProps={{ + endAdornment: ( + + {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + ? "°F" + : "℃"} + + ) + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + setOutdoorHumidityInput(event.target.value)} + InputProps={{ + endAdornment: % + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + + ) + }; + + const selectDeviceStep = () => { switch(binMode){ case pond.BinMode.BIN_MODE_STORAGE: return ( - - Entering Storage Mode - - {binCables && binCables?.length > 0 && compDevMap && ( - - - - )} - - Select device to turn off controllers and end conditioning. Any interactions will be - left alone. Heaters/Fans will be turned off. - - {deviceSelector()} - - - - {/* Confirm - button to enter storage and end conditioning */} - - - - ) - case pond.BinMode.BIN_MODE_COOLDOWN: + + {binCables && binCables?.length > 0 && compDevMap && ( + + + + )} + + Select device to turn off controllers and end conditioning. Any interactions will be + left alone. Heaters/Fans will be turned off. + + {deviceSelector()} + + ) case pond.BinMode.BIN_MODE_DRYING: + case pond.BinMode.BIN_MODE_COOLDOWN: case pond.BinMode.BIN_MODE_HYDRATING: return ( - - Choose Device for {modeDescription()} Method - - - {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()}. - - {deviceSelector()} - { - //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){ - console.log(i) - setConflictingSetInteractions(i.conflicting) - setInteractionsToAdd(i.toAdd) - } - }}/> - - - - - - - ) + + + {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()}. + + {deviceSelector()} + { + //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) + } + }} + /> + + ) } } + const progressStep = () => { + return ( + + { + console.log("COMPLETED PROMISES") + setCurrentStep(0)//reset the stepper + changeComplete()//change the boolean preventing more mode changes + }} + /> + + ) + } + + const stepper = () => { + return ( + + {dialogSteps.map(dialogStep => { + const labelProps: { + optional?: React.ReactNode; + } = {}; + return ( + + {dialogStep.label} + + ); + })} + + ); + } + + const actions = () => { + let buttons: JSX.Element[] = [] + //this button should be on every step of all of them + buttons.push( + + ) + // 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( + + ) + } + // this button should appear if the step they are on is less than the last step -1 + if(currentStep < dialogSteps.length-2){ + buttons.push( + + ) + } + 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( + + ) + } + 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 ( + + {/* { + setOpenProgress(false) + close(refresh) + }} /> */} {close(false)}}> - {content()} + + {binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"} + + + {stepper()} + {stepperContent(currentStep)} + + + {actions()} + + ) } \ No newline at end of file diff --git a/src/common/ButtonGroup.tsx b/src/common/ButtonGroup.tsx index dfadd58..3c2c64c 100644 --- a/src/common/ButtonGroup.tsx +++ b/src/common/ButtonGroup.tsx @@ -19,6 +19,10 @@ export interface ButtonData { * The function to run when the button is clicked or toggled on when toggle is true */ function: () => void + /** + * whether the button is disabled or not + */ + disabled?: boolean } interface Props { @@ -49,6 +53,10 @@ interface Props { * Numerical value for the size of the text for buttons that dont use the icon */ 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) => { @@ -88,7 +96,7 @@ const useStyles = makeStyles((theme: Theme) => { }) 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 [currentToggle, setCurrentToggle] = useState([]) @@ -135,6 +143,7 @@ export default function ButtonGroup(props: Props){ {buttons.map((b, i) => ( + + + ) + +} \ No newline at end of file From 79c29f6f8121fac170ff39fe80b9dbb8979856e8 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 20 Nov 2025 11:07:14 -0600 Subject: [PATCH 08/11] increasing the limit for conditions on devices running 2.1.9 to 4, and changing the button logic for adding and removing them --- src/interactions/InteractionSettings.tsx | 43 +++++++++++------------- src/models/Device.ts | 12 +++++++ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/interactions/InteractionSettings.tsx b/src/interactions/InteractionSettings.tsx index 40b9132..2519669 100644 --- a/src/interactions/InteractionSettings.tsx +++ b/src/interactions/InteractionSettings.tsx @@ -957,29 +957,16 @@ export default function InteractionSettings(props: Props) { - {index === 0 ? ( - - 1 || !canEdit} - aria-label="Add another condition" - onClick={addCondition} - className={classNames(classes.greenButton, classes.noPadding)}> - - - - ) : ( - - removeCondition(index)} - className={classNames(classes.redButton, classes.noPadding)}> - - - - )} + + removeCondition(index)} + className={classNames(classes.redButton, classes.noPadding)}> + + + {multiNodeSource() && !sensor && ( @@ -1076,6 +1063,16 @@ export default function InteractionSettings(props: Props) { ) : ( You must select a source before adding conditions )} + + = device.maxConditions() || !canEdit} + aria-label="Add another condition" + onClick={addCondition} + className={classNames(classes.greenButton, classes.noPadding)}> + + + ); }; diff --git a/src/models/Device.ts b/src/models/Device.ts index de372ce..2577618 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -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 { let versions = featureVersions.get(feature); if (!versions) { From 004f54b1131d5eecb0406d083445256281240068 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 20 Nov 2025 11:27:14 -0600 Subject: [PATCH 09/11] updating the logic in the actual addCondition function --- src/interactions/InteractionSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interactions/InteractionSettings.tsx b/src/interactions/InteractionSettings.tsx index 2519669..87bdf98 100644 --- a/src/interactions/InteractionSettings.tsx +++ b/src/interactions/InteractionSettings.tsx @@ -357,7 +357,7 @@ export default function InteractionSettings(props: Props) { const addCondition = () => { let updatedInteraction = Interaction.clone(interaction); let updatedRawConditionValues = rawConditionValues; - if (numConditions < 2 && interaction.settings.source) { + if (numConditions < device.maxConditions() && interaction.settings.source) { let condition = pond.InteractionCondition.create(); condition.measurementType = getMeasurements( interaction.settings.source.type From 3df6db8a0128ce44c2ea23ab99877d9656002463 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 20 Nov 2025 15:39:29 -0600 Subject: [PATCH 10/11] finished the refactoring of the bin mode change to allow for more components to be selected --- src/bin/BinVisualizerV2.tsx | 146 +++--------- src/bin/conditioning/conditioningSelector.tsx | 14 +- src/bin/conditioning/modeChangeDialog.tsx | 222 ++++++++++-------- src/bin/conditioning/pickComponentSet.tsx | 198 ++++++++++++---- src/common/PromiseProgress.tsx | 40 ++-- src/pages/Bin.tsx | 2 - 6 files changed, 350 insertions(+), 272 deletions(-) diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index 4f9ae41..7d99671 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -7,11 +7,9 @@ import { darken, DialogActions, DialogContent, - DialogContentText, DialogTitle, Grid2 as Grid, IconButton, - InputAdornment, lighten, Link, Skeleton, @@ -19,8 +17,6 @@ import { Switch, TextField, Theme, - ToggleButton, - ToggleButtonGroup, Typography, useTheme, } from "@mui/material"; @@ -31,8 +27,8 @@ import HumidityIcon from "component/HumidityIcon"; import TemperatureIcon from "component/TemperatureIcon"; import GrainDescriber, { GrainOptions, ToGrainOption } from "grain/GrainDescriber"; import useViewport from "hooks/useViewport"; -import { cloneDeep, round } from "lodash"; -import { Bin, Component, Device, Interaction } from "models"; +import { round } from "lodash"; +import { Bin, Component, Device } from "models"; import { GetComponentIcon } from "pbHelpers/ComponentType"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { useMobile } from "hooks"; @@ -49,7 +45,6 @@ import { useBinAPI } from "providers/pond/binAPI"; import BindaptIcon from "products/Bindapt/BindaptIcon"; import { AccessTime, - ArrowForward, ArrowForwardIos, CheckCircleOutline, Error, @@ -59,7 +54,6 @@ import { TrendingUp, Warning } from "@mui/icons-material"; -import DevicePresetsFromPicker from "device/DevicePresetsFromPicker"; import { Plenum } from "models/Plenum"; import { Pressure } from "models/Pressure"; import { GrainCable } from "models/GrainCable"; @@ -73,7 +67,7 @@ import { Ambient } from "models/Ambient"; import { ExtractMoisture } from "grain"; import SearchSelect, { Option } from "common/SearchSelect"; import Edit from "@mui/icons-material/Edit"; -import { makeStyles, styled } from "@mui/styles"; +import { makeStyles } from "@mui/styles"; import ButtonGroup from "common/ButtonGroup"; import ModeChangeDialog from "./conditioning/modeChangeDialog"; @@ -191,8 +185,6 @@ interface Props { ambient?: Ambient; cables?: GrainCable[]; pressures?: Pressure[]; - interactions?: Interaction[]; - //changeBinMode: (binMode: pond.BinMode) => boolean; refresh: (showSnack?: boolean) => void; afterUpdate?(): void; preferences?: Map; @@ -204,8 +196,6 @@ interface Props { export default function BinVisualizer(props: Props) { const { bin, - //changeBinMode, - //changeGrainAmount, plenums, ambient, pressures, @@ -216,7 +206,6 @@ export default function BinVisualizer(props: Props) { preferences, componentDevices, permissions, - //interactions, refresh, updateComponentCallback, binPresets @@ -243,9 +232,9 @@ export default function BinVisualizer(props: Props) { const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour(); const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour(); // const [showInputMoisture, setShowInputMoisture] = useState(false); - const [moistureInput, setMoistureInput] = useState(""); - const [tempInput, setTempInput] = useState(""); - const [outdoorHumidityInput, setOutdoorHumidityInput] = useState(""); + const [targetMoisture, setTargetMoisture] = useState(0); + const [outdoorTemp, setOutdoorTemp] = useState(0); + const [outdoorHumidity, setOutdoorHumidity] = useState(0); const [modeTime, setModeTime] = useState(moment()); const [grainUpdate, setGrainUpdate] = useState(false); const [openStorageTime, setOpenStorageTime] = useState(false); @@ -331,7 +320,7 @@ export default function BinVisualizer(props: Props) { setStorageType(bin.storage()); // setNewPreset(pond.BinMode.BIN_MODE_NONE); if (bin.settings.inventory) { - setMoistureInput(bin.settings.inventory.initialMoisture.toString()); + setTargetMoisture(bin.settings.inventory.initialMoisture); setCustomTypeName(bin.settings.inventory.customTypeName); setGrainType(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) { 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]); useEffect(() => { @@ -774,7 +763,7 @@ export default function BinVisualizer(props: Props) { setIsCustomInventory(bin.storage() !== pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN); setStorageType(bin.storage()); if (bin.settings.inventory) { - setMoistureInput(bin.settings.inventory.initialMoisture.toString()); + setTargetMoisture(bin.settings.inventory.initialMoisture); setCustomTypeName(bin.settings.inventory.customTypeName); setGrainType(bin.settings.inventory.grainType); setGrainOption(ToGrainOption(bin.settings.inventory.grainType)); @@ -1960,83 +1949,6 @@ export default function BinVisualizer(props: Props) { setOpenModeChange(true) }; - // const closeMoistureDialog = () => { - // // setNewPreset(0); - // setNewBinMode(bin.settings.mode); - // setShowInputMoisture(false); - // }; - - // const moistureDialog = () => { - // return ( - // - // Input new grain moisture - // - // - // 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. - // - // setMoistureInput(event.target.value)} - // InputProps={{ - // endAdornment: % - // }} - // style={{ marginTop: theme.spacing(2) }} - // fullWidth - // variant="outlined" - // /> - // - // Weather - // - // setTempInput(event.target.value)} - // InputProps={{ - // endAdornment: ( - // - // {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT - // ? "°F" - // : "℃"} - // - // ) - // }} - // style={{ marginTop: theme.spacing(2) }} - // fullWidth - // variant="outlined" - // /> - // setOutdoorHumidityInput(event.target.value)} - // InputProps={{ - // endAdornment: % - // }} - // style={{ marginTop: theme.spacing(2) }} - // fullWidth - // variant="outlined" - // /> - // - // - // - // - // - // - // ); - // }; - if (loading) { return ; } @@ -2046,7 +1958,7 @@ export default function BinVisualizer(props: Props) { b.settings.storage = storageType; if (b.settings.inventory) { if (b.settings.inventory.initialMoisture) - b.settings.inventory.initialMoisture = Number(moistureInput); + b.settings.inventory.initialMoisture = targetMoisture; //update all the grain stuff b.settings.inventory.grainType = grainType; b.settings.inventory.customTypeName = customTypeName; @@ -2057,14 +1969,14 @@ export default function BinVisualizer(props: Props) { } if (b.settings.outdoorTemp !== undefined) { - let t = Number(tempInput); + let t = outdoorTemp; if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { - t = FtoC(Number(tempInput)); + t = FtoC(outdoorTemp); } b.settings.outdoorTemp = t; } if (b.settings.outdoorHumidity !== undefined) - b.settings.outdoorHumidity = Number(outdoorHumidityInput); + b.settings.outdoorHumidity = outdoorHumidity; if (b.settings.mode != newBinMode){ if(b.settings.inventory){ @@ -2080,26 +1992,10 @@ export default function BinVisualizer(props: Props) { return ( - {/* {moistureDialog()} */} {cfmLowDialog()} {cfmHighDialog()} {changeGrain()} {storageTimeDialog()} - {/* { - setShowInputMoisture(false); - setNewPreset(0) - if (success) updateBin(); - }} - parentPreset={newPreset} - grain={bin.settings.inventory?.grainType} - binCables={cables} - compDevMap={componentDevices} - presets={binPresets} - /> */} { 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) }} + /> {binMode()} diff --git a/src/bin/conditioning/conditioningSelector.tsx b/src/bin/conditioning/conditioningSelector.tsx index 9b98e20..79fdc25 100644 --- a/src/bin/conditioning/conditioningSelector.tsx +++ b/src/bin/conditioning/conditioningSelector.tsx @@ -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 { Controller } from "models/Controller" import { Plenum } from "models/Plenum" -import React, { useEffect, useState } from "react" +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 { @@ -12,12 +14,14 @@ interface Props { 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} = props + const {plenums, ambients, heaters, fans, updateSets, presets, binMode} = props const [currentSets, setCurrentSets] = useState([]) const sensorIndex = (key: string) => { @@ -64,6 +68,8 @@ export default function ConditioningSelector(props: Props){ sensor={plenum} heaters={heaters} fans={fans} + binMode={binMode} + presets={presets} updateSet={setUpdate} /> )})} @@ -74,6 +80,8 @@ export default function ConditioningSelector(props: Props){ sensor={ambient} heaters={heaters} fans={fans} + binMode={binMode} + presets={presets} updateSet={setUpdate} /> )})} diff --git a/src/bin/conditioning/modeChangeDialog.tsx b/src/bin/conditioning/modeChangeDialog.tsx index 4d420e2..630b158 100644 --- a/src/bin/conditioning/modeChangeDialog.tsx +++ b/src/bin/conditioning/modeChangeDialog.tsx @@ -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 { useComponentAPI, useInteractionsAPI } from "hooks"; import { Component, Device, Interaction } from "models"; @@ -19,7 +19,7 @@ import { sameComponentID } from "pbHelpers/Component"; import moment from "moment"; import { lowerCase } from "lodash"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; -import { getTemperatureUnit } from "utils"; +import { fahrenheitToCelsius, getTemperatureUnit } from "utils"; import { GetGrainExtensionMap } from "grain"; import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress"; @@ -37,10 +37,11 @@ interface Props { startChange: () => void; changeComplete: () => void; defaultTargetMoisture?: number; - changeTargetMoisture?: (newTarget: number) => {} + changeTargetMoisture?: (newTarget: number) => void defaultOutdoorTemp?: number; - changeOutdoorTemp?: (newTemp: number) => {} + changeOutdoorTemp?: (newTemp: number) => void defaultOutdoorHumidity?: number; + changeOutdoorHumidity?: (newHumidity: number) => void } interface Option { @@ -67,7 +68,13 @@ export default function ModeChangeDialog(props: Props){ grain, startChange, changeComplete, - defaultTargetMoisture + defaultTargetMoisture, + defaultOutdoorTemp, + defaultOutdoorHumidity, + changeTargetMoisture, + changeOutdoorTemp, + changeOutdoorHumidity, + presets } = props const [{as}] = useGlobalState() const [componentSets, setComponentSets] = useState([]) @@ -85,20 +92,24 @@ export default function ModeChangeDialog(props: Props){ const [ambients, setAmbients] = useState([]) const [heaters, setHeaters] = useState([]) const [fans, setFans] = useState([]) - const [selectedPreset, setSelectedPreset] = useState(undefined); const grainExtensionMap = GetGrainExtensionMap(); const [conflictingSetInteractions, setConflictingSetInteractions] = useState([]) const [interactionsToAdd, setInteractionsToAdd] = useState() const [promiseStages, setPromiseStages] = useState([]) const [dialogSteps, setDialogSteps] = useState([]) const [currentStep, setCurrentStep] = useState(0) + const theme = useTheme() //the state variables for the moisture const [moistureInput, setMoistureInput] = useState("") + const [outdoorTempInput, setOutdoorTempInput] = useState("") + const [outdoorHumidityInput, setOutdoorHumidityInput] = useState("") useEffect(()=>{ 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 useEffect(()=>{ @@ -214,7 +225,8 @@ if (!selectedDevice) return; const buildHeaterInteraction = ( sensor: Plenum | Ambient, - heater: Controller + heater: Controller, + customPreset?: DevicePreset ): pond.InteractionSettings => { let interaction = pond.InteractionSettings.create({ source: sensor.location(), @@ -236,11 +248,10 @@ if (!selectedDevice) return; let temp = 0; let hum = 0; - //if a preset was selected use its temp/hum values - if (selectedPreset !== undefined) { - temp = selectedPreset.temp(); - hum = selectedPreset.hum(); + if (customPreset !== undefined) { + temp = customPreset.temp(); + hum = customPreset.hum(); } else { //otherwise use default values switch(binMode){ @@ -303,7 +314,8 @@ if (!selectedDevice) return; sensor: Plenum | Ambient, fan: Controller, tempComparison: "greater" | "less", - humidityComparison: "greater" | "less" + humidityComparison: "greater" | "less", + customPreset?: DevicePreset ) => { let interaction = pond.InteractionSettings.create({ source: sensor.location(), @@ -323,13 +335,13 @@ if (!selectedDevice) return; }) }); - let tempPreset = 0; - let humidityPreset = 0; + let tempPreset = 0; + let humidityPreset = 0; //if a custom preset was selected use it - if (selectedPreset !== undefined) { - tempPreset = selectedPreset.temp(); - humidityPreset = selectedPreset.hum(); + if (customPreset !== undefined) { + tempPreset = customPreset.temp(); + humidityPreset = customPreset.hum(); } else { //otherwise use one of the defaults switch (binMode) { @@ -437,7 +449,6 @@ if (!selectedDevice) return; } componentSets.forEach(set => { set.controllers.forEach(controller => { - console.log(controller) let newStep: PromiseStep = { title: "Updating " + controller.name(), promise: componentAPI.update(selectedDevice.id(), controller.settings) @@ -461,6 +472,7 @@ if (!selectedDevice) return; 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; @@ -478,17 +490,17 @@ if (!selectedDevice) return; 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)) + 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")) + 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)) + 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 @@ -496,7 +508,7 @@ if (!selectedDevice) return; controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON }else{ //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; @@ -505,7 +517,7 @@ if (!selectedDevice) return; 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")) + multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "greater", customPreset)) } break; } @@ -563,55 +575,81 @@ if (!selectedDevice) return; //this step will only be used for drying and hydrating const moistureStep = () => { return ( - - - 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. - - setMoistureInput(event.target.value)} - InputProps={{ - endAdornment: % - }} - style={{ marginTop: theme.spacing(2) }} - fullWidth - variant="outlined" - /> - - Weather - - setTempInput(event.target.value)} - InputProps={{ - endAdornment: ( - - {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT - ? "°F" - : "℃"} - - ) - }} - style={{ marginTop: theme.spacing(2) }} - fullWidth - variant="outlined" - /> - setOutdoorHumidityInput(event.target.value)} - InputProps={{ - endAdornment: % - }} - style={{ marginTop: theme.spacing(2) }} - fullWidth - variant="outlined" - /> - + + + 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. + + { + setMoistureInput(event.target.value) + if(changeTargetMoisture && !isNaN(parseFloat(event.target.value))){ + changeTargetMoisture(parseFloat(event.target.value)) + } + }} + InputProps={{ + endAdornment: % + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + + Weather + + { + 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: ( + + {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + ? "°F" + : "℃"} + + ) + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + { + setOutdoorHumidityInput(event.target.value) + if(changeOutdoorHumidity && !isNaN(parseFloat(event.target.value))){ + changeOutdoorHumidity(parseFloat(event.target.value)) + } + }} + InputProps={{ + endAdornment: % + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + ) }; @@ -652,6 +690,8 @@ if (!selectedDevice) return; 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 @@ -673,9 +713,13 @@ if (!selectedDevice) return; { + startChange() + }} onComplete={()=>{ - console.log("COMPLETED PROMISES") + close(true) setCurrentStep(0)//reset the stepper changeComplete()//change the boolean preventing more mode changes }} @@ -686,13 +730,13 @@ if (!selectedDevice) return; const stepper = () => { return ( - - {dialogSteps.map(dialogStep => { + + {dialogSteps.map((dialogStep, i) => { const labelProps: { optional?: React.ReactNode; } = {}; return ( - + {dialogStep.label} ); @@ -705,24 +749,24 @@ if (!selectedDevice) return; let buttons: JSX.Element[] = [] //this button should be on every step of all of them buttons.push( - + ) // 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( - + ) } // this button should appear if the step they are on is less than the last step -1 if(currentStep < dialogSteps.length-2){ buttons.push( - + ) } - 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){ + 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( - ) } @@ -778,13 +821,6 @@ if (!selectedDevice) return; return ( - {/* { - setOpenProgress(false) - close(refresh) - }} /> */} {close(false)}}> {binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"} diff --git a/src/bin/conditioning/pickComponentSet.tsx b/src/bin/conditioning/pickComponentSet.tsx index 87572ed..7848547 100644 --- a/src/bin/conditioning/pickComponentSet.tsx +++ b/src/bin/conditioning/pickComponentSet.tsx @@ -1,30 +1,89 @@ 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 { 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 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} = props + const {sensor, heaters, fans, updateSet, presets, binMode} = props const [sensorSelected, setSensorSelected] = useState(false) const [currentSet, setCurrentSet] = useState() + 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 @@ -64,7 +123,8 @@ export default function PickComponentSet(props: Props) { if(checked){ newSet = { sensor: sensor, - controllers: [] + controllers: [], + selectedPresets: new Map() } } setCurrentSet(newSet) @@ -78,32 +138,59 @@ export default function PickComponentSet(props: Props) { }>Controllers Selected: {currentSet.controllers.length} - {heaters && heaters.length > 0 && + {heaters && heaters.length > 0 && binMode !== pond.BinMode.BIN_MODE_HYDRATING && Heaters {heaters.map(heater => ( - { - 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) - }} + + + { + 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()} /> - } - label={heater.name()} - /> + + + {presets && getOptionsForController(pond.ControllerType.CONTROLLER_TYPE_HEATER).length > 0 && + 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 => } + /> + } + + ))} } @@ -111,28 +198,55 @@ export default function PickComponentSet(props: Props) { Fans {fans.map(fan => ( - { - 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) + + + { + 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()} + /> + + + {presets && getOptionsForController(pond.ControllerType.CONTROLLER_TYPE_FAN).length > 0 && + 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) } } - set.combo = comboCheck(set) - setCurrentSet(set) - updateSet(sensor.key(), set) }} + renderInput={params => } /> } - label={fan.name()} - /> + + ))} } diff --git a/src/common/PromiseProgress.tsx b/src/common/PromiseProgress.tsx index 1b3b1d8..41b71cd 100644 --- a/src/common/PromiseProgress.tsx +++ b/src/common/PromiseProgress.tsx @@ -1,6 +1,5 @@ import { AxiosResponse } from "axios" -import ResponsiveDialog from "./ResponsiveDialog" -import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Icon, Typography } from "@mui/material" +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" @@ -19,7 +18,9 @@ export interface Step { interface Props { stages: Stage[] + automaticStart: boolean failFast?: boolean + onStart?: () => void onComplete?: () => void } @@ -31,7 +32,7 @@ enum progress { } export function PromiseProgress(props: Props){ - const {stages, failFast, onComplete} = props + const {stages, failFast, onStart, onComplete, automaticStart} = props const [completion, setCompletion] = useState>(new Map()) const execute = () => { @@ -52,7 +53,7 @@ export function PromiseProgress(props: Props){ progMap.set(i+"-"+k, progress.InProgress); setCompletion(new Map(progMap)); return step.promise - .then(resp => { + .then(() => { // mark step as completed progMap.set(i+"-"+k, progress.Complete); setCompletion(new Map(progMap)); @@ -73,13 +74,22 @@ export function PromiseProgress(props: Props){ await Promise.all(stepPromises); } }; - runStages().catch(err => { - console.error("Execution stopped due to error:", err); - }).finally(() => { - if(onComplete) onComplete() - }); + 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: @@ -116,11 +126,13 @@ export function PromiseProgress(props: Props){ {stages.map((stage, number) => displayStage(stage, number))} - + {!automaticStart && + + } ) diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index c352634..1fb51fb 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -698,8 +698,6 @@ export default function Bin(props: Props) { components={components} componentDevices={componentDevices} devices={devices} - interactions={interactions} - //changeBinMode={changeBinMode} afterUpdate={refresh} refresh={refresh} preferences={preferences} From 3cce11db6b988b7f370dfb6c6f6943db7f4f6f6d Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 20 Nov 2025 16:17:16 -0600 Subject: [PATCH 11/11] setting the default option to the first device in the list --- src/bin/conditioning/modeChangeDialog.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/bin/conditioning/modeChangeDialog.tsx b/src/bin/conditioning/modeChangeDialog.tsx index 630b158..2913e4a 100644 --- a/src/bin/conditioning/modeChangeDialog.tsx +++ b/src/bin/conditioning/modeChangeDialog.tsx @@ -105,6 +105,8 @@ export default function ModeChangeDialog(props: Props){ const [outdoorTempInput, setOutdoorTempInput] = useState("") const [outdoorHumidityInput, setOutdoorHumidityInput] = useState("") + const [deviceOption, setDeviceOption] = useState