the component sets are being properly constructed and passed up to the mode dialog that will control adding the interactions to them

This commit is contained in:
csawatzky 2025-11-10 15:21:43 -06:00
parent ea81610635
commit 393a2f77dc
4 changed files with 512 additions and 9 deletions

View file

@ -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>(pond.BinMode.BIN_MODE_NONE);
// const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [newBinMode, setNewBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [selectedCable, setSelectedCable] = useState<GrainCable>();
const [openNodeDialog, setOpenNodeDialog] = useState(false);
@ -293,6 +294,8 @@ export default function BinVisualizer(props: Props) {
const [activePlenumIndex, setActivePlenumIndex] = useState(0)
const [combinedPlenums, setCombinedPlenums] = useState<CombinedPlenum[]>([])
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) {
<DialogActions>
<Button
onClick={() => {
setNewPreset(0);
// setNewPreset(0);
setNewBinMode(bin.settings.mode);
setShowInputMoisture(false);
}}
@ -2170,7 +2176,7 @@ export default function BinVisualizer(props: Props) {
{cfmHighDialog()}
{changeGrain()}
{storageTimeDialog()}
<DevicePresetsFromPicker
{/* <DevicePresetsFromPicker
preferences={preferences}
binKey={bin.key()}
devices={devices}
@ -2184,6 +2190,21 @@ export default function BinVisualizer(props: Props) {
binCables={cables}
compDevMap={componentDevices}
presets={binPresets}
/> */}
<ModeChangeDialog
binKey={bin.key()}
binMode={newBinMode}
devices={devices}
open={openModeChange}
binCables={cables}
compDevMap={componentDevices}
preferences={preferences}
onClose={(refresh) => {
setShowInputMoisture(false)
setOpenModeChange(false)
if(refresh) updateBin();
}}
/>
<Box paddingRight={1}>
{binMode()}

View file

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

View file

@ -0,0 +1,275 @@
import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useComponentAPI, useInteractionsAPI } from "hooks";
import { Component, Device, Interaction } from "models";
import { Ambient } from "models/Ambient";
import { Controller } from "models/Controller";
import { DevicePreset } from "models/DevicePreset";
import { GrainCable } from "models/GrainCable";
import { Plenum } from "models/Plenum";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState } from "providers";
import React from "react";
import { useEffect, useState } from "react";
import ConditioningSelector from "./conditioningSelector";
import CableTopNodeSummary from "bin/CableTopNodeSummary";
import { ComponentSet } from "./pickComponentSet";
interface Props {
open: boolean
onClose: (refresh: boolean) => void
binKey: string;
devices: Device[];
binMode: pond.BinMode;
grain?: pond.Grain;
preferences?: Map<string, pond.BinComponentPreferences>;
binCables?: GrainCable[];
compDevMap?: Map<string, number>;
presets?: DevicePreset[];
}
interface Option {
label: string;
device: Device;
icon?: string;
}
export default function ModeChangeDialog(props: Props){
const {
open,
onClose,
binMode,
devices,
binKey,
preferences,
binCables,
compDevMap
} = props
const [{as}] = useGlobalState()
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
const interactionAPI = useInteractionsAPI()
const componentAPI = useComponentAPI();
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
const [loading, setLoading] = useState(false)
const [options, setOptions] = useState<Option[]>([])
const [deviceComponents, setDeviceComponents] = useState<Map<string, Component[]>>(
new Map<string, Component[]>()
);
const [plenums, setPlenums] = useState<Plenum[]>([])
const [ambients, setAmbients] = useState<Ambient[]>([])
const [heaters, setHeaters] = useState<Controller[]>([])
const [fans, setFans] = useState<Controller[]>([])
//get the interactions and components for the device when it is selected from the dropdown
useEffect(()=>{
if (!binKey) return;
if (!selectedDevice) return
let comps = deviceComponents.get(selectedDevice.id().toString());
if (!comps) {
comps = [];
setLoading(true);
let interactionPromise = interactionAPI.listInteractionsByDevice(selectedDevice.id(), undefined, as);
let componentPromise = componentAPI.list(selectedDevice.id(), undefined, [binKey], ["bin"], true, as);
let dComponents = new Map<string, Component[]>();
Promise.all([componentPromise, interactionPromise])
.then(([compResp, interactionResp]) => {
let d = pond.ListComponentsResponse.fromObject(compResp.data);
d.components.forEach(comp => {
let c = Component.create(comp);
let deviceNumber = compResp.data.componentDevices[c.key()];
comps!.push(c);
let newDComponents = dComponents.get(deviceNumber.toString());
if (newDComponents === undefined) newDComponents = [];
newDComponents.push(c);
dComponents.set(deviceNumber.toString(), newDComponents);
});
setDeviceComponents(dComponents);
setExistingInteractions(interactionResp);
})
.finally(() => {
setLoading(false);
});
}
},[selectedDevice, binKey, componentAPI, interactionAPI, deviceComponents, as])
//sort the components according to the bin preferences
useEffect(()=>{
if (loading) return;
if (!selectedDevice) return;
if (!deviceComponents.get(selectedDevice.id().toString())) return;
var plenums: Plenum[] = [];
var ambients: Ambient[] = [];
//var grainCables: GrainCable[] = [];
var heaters: Controller[] = [];
var fans: Controller[] = [];
deviceComponents.get(selectedDevice.id().toString())!.forEach(comp => {
let pref = preferences?.get(comp.key());
if (pref) {
if (pref.type) {
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM)
plenums.push(Plenum.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT)
ambients.push(Ambient.create(comp));
// if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE)
// grainCables.push(GrainCable.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) {
let heater = Controller.create(comp);
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
heaters.push(heater);
}
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
let fan = Controller.create(comp)
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
fans.push(fan);
}
}
}
});
setPlenums(plenums);
setAmbients(ambients);
setHeaters(heaters);
setFans(fans);
}, [loading, setPlenums, setHeaters, deviceComponents, selectedDevice, preferences]);
useEffect(() => {
let o: Option[] = [];
devices.forEach(device => {
o.push({ device: device, label: device.name() });
});
setOptions(o);
}, [devices, setOptions]);
//function to loop through the interaction sets building the settings
const createInteractions = () => {
}
const deviceSelector = () => {
return (
<Autocomplete
disablePortal
options={options}
fullWidth
getOptionLabel={option => option.label || ""}
onChange={(_, newValue) => {
if(newValue){
setSelectedDevice(newValue.device);
}
}}
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
/>
)
}
const modeDescription = () => {
switch(binMode){
case pond.BinMode.BIN_MODE_DRYING:
return "Drying"
case pond.BinMode.BIN_MODE_HYDRATING:
return "Hydrating"
case pond.BinMode.BIN_MODE_COOLDOWN:
return "Cooldown"
case pond.BinMode.BIN_MODE_STORAGE:
return "Storage"
default:
return ""
}
}
const close = (refresh: boolean) => {
onClose(refresh)
}
const content = () => {
switch(binMode){
case pond.BinMode.BIN_MODE_STORAGE:
return (
<React.Fragment>
<DialogTitle>Entering Storage Mode</DialogTitle>
<DialogContent>
{binCables && binCables?.length > 0 && compDevMap && (
<Box marginBottom={2}>
<CableTopNodeSummary
binKey={binKey}
cables={binCables}
componentDevMap={compDevMap}
/>
</Box>
)}
<DialogContentText>
Select device to turn off controllers and end conditioning. Any interactions will be
left alone. Heaters/Fans will be turned off.
</DialogContentText>
{deviceSelector()}
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={()=>{
close(false)
}}>
Close
</Button>
{/* Confirm - button to enter storage and end conditioning */}
<Button variant="contained" color="primary" onClick={() => {
close(true)
if (selectedDevice) {
//update the controllers to be off for the selected device
// updateControllers();
}
}}>
Confirm
</Button>
</DialogActions>
</React.Fragment>
)
case pond.BinMode.BIN_MODE_COOLDOWN:
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_HYDRATING:
return (
<React.Fragment>
<DialogTitle>Choose Device for {modeDescription()} Method</DialogTitle>
<DialogContent>
<DialogContentText>
{modeDescription()} control is handled by interactions between multiple components on the
same device. Please choose which device and components will handle this bin's{" "}
{modeDescription()}.
</DialogContentText>
{deviceSelector()}
<ConditioningSelector
plenums={plenums}
ambients={ambients}
fans={fans}
heaters={heaters}
updateSets={(sets) => {
//update the component sets that will be used for the interactions
console.log(sets)
}}/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={()=>{
close(false)
}}>
Cancel
</Button>
<Button variant="contained" color="primary" onClick={()=>{
//remove comflicting interactions
//add new interactions to the component sets
close(true)
}}>
Submit
</Button>
</DialogActions>
</React.Fragment>
)
}
}
return (
<ResponsiveDialog open={open} onClose={() => {close(false)}}>
{content()}
</ResponsiveDialog>
)
}

View file

@ -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<ComponentSet | undefined>()
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 (
<Box>
<FormControlLabel
control={
<Checkbox
value={sensorSelected}
checked={sensorSelected}
onChange={(_, checked) => {
setSensorSelected(checked)
let newSet: ComponentSet | undefined
if(checked){
newSet = {
sensor: sensor,
controllers: []
}
}
setCurrentSet(newSet)
updateSet(sensor.key(), newSet)
}}
/>
}
label={sensor.name()}
/>
{sensorSelected && currentSet &&
<Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>Controllers Selected: {currentSet.controllers.length}</AccordionSummary>
<AccordionDetails>
{heaters && heaters.length > 0 &&
<React.Fragment>
<Typography>Heaters</Typography>
{heaters.map(heater => (
<FormControlLabel
key={heater.key()}
control={
<Checkbox
onChange={(_, checked) => {
let set = cloneDeep(currentSet)
if(checked){//if checked need to add it to the controllers
set.controllers.push(heater)
}else{//will need to make sure it is not in the controllers
let index = controllerIndex(heater.key())
if (index !== -1){
set.controllers.splice(index, 1)
}
}
setCurrentSet(set)
updateSet(sensor.key(), set)
}}
/>
}
label={heater.name()}
/>
))}
</React.Fragment>
}
{fans && fans.length > 0 &&
<React.Fragment>
<Typography>Fans</Typography>
{fans.map(fan => (
<FormControlLabel
key={fan.key()}
control={
<Checkbox
onChange={(_, checked) => {
let set = cloneDeep(currentSet)
if(checked){//if checked need to add it to the controllers
set.controllers.push(fan)
}else{//will need to make sure it is not in the controllers
let index = controllerIndex(fan.key())
if (index !== -1){
set.controllers.splice(index, 1)
}
}
setCurrentSet(set)
updateSet(sensor.key(), set)
}}
/>
}
label={fan.name()}
/>
))}
</React.Fragment>
}
</AccordionDetails>
</Accordion>
}
</Box>
)
}