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"; 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 { getTemperatureUnit } from "utils"; 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; binCables?: GrainCable[]; compDevMap?: Map; presets?: DevicePreset[]; startChange: () => void; changeComplete: () => void; defaultTargetMoisture?: number; changeTargetMoisture?: (newTarget: number) => {} defaultOutdoorTemp?: number; changeOutdoorTemp?: (newTemp: number) => {} defaultOutdoorHumidity?: number; } 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 } = props const [{as}] = useGlobalState() const [componentSets, setComponentSets] = useState([]) const interactionAPI = useInteractionsAPI() const componentAPI = useComponentAPI(); const [selectedDevice, setSelectedDevice] = useState() const [existingInteractions, setExistingInteractions] = useState([]) const [loading, setLoading] = useState(false) const [options, setOptions] = useState([]) const [deviceComponents, setDeviceComponents] = useState>( new Map() ); const [plenums, setPlenums] = useState([]) 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) //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(()=>{ 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(); 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]); //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 ): 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; //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 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; //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); }); 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 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 => { //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)) }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; } }) }) setComponentSets([...sets]) return { conflicting: conflictingInteractions, toAdd: multiInteractionSettings } } const deviceSelector = () => { return ( option.label || ""} onChange={(_, newValue) => { if(newValue){ setSelectedDevice(newValue.device); } }} renderInput={params => } /> ) } 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 ( 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 ( {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 ( {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)}}> {binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"} {stepper()} {stepperContent(currentStep)} {actions()} ) }