frontend/src/bin/binModes/DryingMode.tsx

519 lines
No EOL
20 KiB
TypeScript

import { ArrowBack, ArrowForward } from "@mui/icons-material";
import { Box, Theme, Stepper, Step, StepLabel, Typography, RadioGroup, Radio, FormControlLabel, Grid2, Button, Autocomplete, TextField, DialogTitle, DialogContent, DialogActions, CircularProgress } from "@mui/material";
import { makeStyles } from "@mui/styles";
import ConditioningSelector from "bin/conditioning/conditioningSelector";
import { Component, Device, Interaction } from "models";
import { Ambient } from "models/Ambient";
import { Controller } from "models/Controller";
import { Plenum } from "models/Plenum";
import { pond, quack } from "protobuf-ts/pond";
import { useEffect, useMemo, useState } from "react";
import { ComponentSet } from "bin/conditioning/pickComponentSet"
import { componentIDToString, sameComponentID } from "pbHelpers/Component";
import moment from "moment";
import { lowerCase } from "lodash";
import { GetGrainExtensionMap } from "grain";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { useGlobalState, useInteractionsAPI } from "providers";
import BinConditioningInteraction from "bin/BinConditioningInteraction";
import ConditionDisplay from "./conditionDisplay";
import React from "react";
const useStyles = makeStyles((theme: Theme) => {
return ({
stepper: {
padding: theme.spacing(0.5)
},
})
});
interface Step {
label: string;
completed?: boolean;
}
export const DryingBaseStepCount = 3
interface Option {
label: string;
device: Device;
icon?: string;
}
interface Props {
devices: Device[]
deviceComponents: Map<number, Component[]>
binPrefs?: Map<string, pond.BinComponentPreferences>
grain?: pond.Grain
cancel: () => void
confirm: (conflictingInteractions: Interaction[], deviceId: number, newInteractions: pond.MultiInteractionSettings, componentSets: ComponentSet[]) => void
}
const steps = [{label: "Style"}, {label: "Device"}, {label: "Interaction"}]
//this is our simple drying mode the way it currently works, just building the dialog to be a little more trnasparent and customizable as to what the interaction is doing
export default function DryingMode(props: Props){
const {devices, deviceComponents, binPrefs, grain, cancel, confirm} = props
const grainExtensionMap = GetGrainExtensionMap();
const classes = useStyles()
const [{user}] = useGlobalState()
const interactionAPI = useInteractionsAPI()
const [dryingMethod, setDryingMethod] = useState("air")//whether they are using natural air or a heater
const [options, setOptions] = useState<Option[]>([])
const [deviceOption, setDeviceOption] = useState<Option>()
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
const [plenums, setPlenums] = useState<Plenum[]>([])
const [ambients, setAmbients] = useState<Ambient[]>([])
const [heaters, setHeaters] = useState<Controller[]>([])
const [fans, setFans] = useState<Controller[]>([])
const [currentStep, setCurrentStep] = useState(0)
const [interactionLoading,setInteractionsLoading] = useState(false)
// possibly all 4 of these will need to be passed up, at least the conflicting, toAdd, and sets will
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
const [conflictingInteractions, setConflictingInteractions] = useState<Interaction[]>([])
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
const [sourceMap, setSourceMap] = useState<Map<string, Component>>(new Map());
const [sinkMap, setSinkMap] = useState<Map<string, Component>>(new Map());
//sort the components according to the bin preferences
useEffect(()=>{
if (!selectedDevice) return;
if (!deviceComponents.get(selectedDevice.id())) return;
var plenums: Plenum[] = [];
var ambients: Ambient[] = [];
//var grainCables: GrainCable[] = [];
var heaters: Controller[] = [];
var fans: Controller[] = [];
var sinkMap: Map<string, Component> = new Map()
var sourceMap: Map<string, Component> = new Map()
deviceComponents.get(selectedDevice.id())!.forEach(comp => {
let pref = binPrefs?.get(comp.key());
if (pref) {
if (pref.type) {
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM){
plenums.push(Plenum.create(comp));
sourceMap.set(comp.locationString(), comp)
}
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT){
ambients.push(Ambient.create(comp));
sourceMap.set(comp.locationString(), 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);
sinkMap.set(comp.locationString(), comp)
}
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
let fan = Controller.create(comp)
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
fans.push(fan);
sinkMap.set(comp.locationString(), comp)
}
}
}
});
setPlenums(plenums);
setAmbients(ambients);
setHeaters(heaters);
setFans(fans);
setSourceMap(sourceMap)
setSinkMap(sinkMap)
//also load the interactions for the selected device so that we can find any conflicting ones
setInteractionsLoading(true)
interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => {
setExistingInteractions(resp)
}).finally(() => {
setInteractionsLoading(false)
})
}, [deviceComponents, selectedDevice, binPrefs]);
useEffect(() => {
let o: Option[] = [];
devices.forEach(device => {
let newOption: Option = {device: device, label: device.name()}
if(deviceOption === undefined) {
setDeviceOption(newOption)
setSelectedDevice(device)
}
o.push(newOption);
});
setOptions(o);
}, [devices, setOptions]);
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(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,
undefined,
user
).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 (user.tempUnit() === 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,
undefined,
user
).toStored(temp);
let tempCondition = pond.InteractionCondition.create({
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: Math.round(tempVal) //and then round the converted value since interaction conditions wont take floats
});
conditions.push(tempCondition);
interaction.conditions = conditions;
//the the heater output to auto so that when the components are update it uses the new mode
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
return interaction;
};
const buildFanInteraction = (
sensor: Plenum | Ambient,
fan: Controller,
tempComparison: "greater" | "less",
humidityComparison: "greater" | "less",
) => {
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(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
}
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,
undefined,
user
).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 (user.tempUnit() === 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,
undefined,
user
).toStored(tempPreset);
let fanConditionTwo = pond.InteractionCondition.create({
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
comparison:
tempComparison === "greater"
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: Math.round(tempVal) //and then round the converted value since the interaction does not take decimals
});
conditions.push(fanConditionTwo);
interaction.conditions = conditions;
//set the output mode to auto in the fan so that when the components are updated it uses the new mode
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
return interaction;
};
//this 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());
//loop through the sets
sets.forEach((set) => {
//loop through the controllers
set.controllers.forEach(controller => {
//filter the conflicting interactions for that controller
let c = existingInteractions.filter(i => {
let conflicting = false;
if (linkedComponents) {
linkedComponents.forEach(comp => {
if (sameComponentID(comp.location(), i.settings.sink)) {
conflicting = true;
}
});
}
return conflicting;
});
conflictingInteractions = conflictingInteractions.concat(c)
// 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"))
}
}
})
})
setComponentSets([...sets])
return {
conflicting: conflictingInteractions,
toAdd: multiInteractionSettings
}
}
const stepper = () => {
return (
<Stepper
activeStep={currentStep}
alternativeLabel
classes={{
root: classes.stepper
}}>
{steps.map((s, i) => (
<Step key={i}>
<StepLabel>{s.label}</StepLabel>
</Step>
))}
</Stepper>
);
}
const styleStep = () => {
return (
<Box>
<Typography>Select your drying method</Typography>
<RadioGroup
value={dryingMethod}
onChange={(_, value) => {
setDryingMethod(value)
}}>
<FormControlLabel
control={<Radio />}
value={"air"}
label={"Natural Air"}
/>
<FormControlLabel
control={<Radio />}
value={"heat"}
label={"Supplemental Heat"}
/>
</RadioGroup>
</Box>
)
}
const deviceSelector = () => {
return (
<Autocomplete
disablePortal
options={options}
value={deviceOption}
fullWidth
getOptionLabel={option => option.label || ""}
onChange={(_, newValue) => {
if(newValue){
setSelectedDevice(newValue.device);
}
}}
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
/>
)
}
const deviceStep = () => {
return (
<Box>
<Typography>Devices</Typography>
{deviceSelector()}
{interactionLoading ? <CircularProgress /> :
<ConditioningSelector
plenums={plenums}
ambients={ambients}
fans={fans}
heaters={heaters}
//presets={presets}
binMode={pond.BinMode.BIN_MODE_DRYING}
updateSets={(sets) => {
//update the component sets that will be used for the interactions
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
let i = createInteractions(sets)
if(i !== undefined){
setConflictingInteractions(i.conflicting)
setInteractionsToAdd(i.toAdd)
}
}}
/>
}
</Box>
)
}
//this step will use the conflicting interactions and interactions to add to display them and allow users to make changes
const interactionStep = () => {
console.log(interactionsToAdd)
return (
<Box>
<Typography>Interactions</Typography>
{interactionsToAdd?.interactions.map((interaction, index) => {
let temp = Interaction.create()
temp.settings = interaction
let sink = sinkMap.get(componentIDToString(interaction.sink))
let source = sourceMap.get(componentIDToString(interaction.source))
if(sink && source){
return (
<ConditionDisplay
key={index}
interaction={temp}
sink={sink}
source={source}
changeConditions={(newSettings) => {
console.log(newSettings)
}}
/>
)
}
})}
</Box>
)
}
const stepperContent = () => {
switch(currentStep){
case 1:
return deviceStep()
case 2:
return interactionStep()
default:
return styleStep()
}
}
const actions = () => {
return (
<React.Fragment>
{/* close - tells the parent to close the dialog without doing anything, always visible */}
<Button onClick={cancel}>Close</Button>
{/* back - goes back to the previous step, hidden on the forst step */}
{currentStep !== 0 && <Button onClick={() => {setCurrentStep(currentStep-1)}}>Back</Button>}
{/* next - goes to the next step, hidden on the last step */}
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>}
{/* confirm - tells the parent to build the stages using the data, only visible on the last step */}
{currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && <Button onClick={() => {confirm(conflictingInteractions, selectedDevice.id(), interactionsToAdd, componentSets)}}>Confirm</Button>}
</React.Fragment>
)
}
return (
<React.Fragment>
<DialogTitle>Bin Drying</DialogTitle>
<DialogContent>
{stepper()}
{stepperContent()}
</DialogContent>
<DialogActions>
{actions()}
</DialogActions>
</React.Fragment>
)
}