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 { ComponentSet } from "bin/conditioning/pickComponentSet";
import { cloneDeep } from "lodash";
import { Controller } from "models/Controller";
export interface DeviceChangeData {
/**
* the id of the device changes are being made to
*/
deviceId: number
/**
* the array of interactions to remove from the device
*/
toRemove: Interaction[]
/**
* the new interactions to add to the device
*/
toAdd?: pond.MultiInteractionSettings
componentSets: ComponentSet[]
/**
* the controllers that need to have there state updated
*/
controllers: Controller[]
}
interface Props {
@ -99,8 +112,8 @@ export default function BinModeController(props: Props) {
promise: () => interactionAPI.addMultiInteractions(data.deviceId, newInteractions)
})
}
data.componentSets.forEach(set => {
set.controllers.forEach(controller => {
data.controllers.forEach(controller => {
let newStep: Step = {
title: "Updating " + controller.name(),
promise: () => componentAPI.update(data.deviceId, controller.settings)
@ -108,7 +121,7 @@ export default function BinModeController(props: Props) {
stage3.steps.push(newStep)
})
})
})
if(stage1.steps.length > 0){
stages.push(stage1)
}
@ -195,7 +208,15 @@ export default function BinModeController(props: Props) {
case pond.BinMode.BIN_MODE_COOLDOWN:
return <CooldownMode />
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 React from "react";
import { DeviceChangeData } from "./BinModeController";
import { GrainCable } from "models/GrainCable";
const useStyles = makeStyles((theme: Theme) => {
@ -66,6 +67,7 @@ export default function DryingMode(props: Props){
const [ambients, setAmbients] = useState<Ambient[]>([])
const [heaters, setHeaters] = useState<Controller[]>([])
const [fans, setFans] = useState<Controller[]>([])
const [cables, setCables] = useState<GrainCable[]>([])
const [currentStep, setCurrentStep] = useState(0)
const [interactionLoading,setInteractionsLoading] = useState(false)
@ -87,7 +89,7 @@ export default function DryingMode(props: Props){
if (!deviceComponents.get(selectedDevice.id())) return;
var plenums: Plenum[] = [];
var ambients: Ambient[] = [];
//var grainCables: GrainCable[] = [];
var grainCables: GrainCable[] = [];
var heaters: Controller[] = [];
var fans: Controller[] = [];
var sinkMap: Map<string, Component> = new Map()
@ -105,8 +107,10 @@ export default function DryingMode(props: Props){
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_GRAIN_CABLE){
grainCables.push(GrainCable.create(comp));
sourceMap.set(comp.locationString(), comp)
}
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER) {
let heater = Controller.create(comp);
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_OFF;
@ -128,7 +132,7 @@ export default function DryingMode(props: Props){
setFans(fans);
setSourceMap(sourceMap)
setSinkMap(sinkMap)
setCables(grainCables)
//also load the interactions for the selected device so that we can find any conflicting ones
setInteractionsLoading(true)
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
const createInteractions = (sets: ComponentSet[]) => {
console.log("create interactions")
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
//loop through the sets to find interactions to remove as well as set the new ones
sets.forEach((set) => {
//loop through the controllers
set.controllers.forEach(controller => {
@ -335,6 +340,7 @@ export default function DryingMode(props: Props){
}
return conflicting;
});
conflictingInteractions = conflictingInteractions.concat(c)
// if the controller is a heater create heater interaction
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])
return {
conflicting: conflictingInteractions,
@ -497,7 +514,7 @@ export default function DryingMode(props: Props){
{/* 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([{ 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>
)
}

View file

@ -1,5 +1,5 @@
import { DialogContent, DialogTitle, DialogActions } from "@mui/material";
import { Component, Device, Interaction } from "models";
import { DialogContent, DialogTitle, DialogActions, Button } from "@mui/material";
import { Bin, Component, Device, Interaction } from "models";
import { Ambient } from "models/Ambient";
import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable";
@ -10,15 +10,16 @@ import React, { useEffect, useState } from "react";
import { DeviceChangeData } from "./BinModeController";
import { useInteractionsAPI } from "providers";
import { AxiosResponse } from "axios";
interface InteractionToRemove {
deviceID: number,
}
import { componentIDToString } from "pbHelpers/Component";
import { ComponentSet } from "bin/conditioning/pickComponentSet";
import moment from "moment";
import { lowerCase } from "lodash";
interface Props {
// the devices connected to the bin
devices: Device[]
deviceComponents: Map<number, Component[]>
bin: Bin
binPrefs?: Map<string, pond.BinComponentPreferences>
confirm: (deviceData: DeviceChangeData[]) => void
}
@ -29,13 +30,17 @@ interface Props {
* @returns
*/
export default function StorageMode(props: Props){
const {devices, deviceComponents, binPrefs} = props
const {devices, deviceComponents, bin, binPrefs, confirm} = props
const interactionsAPI = useInteractionsAPI()
const [data, setData] = useState<DeviceChangeData[]>([])
const [loading, setLoading] = useState(false)
const [loadErr, setLoadErr] = useState(false)
useEffect(()=>{
let interactionPromises: Promise<Interaction[]>[] = []
let deviceContext: {device: Device, plenums: Plenum[], ambients: Ambient[], cables: GrainCable[], controllers: Controller[]}[] = []
devices.forEach(device => {
if (!deviceComponents.get(device.id())) return;
let plenums: Plenum[] = []
let ambients: Ambient[] = []
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})
interactionPromises.push(interactionsAPI.listInteractionsByDevice(device.id()))
})
setLoading(true)
let deviceChangeData: DeviceChangeData[] = []
Promise.all(interactionPromises).then(resp => {
//the index of this should match the device index in the devices array
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 => {
//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])
//load the interactions for all of the bins connected devices
//
return (
<React.Fragment>
<DialogTitle>Storage Mode</DialogTitle>
<DialogContent>
{/* could have the explanation of what changing to storage mode does here */}
</DialogContent>
<DialogActions></DialogActions>
<DialogActions>
<Button onClick={()=>{
confirm(data)
}} disabled={loading || loadErr}>Confirm</Button>
</DialogActions>
</React.Fragment>
)
}

View file

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

View file

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

View file

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

View file

@ -115,13 +115,18 @@ export class Plenum {
}
public locationString(): string {
return (
or(this.settings.type, 0).toString() +
let compositeLocation = or(this.settings.type, 0).toString() +
"-" +
or(this.settings.addressType, 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 {