finished the functional part of storage as well as modified the drying dialog to remove notification interactions from the cables

This commit is contained in:
csawatzky 2026-07-10 11:59:42 -06:00
parent 2c6799bcc1
commit 208a657528
7 changed files with 193 additions and 47 deletions

View file

@ -11,12 +11,25 @@ import { PromiseProgress, Stage, Step } from "common/PromiseProgress";
import { useBinAPI, useComponentAPI, useInteractionsAPI } from "providers"; import { useBinAPI, useComponentAPI, useInteractionsAPI } from "providers";
import { ComponentSet } from "bin/conditioning/pickComponentSet"; import { ComponentSet } from "bin/conditioning/pickComponentSet";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Controller } from "models/Controller";
export interface DeviceChangeData { export interface DeviceChangeData {
/**
* the id of the device changes are being made to
*/
deviceId: number deviceId: number
/**
* the array of interactions to remove from the device
*/
toRemove: Interaction[] toRemove: Interaction[]
/**
* the new interactions to add to the device
*/
toAdd?: pond.MultiInteractionSettings toAdd?: pond.MultiInteractionSettings
componentSets: ComponentSet[] /**
* the controllers that need to have there state updated
*/
controllers: Controller[]
} }
interface Props { interface Props {
@ -99,8 +112,8 @@ export default function BinModeController(props: Props) {
promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions) promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions)
}) })
} }
data.componentSets.forEach(set => {
set.controllers.forEach(controller => { data.controllers.forEach(controller => {
let newStep: Step = { let newStep: Step = {
title: "Updating " + controller.name(), title: "Updating " + controller.name(),
promise: () => componentAPI.update(data.deviceId, controller.settings) promise: () => componentAPI.update(data.deviceId, controller.settings)
@ -108,7 +121,7 @@ export default function BinModeController(props: Props) {
stage3.steps.push(newStep) stage3.steps.push(newStep)
}) })
}) })
})
if(stage1.steps.length > 0){ if(stage1.steps.length > 0){
stages.push(stage1) stages.push(stage1)
} }
@ -195,7 +208,15 @@ export default function BinModeController(props: Props) {
case pond.BinMode.BIN_MODE_COOLDOWN: case pond.BinMode.BIN_MODE_COOLDOWN:
return <CooldownMode /> return <CooldownMode />
default: default:
return <StorageMode devices={devices} confirm={()=>{}}/> return <StorageMode
devices={devices}
binPrefs={binPrefs}
bin={bin}
deviceComponents={deviceComponents}
confirm={(deviceData)=>{
buildStages(deviceData)
setShowProgress(true)
}}/>
} }
} }

View file

@ -19,6 +19,7 @@ import BinConditioningInteraction from "bin/BinConditioningInteraction";
import ConditionDisplay from "./conditionDisplay"; import ConditionDisplay from "./conditionDisplay";
import React from "react"; import React from "react";
import { DeviceChangeData } from "./BinModeController"; import { DeviceChangeData } from "./BinModeController";
import { GrainCable } from "models/GrainCable";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
@ -66,6 +67,7 @@ export default function DryingMode(props: Props){
const [ambients, setAmbients] = useState<Ambient[]>([]) const [ambients, setAmbients] = useState<Ambient[]>([])
const [heaters, setHeaters] = useState<Controller[]>([]) const [heaters, setHeaters] = useState<Controller[]>([])
const [fans, setFans] = useState<Controller[]>([]) const [fans, setFans] = useState<Controller[]>([])
const [cables, setCables] = useState<GrainCable[]>([])
const [currentStep, setCurrentStep] = useState(0) const [currentStep, setCurrentStep] = useState(0)
const [interactionLoading,setInteractionsLoading] = useState(false) const [interactionLoading,setInteractionsLoading] = useState(false)
@ -87,7 +89,7 @@ export default function DryingMode(props: Props){
if (!deviceComponents.get(selectedDevice.id())) return; if (!deviceComponents.get(selectedDevice.id())) return;
var plenums: Plenum[] = []; var plenums: Plenum[] = [];
var ambients: Ambient[] = []; var ambients: Ambient[] = [];
//var grainCables: GrainCable[] = []; var grainCables: GrainCable[] = [];
var heaters: Controller[] = []; var heaters: Controller[] = [];
var fans: Controller[] = []; var fans: Controller[] = [];
var sinkMap: Map<string, Component> = new Map() var sinkMap: Map<string, Component> = new Map()
@ -105,8 +107,10 @@ export default function DryingMode(props: Props){
ambients.push(Ambient.create(comp)); ambients.push(Ambient.create(comp));
sourceMap.set(comp.locationString(), comp) sourceMap.set(comp.locationString(), comp)
} }
// if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE){
// grainCables.push(GrainCable.create(comp)); grainCables.push(GrainCable.create(comp));
sourceMap.set(comp.locationString(), comp)
}
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) { if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) {
let heater = Controller.create(comp); let heater = Controller.create(comp);
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF; heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
@ -128,7 +132,7 @@ export default function DryingMode(props: Props){
setFans(fans); setFans(fans);
setSourceMap(sourceMap) setSourceMap(sourceMap)
setSinkMap(sinkMap) setSinkMap(sinkMap)
setCables(grainCables)
//also load the interactions for the selected device so that we can find any conflicting ones //also load the interactions for the selected device so that we can find any conflicting ones
setInteractionsLoading(true) setInteractionsLoading(true)
interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => { interactionAPI.listInteractionsByDevice(selectedDevice.id()).then(resp => {
@ -315,11 +319,12 @@ export default function DryingMode(props: Props){
//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 //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[]) => { const createInteractions = (sets: ComponentSet[]) => {
console.log("create interactions")
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
let conflictingInteractions: Interaction[] = [] let conflictingInteractions: Interaction[] = []
let linkedComponents = deviceComponents.get(selectedDevice.id()); let linkedComponents = deviceComponents.get(selectedDevice.id());
//loop through the sets //loop through the sets to find interactions to remove as well as set the new ones
sets.forEach((set) => { sets.forEach((set) => {
//loop through the controllers //loop through the controllers
set.controllers.forEach(controller => { set.controllers.forEach(controller => {
@ -335,6 +340,7 @@ export default function DryingMode(props: Props){
} }
return conflicting; return conflicting;
}); });
conflictingInteractions = conflictingInteractions.concat(c) conflictingInteractions = conflictingInteractions.concat(c)
// if the controller is a heater create heater interaction // if the controller is a heater create heater interaction
if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){ if(controller.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
@ -351,6 +357,17 @@ export default function DryingMode(props: Props){
} }
}) })
}) })
console.log(cables)
cables.forEach(cable => {
console.log(cable.name())
let c = existingInteractions.filter(i => {
if (sameComponentID(cable.location(), i.settings.source) && !i.settings.sink && i.settings.notifications?.notify) {
return true
}
});
conflictingInteractions = conflictingInteractions.concat(c)
})
setComponentSets([...sets]) setComponentSets([...sets])
return { return {
conflicting: conflictingInteractions, conflicting: conflictingInteractions,
@ -497,7 +514,7 @@ export default function DryingMode(props: Props){
{/* next - goes to the next step, hidden on the last step */} {/* next - goes to the next step, hidden on the last step */}
{currentStep !== steps.length - 1 && <Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>} {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 */} {/* 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([{ toRemove: conflictingInteractions, deviceId: selectedDevice.id(), toAdd: interactionsToAdd, componentSets}])}}>Confirm</Button>} {currentStep === steps.length - 1 && interactionsToAdd && selectedDevice && <Button onClick={() => {confirm([{ toRemove: conflictingInteractions, deviceId: selectedDevice.id(), toAdd: interactionsToAdd, controllers: componentSets.flatMap(set => set.controllers)}])}}>Confirm</Button>}
</React.Fragment> </React.Fragment>
) )
} }

View file

@ -1,5 +1,5 @@
import { DialogContent, DialogTitle, DialogActions } from "@mui/material"; import { DialogContent, DialogTitle, DialogActions, Button } from "@mui/material";
import { Component, Device, Interaction } from "models"; import { Bin, Component, Device, Interaction } from "models";
import { Ambient } from "models/Ambient"; import { Ambient } from "models/Ambient";
import { Controller } from "models/Controller"; import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable"; import { GrainCable } from "models/GrainCable";
@ -10,15 +10,16 @@ import React, { useEffect, useState } from "react";
import { DeviceChangeData } from "./BinModeController"; import { DeviceChangeData } from "./BinModeController";
import { useInteractionsAPI } from "providers"; import { useInteractionsAPI } from "providers";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { componentIDToString } from "pbHelpers/Component";
interface InteractionToRemove { import { ComponentSet } from "bin/conditioning/pickComponentSet";
deviceID: number, import moment from "moment";
} import { lowerCase } from "lodash";
interface Props { interface Props {
// the devices connected to the bin // the devices connected to the bin
devices: Device[] devices: Device[]
deviceComponents: Map<number, Component[]> deviceComponents: Map<number, Component[]>
bin: Bin
binPrefs?: Map<string, pond.BinComponentPreferences> binPrefs?: Map<string, pond.BinComponentPreferences>
confirm: (deviceData: DeviceChangeData[]) => void confirm: (deviceData: DeviceChangeData[]) => void
} }
@ -29,13 +30,17 @@ interface Props {
* @returns * @returns
*/ */
export default function StorageMode(props: Props){ export default function StorageMode(props: Props){
const {devices, deviceComponents, binPrefs} = props const {devices, deviceComponents, bin, binPrefs, confirm} = props
const interactionsAPI = useInteractionsAPI() const interactionsAPI = useInteractionsAPI()
const [data, setData] = useState<DeviceChangeData[]>([])
const [loading, setLoading] = useState(false)
const [loadErr, setLoadErr] = useState(false)
useEffect(()=>{ useEffect(()=>{
let interactionPromises: Promise<Interaction[]>[] = [] let interactionPromises: Promise<Interaction[]>[] = []
let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = [] let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = []
devices.forEach(device => { devices.forEach(device => {
if (!deviceComponents.get(device.id())) return;
let plenums: Plenum[] = [] let plenums: Plenum[] = []
let ambients: Ambient[] = [] let ambients: Ambient[] = []
let cables: GrainCable[] = [] //since this mode is going to add notification interactions to cables they should be filtered as well let cables: GrainCable[] = [] //since this mode is going to add notification interactions to cables they should be filtered as well
@ -70,29 +75,106 @@ export default function StorageMode(props: Props){
deviceContext.push({device, plenums, ambients, cables, controllers}) deviceContext.push({device, plenums, ambients, cables, controllers})
interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id())) interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id()))
}) })
setLoading(true)
let deviceChangeData: DeviceChangeData[] = []
Promise.all(interactionPromises).then(resp => { Promise.all(interactionPromises).then(resp => {
//the index of this should match the device index in the devices array //the index of this should match the device index in the devices array
resp.forEach((interactions,i) => { resp.forEach((interactions,i) => {
let toRemove: Interaction[] = []
let ctx = deviceContext[i]
let sensorsAddr = ctx.plenums.concat(ctx.ambients).map(sensor => sensor.locationString())
let controllerAddr = ctx.controllers.map(cont => cont.locationString())
let cableAddr = ctx.cables.map(cab => cab.locationString())
let cablesWithInteraction = new Set<string>()
interactions.forEach(interaction => { interactions.forEach(interaction => {
//first check if the interaction has a sink in the devices fans or heaters and is being controlled by a plenum or ambient
//this should make it so that it only removes conditioning interactions and not all interactions on the device
let source = interaction.settings.source
let sink = interaction.settings.sink
//this checks if the interaction is a 'conditioning' interaction that needs to be removed
if(source && sink){
if(controllerAddr.includes(componentIDToString(sink)) && sensorsAddr.includes(componentIDToString(source))){
toRemove.push(interaction)
}
}
//need to make sure that it is a notification only interaction and not a controlling one
//it is possible to control things with cables and we want to leave those alone and not consider them notification interactions
//the reason we check that there is no sink is because even control interactions can still send notifications
//we want interactions that effectively ONLY send notifications
if (source && !sink && cableAddr.includes(componentIDToString(source))) {
//check if notifications is true
if(interaction.settings.notifications?.notify){
cablesWithInteraction.add(componentIDToString(source))
}
}
})
let cablesNeedingNotification = ctx.cables.filter(
cable => !cablesWithInteraction.has(cable.locationString())
)
}) let dd: DeviceChangeData = {
}) deviceId: ctx.device.id(),
toRemove: toRemove,
controllers: ctx.controllers,
}
if(cablesNeedingNotification.length > 0){
let toAdd: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
cablesNeedingNotification.forEach(cable => {
//create the new interaction here
let notification: pond.InteractionSettings = pond.InteractionSettings.create({
source: cable.location(),
schedule: pond.InteractionSchedule.create({
timeOfDayStart: "00:00",
timeOfDayEnd: "24:00",
timezone: moment.tz.guess(),
weekdays: moment.weekdays().map(d => lowerCase(d))
}),
conditions: [pond.InteractionCondition.create({
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: bin.settings.highTemp
})],
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT
}),
notifications: pond.InteractionNotifications.create({
notify: true
}),
}) })
//push it to toAdd.interactions
toAdd.interactions.push(notification)
})
//add toAdd to dd
dd.toAdd = toAdd
}
deviceChangeData.push(dd)
})
setData(deviceChangeData)
}).catch(err => {
//TODO: display a snackBar message saying there was an issue loading the data for one or more devices
setData([])
setLoadErr(true)
}).finally(() => {
setLoading(false)
})
},[devices, binPrefs, deviceComponents]) },[devices, binPrefs, deviceComponents])
//load the interactions for all of the bins connected devices
//
return ( return (
<React.Fragment> <React.Fragment>
<DialogTitle>Storage Mode</DialogTitle> <DialogTitle>Storage Mode</DialogTitle>
<DialogContent> <DialogContent>
{/* could have the explanation of what changing to storage mode does here */} {/* could have the explanation of what changing to storage mode does here */}
</DialogContent> </DialogContent>
<DialogActions></DialogActions> <DialogActions>
<Button onClick={()=>{
confirm(data)
}} disabled={loading || loadErr}>Confirm</Button>
</DialogActions>
</React.Fragment> </React.Fragment>
) )
} }

View file

@ -108,18 +108,25 @@ export class Ambient {
return quack.ComponentID.fromObject({ return quack.ComponentID.fromObject({
type: this.settings.type, type: this.settings.type,
addressType: this.settings.addressType, addressType: this.settings.addressType,
address: this.settings.address address: this.settings.address,
expansionLine: this.settings.expansionLine,
muxLine: this.settings.muxLine
}); });
} }
public locationString(): string { public locationString(): string {
return ( let compositeLocation = or(this.settings.type, 0).toString() +
or(this.settings.type, 0).toString() +
"-" + "-" +
or(this.settings.addressType, 0).toString() + or(this.settings.addressType, 0).toString() +
"-" + "-" +
or(this.settings.address, 0).toString() or(this.settings.address, 0).toString()
); if(this.settings.expansionLine){
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
}
if(this.settings.muxLine){
compositeLocation = compositeLocation + ":" + this.settings.muxLine
}
return compositeLocation;
} }
public type(): quack.ComponentType { public type(): quack.ComponentType {

View file

@ -82,18 +82,25 @@ export class Controller {
return quack.ComponentID.fromObject({ return quack.ComponentID.fromObject({
type: this.settings.type, type: this.settings.type,
addressType: this.settings.addressType, addressType: this.settings.addressType,
address: this.settings.address address: this.settings.address,
expansionLine: this.settings.expansionLine,
muxLine: this.settings.muxLine
}); });
} }
public locationString(): string { public locationString(): string {
return ( let compositeLocation = or(this.settings.type, 0).toString() +
or(this.settings.type, 0).toString() +
"-" + "-" +
or(this.settings.addressType, 0).toString() + or(this.settings.addressType, 0).toString() +
"-" + "-" +
or(this.settings.address, 0).toString() or(this.settings.address, 0).toString()
); if(this.settings.expansionLine){
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
}
if(this.settings.muxLine){
compositeLocation = compositeLocation + ":" + this.settings.muxLine
}
return compositeLocation;
} }
public type(): quack.ComponentType { public type(): quack.ComponentType {

View file

@ -230,18 +230,25 @@ export class GrainCable {
return quack.ComponentID.fromObject({ return quack.ComponentID.fromObject({
type: this.settings.type, type: this.settings.type,
addressType: this.settings.addressType, addressType: this.settings.addressType,
address: this.settings.address address: this.settings.address,
expansionLine: this.settings.expansionLine,
muxLine: this.settings.muxLine
}); });
} }
public locationString(): string { public locationString(): string {
return ( let compositeLocation = or(this.settings.type, 0).toString() +
or(this.settings.type, 0).toString() +
"-" + "-" +
or(this.settings.addressType, 0).toString() + or(this.settings.addressType, 0).toString() +
"-" + "-" +
or(this.settings.address, 0).toString() or(this.settings.address, 0).toString()
); if(this.settings.expansionLine){
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
}
if(this.settings.muxLine){
compositeLocation = compositeLocation + ":" + this.settings.muxLine
}
return compositeLocation;
} }
public type(): quack.ComponentType { public type(): quack.ComponentType {

View file

@ -115,13 +115,18 @@ export class Plenum {
} }
public locationString(): string { public locationString(): string {
return ( let compositeLocation = or(this.settings.type, 0).toString() +
or(this.settings.type, 0).toString() +
"-" + "-" +
or(this.settings.addressType, 0).toString() + or(this.settings.addressType, 0).toString() +
"-" + "-" +
or(this.settings.address, 0).toString() or(this.settings.address, 0).toString()
); if(this.settings.expansionLine){
compositeLocation = compositeLocation + "-" + this.settings.expansionLine
}
if(this.settings.muxLine){
compositeLocation = compositeLocation + ":" + this.settings.muxLine
}
return compositeLocation;
} }
public type(): quack.ComponentType { public type(): quack.ComponentType {