855 lines
No EOL
31 KiB
TypeScript
855 lines
No EOL
31 KiB
TypeScript
import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Stepper, Step, StepLabel, InputAdornment, Typography, useTheme } from "@mui/material";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import { useComponentAPI, useInteractionsAPI } from "hooks";
|
|
import { Component, Device, Interaction } from "models";
|
|
import { Ambient } from "models/Ambient";
|
|
import { Controller } from "models/Controller";
|
|
import { DevicePreset } from "models/DevicePreset";
|
|
import { GrainCable } from "models/GrainCable";
|
|
import { Plenum } from "models/Plenum";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { quack } from "protobuf-ts/quack";
|
|
import { useGlobalState } from "providers";
|
|
import React from "react";
|
|
import { useEffect, useState } from "react";
|
|
import ConditioningSelector from "./conditioningSelector";
|
|
import CableTopNodeSummary from "bin/CableTopNodeSummary";
|
|
import { ComponentSet } from "./pickComponentSet";
|
|
import { sameComponentID } from "pbHelpers/Component";
|
|
import moment from "moment";
|
|
import { lowerCase } from "lodash";
|
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
|
import { GetGrainExtensionMap } from "grain";
|
|
import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress";
|
|
|
|
interface Props {
|
|
open: boolean
|
|
onClose: (refresh: boolean) => void
|
|
binKey: string;
|
|
devices: Device[];
|
|
binMode: pond.BinMode;
|
|
grain?: pond.Grain;
|
|
preferences?: Map<string, pond.BinComponentPreferences>;
|
|
binCables?: GrainCable[];
|
|
compDevMap?: Map<string, number>;
|
|
presets?: DevicePreset[];
|
|
startChange: () => void;
|
|
changeComplete: () => void;
|
|
defaultTargetMoisture?: number;
|
|
changeTargetMoisture?: (newTarget: number) => void
|
|
defaultOutdoorTemp?: number;
|
|
changeOutdoorTemp?: (newTemp: number) => void
|
|
defaultOutdoorHumidity?: number;
|
|
changeOutdoorHumidity?: (newHumidity: number) => void
|
|
}
|
|
|
|
interface Option {
|
|
label: string;
|
|
device: Device;
|
|
icon?: string;
|
|
}
|
|
|
|
interface DialogStep {
|
|
label: string;
|
|
completed?: boolean;
|
|
}
|
|
|
|
export default function ModeChangeDialog(props: Props){
|
|
const {
|
|
open,
|
|
onClose,
|
|
binMode,
|
|
devices,
|
|
binKey,
|
|
preferences,
|
|
binCables,
|
|
compDevMap,
|
|
grain,
|
|
startChange,
|
|
changeComplete,
|
|
defaultTargetMoisture,
|
|
defaultOutdoorTemp,
|
|
defaultOutdoorHumidity,
|
|
changeTargetMoisture,
|
|
changeOutdoorTemp,
|
|
changeOutdoorHumidity,
|
|
presets
|
|
} = props
|
|
const [{as, user}] = useGlobalState()
|
|
const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
|
|
const interactionAPI = useInteractionsAPI()
|
|
const componentAPI = useComponentAPI();
|
|
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
|
|
const [existingInteractions, setExistingInteractions] = useState<Interaction[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [options, setOptions] = useState<Option[]>([])
|
|
const [deviceComponents, setDeviceComponents] = useState<Map<string, Component[]>>(
|
|
new Map<string, Component[]>()
|
|
);
|
|
|
|
const [plenums, setPlenums] = useState<Plenum[]>([])
|
|
const [ambients, setAmbients] = useState<Ambient[]>([])
|
|
const [heaters, setHeaters] = useState<Controller[]>([])
|
|
const [fans, setFans] = useState<Controller[]>([])
|
|
const grainExtensionMap = GetGrainExtensionMap();
|
|
const [conflictingSetInteractions, setConflictingSetInteractions] = useState<Interaction[]>([])
|
|
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
|
|
const [promiseStages, setPromiseStages] = useState<Stage[]>([])
|
|
const [dialogSteps, setDialogSteps] = useState<DialogStep[]>([])
|
|
const [currentStep, setCurrentStep] = useState(0)
|
|
const theme = useTheme()
|
|
|
|
//the state variables for the moisture
|
|
const [moistureInput, setMoistureInput] = useState<string>("")
|
|
const [outdoorTempInput, setOutdoorTempInput] = useState<string>("")
|
|
const [outdoorHumidityInput, setOutdoorHumidityInput] = useState<string>("")
|
|
|
|
const [deviceOption, setDeviceOption] = useState<Option>()
|
|
|
|
useEffect(()=>{
|
|
setMoistureInput(defaultTargetMoisture?.toString() ?? "0")
|
|
setOutdoorTempInput(defaultOutdoorTemp?.toString() ?? "0")
|
|
setOutdoorHumidityInput(defaultOutdoorHumidity?.toString() ?? "0")
|
|
},[defaultTargetMoisture, defaultOutdoorTemp, defaultOutdoorHumidity])
|
|
|
|
//get the interactions and components for the device when it is selected from the dropdown
|
|
useEffect(()=>{
|
|
if (!binKey) return;
|
|
if (!selectedDevice) return
|
|
let comps = deviceComponents.get(selectedDevice.id().toString());
|
|
if (!comps) {
|
|
comps = [];
|
|
setLoading(true);
|
|
let interactionPromise = interactionAPI.listInteractionsByDevice(selectedDevice.id(), undefined, as);
|
|
let componentPromise = componentAPI.list(selectedDevice.id(), undefined, [binKey], ["bin"], true, as);
|
|
let dComponents = new Map<string, Component[]>();
|
|
Promise.all([componentPromise, interactionPromise])
|
|
.then(([compResp, interactionResp]) => {
|
|
let d = pond.ListComponentsResponse.fromObject(compResp.data);
|
|
d.components.forEach(comp => {
|
|
let c = Component.create(comp);
|
|
let deviceNumber = compResp.data.componentDevices[c.key()];
|
|
comps!.push(c);
|
|
let newDComponents = dComponents.get(deviceNumber.toString());
|
|
if (newDComponents === undefined) newDComponents = [];
|
|
newDComponents.push(c);
|
|
dComponents.set(deviceNumber.toString(), newDComponents);
|
|
});
|
|
setDeviceComponents(dComponents);
|
|
setExistingInteractions(interactionResp);
|
|
})
|
|
.finally(() => {
|
|
setLoading(false);
|
|
});
|
|
}
|
|
},[selectedDevice, binKey, componentAPI, interactionAPI, deviceComponents, as])
|
|
|
|
//sort the components according to the bin preferences
|
|
useEffect(()=>{
|
|
if (loading) return;
|
|
if (!selectedDevice) return;
|
|
if (!deviceComponents.get(selectedDevice.id().toString())) return;
|
|
var plenums: Plenum[] = [];
|
|
var ambients: Ambient[] = [];
|
|
//var grainCables: GrainCable[] = [];
|
|
var heaters: Controller[] = [];
|
|
var fans: Controller[] = [];
|
|
deviceComponents.get(selectedDevice.id().toString())!.forEach(comp => {
|
|
let pref = preferences?.get(comp.key());
|
|
if (pref) {
|
|
if (pref.type) {
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM)
|
|
plenums.push(Plenum.create(comp));
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT)
|
|
ambients.push(Ambient.create(comp));
|
|
// if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE)
|
|
// grainCables.push(GrainCable.create(comp));
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) {
|
|
let heater = Controller.create(comp);
|
|
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
|
heaters.push(heater);
|
|
}
|
|
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) {
|
|
let fan = Controller.create(comp)
|
|
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
|
|
fans.push(fan);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
setPlenums(plenums);
|
|
setAmbients(ambients);
|
|
setHeaters(heaters);
|
|
setFans(fans);
|
|
}, [loading, setPlenums, setHeaters, deviceComponents, selectedDevice, preferences]);
|
|
|
|
useEffect(() => {
|
|
let o: Option[] = [];
|
|
devices.forEach(device => {
|
|
let newOption: Option = {device: device, label: device.name()}
|
|
if(deviceOption === undefined) {
|
|
setDeviceOption(newOption)
|
|
setSelectedDevice(device)
|
|
}
|
|
o.push(newOption);
|
|
});
|
|
setOptions(o);
|
|
}, [devices, setOptions]);
|
|
|
|
|
|
//use effect here to determine the steps based on the bin mode
|
|
useEffect(() => {
|
|
let steps: DialogStep[] = []
|
|
switch(binMode){
|
|
case pond.BinMode.BIN_MODE_STORAGE:
|
|
steps.push({
|
|
label: "Storage Settings"
|
|
})
|
|
break;
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
steps.push({
|
|
label: "Set Components"
|
|
})
|
|
steps.push({
|
|
label: "Progress"
|
|
})
|
|
break;
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
|
steps.push({
|
|
label: "Weather Conditions"
|
|
})
|
|
steps.push({
|
|
label: "Set Components"
|
|
})
|
|
steps.push({
|
|
label: "Progress"
|
|
})
|
|
}
|
|
setDialogSteps(steps)
|
|
},[binMode])
|
|
|
|
const buildHeaterInteraction = (
|
|
sensor: Plenum | Ambient,
|
|
heater: Controller,
|
|
customPreset?: DevicePreset
|
|
): pond.InteractionSettings => {
|
|
let interaction = pond.InteractionSettings.create({
|
|
source: sensor.location(),
|
|
sink: heater.location(),
|
|
schedule: pond.InteractionSchedule.create({
|
|
timeOfDayStart: "00:00",
|
|
timeOfDayEnd: "24:00",
|
|
timezone: moment.tz.guess(),
|
|
weekdays: moment.weekdays().map(d => lowerCase(d))
|
|
}),
|
|
notifications: pond.InteractionNotifications.create({
|
|
reports: true
|
|
}),
|
|
result: pond.InteractionResult.create({
|
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
|
value: 1
|
|
})
|
|
});
|
|
|
|
let temp = 0;
|
|
let hum = 0;
|
|
//if a preset was selected use its temp/hum values
|
|
if (customPreset !== undefined) {
|
|
temp = customPreset.temp();
|
|
hum = customPreset.hum();
|
|
} else {
|
|
//otherwise use default values
|
|
switch(binMode){
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
temp = 10
|
|
hum = 45
|
|
break;
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
if(grain){
|
|
//get the values using the grain type
|
|
let ext = grainExtensionMap.get(grain)
|
|
if (ext) {
|
|
temp = ext.setTempC
|
|
hum = ext.targetMC
|
|
}
|
|
}else{//otherwise use the default drying interaction
|
|
temp = 40
|
|
hum = 20
|
|
}
|
|
}
|
|
}
|
|
|
|
let conditions = [];
|
|
let humidityCondition = pond.InteractionCondition.create({
|
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
|
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
|
|
value: Math.round(
|
|
describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
|
sensor.settings.type,
|
|
sensor.settings.subtype,
|
|
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",
|
|
customPreset?: DevicePreset
|
|
) => {
|
|
let interaction = pond.InteractionSettings.create({
|
|
source: sensor.location(),
|
|
sink: fan.location(),
|
|
schedule: pond.InteractionSchedule.create({
|
|
timeOfDayStart: "00:00",
|
|
timeOfDayEnd: "24:00",
|
|
timezone: moment.tz.guess(),
|
|
weekdays: moment.weekdays().map(d => lowerCase(d))
|
|
}),
|
|
notifications: pond.InteractionNotifications.create({
|
|
reports: true
|
|
}),
|
|
result: pond.InteractionResult.create({
|
|
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
|
|
value: 1
|
|
})
|
|
});
|
|
|
|
let tempPreset = 0;
|
|
let humidityPreset = 0;
|
|
|
|
//if a custom preset was selected use it
|
|
if (customPreset !== undefined) {
|
|
tempPreset = customPreset.temp();
|
|
humidityPreset = customPreset.hum();
|
|
} else {
|
|
//otherwise use one of the defaults
|
|
switch (binMode) {
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
tempPreset = 15;
|
|
humidityPreset = 60;
|
|
break;
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
if(grain){
|
|
//get the values using the grain type
|
|
let ext = grainExtensionMap.get(grain)
|
|
if (ext) {
|
|
tempPreset = ext.setTempC
|
|
humidityPreset = ext.targetMC
|
|
}
|
|
}else{//otherwise use the default drying interaction
|
|
tempPreset = 40
|
|
humidityPreset = 20
|
|
}
|
|
break;
|
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
|
tempPreset = 25;
|
|
humidityPreset = 60;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let conditions = [];
|
|
let fanConditionOne = pond.InteractionCondition.create({
|
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
|
comparison:
|
|
humidityComparison === "greater"
|
|
? quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
|
|
: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
|
|
value: describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
|
sensor.settings.type,
|
|
sensor.settings.subtype,
|
|
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 is what will use the conflicting interactions
|
|
const buildPromises = () => {
|
|
//if there is no device selected or there are no interactions to add, do nothing
|
|
if(!selectedDevice || !interactionsToAdd) return
|
|
|
|
//build the stages to pass into the PromiseProgress
|
|
//stage one is to remove conflicting interactions
|
|
let stages: Stage[] = []
|
|
let stage1: Stage = {
|
|
title: "Remove Conflicting Interactions",
|
|
steps: []
|
|
}
|
|
conflictingSetInteractions.forEach(i => {
|
|
let newStep: PromiseStep = {
|
|
title: "Removing Interaction",
|
|
promise: () => interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as)
|
|
}
|
|
stage1.steps.push(newStep);
|
|
});
|
|
if(stage1.steps.length > 0){
|
|
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)
|
|
}
|
|
]
|
|
}
|
|
if(stage2.steps.length > 0){
|
|
stages.push(stage2)
|
|
}
|
|
|
|
//stage three is to update the controller components
|
|
let stage3: Stage = {
|
|
title: "Update Controllers",
|
|
steps: []
|
|
}
|
|
componentSets.forEach(set => {
|
|
set.controllers.forEach(controller => {
|
|
let newStep: PromiseStep = {
|
|
title: "Updating " + controller.name(),
|
|
promise: () => componentAPI.update(selectedDevice.id(), controller.settings)
|
|
}
|
|
stage3.steps.push(newStep)
|
|
})
|
|
})
|
|
if(stage3.steps.length > 0){
|
|
stages.push(stage3)
|
|
}
|
|
//set those stages to a state variable
|
|
setPromiseStages(stages)
|
|
}
|
|
|
|
|
|
//this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
|
|
const createInteractions = (sets: ComponentSet[]) => {
|
|
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
|
|
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
|
|
let conflictingInteractions: Interaction[] = []
|
|
let linkedComponents = deviceComponents.get(selectedDevice.id().toString());
|
|
//loop through the sets
|
|
sets.forEach((set) => {
|
|
//loop through the controllers
|
|
set.controllers.forEach(controller => {
|
|
let customPreset = set.selectedPresets.get(controller.key())
|
|
//filter the conflicting interactions for that controller
|
|
let c = existingInteractions.filter(i => {
|
|
let conflicting = false;
|
|
if (linkedComponents) {
|
|
linkedComponents.forEach(comp => {
|
|
if (sameComponentID(comp.location(), i.settings.sink)) {
|
|
conflicting = true;
|
|
}
|
|
});
|
|
}
|
|
return conflicting;
|
|
});
|
|
conflictingInteractions = conflictingInteractions.concat(c)
|
|
switch(binMode){
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
// if the controller is a heater create heater interaction
|
|
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
|
|
multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller, customPreset))
|
|
}else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
|
|
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){
|
|
// if the controller is a fan create a fan interaction
|
|
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "less", customPreset))
|
|
}
|
|
break;
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
// if the controller is a heater create heater interaction
|
|
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
|
|
multiInteractionSettings.interactions.push(buildHeaterInteraction(set.sensor, controller, customPreset))
|
|
}else if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
|
|
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){// if the controller is a fan
|
|
//if it is a combination set using a heater as well just turn the fan on
|
|
if(set.combo){
|
|
controller.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_ON
|
|
}else{
|
|
//otherwise make a fan interaction
|
|
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "greater", "less", customPreset))
|
|
}
|
|
}
|
|
break;
|
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
|
//check to make sure the controller is a fan, you cant hydrate with a heater, if the controller is a heater then it will not add an interaction
|
|
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN ||
|
|
controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN){
|
|
//create fan interaction
|
|
multiInteractionSettings.interactions.push(buildFanInteraction(set.sensor, controller, "less", "greater", customPreset))
|
|
}
|
|
break;
|
|
}
|
|
})
|
|
})
|
|
setComponentSets([...sets])
|
|
return {
|
|
conflicting: conflictingInteractions,
|
|
toAdd: multiInteractionSettings
|
|
}
|
|
}
|
|
|
|
const deviceSelector = () => {
|
|
return (
|
|
<Autocomplete
|
|
disablePortal
|
|
options={options}
|
|
value={deviceOption}
|
|
fullWidth
|
|
getOptionLabel={option => option.label || ""}
|
|
onChange={(_, newValue) => {
|
|
if(newValue){
|
|
setSelectedDevice(newValue.device);
|
|
}
|
|
}}
|
|
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const modeDescription = () => {
|
|
switch(binMode){
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
return "Drying"
|
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
|
return "Hydrating"
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
return "Cooldown"
|
|
case pond.BinMode.BIN_MODE_STORAGE:
|
|
return "Storage"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const close = (refresh: boolean) => {
|
|
//clear state data
|
|
setCurrentStep(0)
|
|
setInteractionsToAdd(undefined)
|
|
setPromiseStages([])
|
|
onClose(refresh)
|
|
}
|
|
|
|
//this step will only be used for drying and hydrating
|
|
const moistureStep = () => {
|
|
return (
|
|
<Box>
|
|
<DialogContentText style={{ paddingBottom: theme.spacing(0) }}>
|
|
Updating the current grain moisture will calibrate the estimate for more accurate
|
|
results. The outdoor temperature will have some effect on the estimate as well; consider
|
|
updating with drastic changes in the weather or using a predicted average.
|
|
</DialogContentText>
|
|
<TextField
|
|
label={"Grain Moisture"}
|
|
value={moistureInput}
|
|
error={isNaN(parseFloat(moistureInput))}
|
|
helperText={isNaN(parseFloat(moistureInput)) ? "Must be a valid number" : ""}
|
|
onChange={event => {
|
|
setMoistureInput(event.target.value)
|
|
if(changeTargetMoisture && !isNaN(parseFloat(event.target.value))){
|
|
changeTargetMoisture(parseFloat(event.target.value))
|
|
}
|
|
}}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">%</InputAdornment>
|
|
}}
|
|
style={{ marginTop: theme.spacing(2) }}
|
|
fullWidth
|
|
variant="outlined"
|
|
/>
|
|
<Typography variant="body1" style={{ marginTop: theme.spacing(2) }}>
|
|
Weather
|
|
</Typography>
|
|
<TextField
|
|
label={"Outdoor Temperature"}
|
|
value={outdoorTempInput}
|
|
error={isNaN(parseFloat(outdoorTempInput))}
|
|
helperText={isNaN(parseFloat(outdoorTempInput)) ? "Must be a valid number" : ""}
|
|
onChange={event => {
|
|
setOutdoorTempInput(event.target.value)
|
|
if(changeOutdoorTemp && !isNaN(parseFloat(event.target.value))){
|
|
let val = parseFloat(event.target.value)
|
|
changeOutdoorTemp(val)
|
|
}
|
|
}}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
{user.tempUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
|
? "°F"
|
|
: "℃"}
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
style={{ marginTop: theme.spacing(2) }}
|
|
fullWidth
|
|
variant="outlined"
|
|
/>
|
|
<TextField
|
|
label={"Outdoor Humidity"}
|
|
value={outdoorHumidityInput}
|
|
error={isNaN(parseFloat(outdoorHumidityInput))}
|
|
helperText={isNaN(parseFloat(outdoorHumidityInput)) ? "Must be a valid number" : ""}
|
|
onChange={event => {
|
|
setOutdoorHumidityInput(event.target.value)
|
|
if(changeOutdoorHumidity && !isNaN(parseFloat(event.target.value))){
|
|
changeOutdoorHumidity(parseFloat(event.target.value))
|
|
}
|
|
}}
|
|
InputProps={{
|
|
endAdornment: <InputAdornment position="end">%</InputAdornment>
|
|
}}
|
|
style={{ marginTop: theme.spacing(2) }}
|
|
fullWidth
|
|
variant="outlined"
|
|
/>
|
|
</Box>
|
|
)
|
|
};
|
|
|
|
const selectDeviceStep = () => {
|
|
switch(binMode){
|
|
case pond.BinMode.BIN_MODE_STORAGE:
|
|
return (
|
|
<Box>
|
|
{binCables && binCables?.length > 0 && compDevMap && (
|
|
<Box marginBottom={2}>
|
|
<CableTopNodeSummary
|
|
binKey={binKey}
|
|
cables={binCables}
|
|
componentDevMap={compDevMap}
|
|
/>
|
|
</Box>
|
|
)}
|
|
<DialogContentText>
|
|
Select device to turn off controllers and end conditioning. Any interactions will be
|
|
left alone. Heaters/Fans will be turned off.
|
|
</DialogContentText>
|
|
{deviceSelector()}
|
|
</Box>
|
|
)
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
|
return (
|
|
<Box>
|
|
<DialogContentText>
|
|
{modeDescription()} control is handled by interactions between multiple components on the
|
|
same device. Please choose which device and components will handle this bin's{" "}
|
|
{modeDescription()}.
|
|
</DialogContentText>
|
|
{deviceSelector()}
|
|
<ConditioningSelector
|
|
plenums={plenums}
|
|
ambients={ambients}
|
|
fans={fans}
|
|
heaters={heaters}
|
|
presets={presets}
|
|
binMode={binMode}
|
|
updateSets={(sets) => {
|
|
//update the component sets that will be used for the interactions
|
|
//when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list
|
|
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
|
|
let i = createInteractions(sets)
|
|
if(i !== undefined){
|
|
setConflictingSetInteractions(i.conflicting)
|
|
setInteractionsToAdd(i.toAdd)
|
|
}
|
|
}}
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|
|
}
|
|
|
|
const progressStep = () => {
|
|
return (
|
|
<Box>
|
|
<PromiseProgress
|
|
stages={promiseStages}
|
|
description="These are the changes that will occur in order to change your bin mode with the given options. Press start to begin."
|
|
failFast
|
|
onStart={() => {
|
|
startChange()
|
|
}}
|
|
onComplete={()=>{
|
|
close(true)
|
|
setCurrentStep(0)//reset the stepper
|
|
changeComplete()//change the boolean preventing more mode changes
|
|
}}
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const stepper = () => {
|
|
return (
|
|
<Stepper nonLinear activeStep={currentStep} style={{ width: "100%", marginBottom: theme.spacing(2) }}>
|
|
{dialogSteps.map((dialogStep, i) => {
|
|
const labelProps: {
|
|
optional?: React.ReactNode;
|
|
} = {};
|
|
return (
|
|
<Step key={i} completed={dialogStep.completed}>
|
|
<StepLabel {...labelProps}>{dialogStep.label}</StepLabel>
|
|
</Step>
|
|
);
|
|
})}
|
|
</Stepper>
|
|
);
|
|
}
|
|
|
|
const actions = () => {
|
|
let buttons: JSX.Element[] = []
|
|
//this button should be on every step of all of them
|
|
buttons.push(
|
|
<Button key={"close"} onClick={() => {close(false)}}>Close</Button>
|
|
)
|
|
// this button should appear if the step they are on is not 0 and not the last step
|
|
if(currentStep !== 0 && currentStep !== dialogSteps.length-1){
|
|
buttons.push(
|
|
<Button key={"back"} onClick={()=>{setCurrentStep(currentStep-1)}}>Back</Button>
|
|
)
|
|
}
|
|
// this button should appear if the step they are on is less than the last step -1
|
|
if(currentStep < dialogSteps.length-2){
|
|
buttons.push(
|
|
<Button key={"next"} onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>
|
|
)
|
|
}
|
|
if(((binMode === pond.BinMode.BIN_MODE_STORAGE || binMode === pond.BinMode.BIN_MODE_COOLDOWN) && currentStep === 0) ||
|
|
(binMode === pond.BinMode.BIN_MODE_DRYING || binMode === pond.BinMode.BIN_MODE_HYDRATING) && currentStep === 1){
|
|
buttons.push(
|
|
<Button key={"confirm"} onClick={() => {
|
|
//if the bin mode is storage, just update the controllers for the selected device and close
|
|
if(binMode === pond.BinMode.BIN_MODE_STORAGE){
|
|
close(true)
|
|
if (selectedDevice) {
|
|
//update the controllers to be off for the selected device
|
|
heaters.forEach(heater => {
|
|
componentAPI
|
|
.update(selectedDevice.id(), heater.settings, undefined, undefined, as)
|
|
.then()
|
|
.catch();
|
|
});
|
|
fans.forEach(fan => {
|
|
componentAPI
|
|
.update(selectedDevice.id(), fan.settings, undefined, undefined, as)
|
|
.then()
|
|
.catch();
|
|
});
|
|
}
|
|
}else{//if it is cooldown, drying, or hydrating, it will need to go to the progress step and initiate all of the promises
|
|
buildPromises()
|
|
setCurrentStep(dialogSteps.length-1)//set the stepper to the progress step
|
|
}
|
|
}}>Confirm</Button>
|
|
)
|
|
}
|
|
return buttons
|
|
}
|
|
|
|
const stepperContent = (step: number) => {
|
|
switch(binMode){
|
|
case pond.BinMode.BIN_MODE_STORAGE:
|
|
return selectDeviceStep();
|
|
case pond.BinMode.BIN_MODE_COOLDOWN:
|
|
switch (step) {
|
|
case 1:
|
|
return progressStep();
|
|
default:
|
|
return selectDeviceStep();
|
|
}
|
|
case pond.BinMode.BIN_MODE_DRYING:
|
|
case pond.BinMode.BIN_MODE_HYDRATING:
|
|
switch (step) {
|
|
case 1:
|
|
return selectDeviceStep();
|
|
case 2:
|
|
return progressStep();
|
|
default:
|
|
return moistureStep();
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<ResponsiveDialog open={open} onClose={() => {close(false)}}>
|
|
<DialogTitle>
|
|
{binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"}
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
{stepper()}
|
|
{stepperContent(currentStep)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
{actions()}
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
</React.Fragment>
|
|
)
|
|
} |