From 0d0936269fb9d9929dc6f3142e7386d8c9174856 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 21 Jan 2026 16:09:14 -0600 Subject: [PATCH 1/2] modified the dialog for adding gate device interactions explains better what the interactions will do and an option to add a pressure threshold that turns off the red light if it goes below, that option will change the redlight from having one toggle interaction to 4 set interactions --- src/gate/GateDeviceInteraction.tsx | 286 +++++++++++++++++++++++------ src/gate/GateFlowGraph.tsx | 16 +- src/gate/GateList.tsx | 8 +- src/gate/GateSettings.tsx | 28 ++- 4 files changed, 264 insertions(+), 74 deletions(-) diff --git a/src/gate/GateDeviceInteraction.tsx b/src/gate/GateDeviceInteraction.tsx index ca05a58..c8e97ef 100644 --- a/src/gate/GateDeviceInteraction.tsx +++ b/src/gate/GateDeviceInteraction.tsx @@ -1,4 +1,5 @@ -import { Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"; +import { CheckBox } from "@mui/icons-material"; +import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormLabel, InputAdornment, TextField, Typography } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; import { Component, Device } from "models"; import { Gate } from "models/Gate"; @@ -6,6 +7,7 @@ import moment from "moment"; import { pond, quack } from "protobuf-ts/pond"; import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers"; import React, { useEffect, useState } from "react"; +import { getPressureUnit } from "utils"; interface Props { open: boolean; @@ -37,6 +39,7 @@ const densityMap = new Map([ export default function GateDeviceInteraction(props: Props) { const { open, close, gate, compDevice, densityTemp } = props; const [{ user, as }] = useGlobalState(); + const [device, setDevice] = useState() const [lowDelta, setLowDelta] = useState(0); const [highDelta, setHighDelta] = useState(0); const [greenComponent, setGreenComponent] = useState(); @@ -44,6 +47,10 @@ export default function GateDeviceInteraction(props: Props) { const [pressureComponent, setPressureComponent] = useState(); const interactionsAPI = useInteractionsAPI(); const [adding, setAdding] = useState(false); + //boolean to determine if a third condition should be put on the interaction for the red light, requires a V2 device running at least fromware version 2.1.9 + const [useRedOffCondition, setUseRedOffCondition] = useState(false) + //if the pressure is under this value then the red light should be off + const [redThreshold, setRedThreshold] = useState("0") const { openSnack } = useSnackbar(); useEffect(() => { @@ -98,6 +105,10 @@ export default function GateDeviceInteraction(props: Props) { } } }); + + if(compDevice.device){ + setDevice(Device.create(compDevice.device)) + } }, [gate, densityTemp, user, compDevice]); // useEffect(() => { @@ -115,26 +126,54 @@ export default function GateDeviceInteraction(props: Props) { // }); // }; + const buttonDisabled = () => { + if (greenComponent === undefined) return true + if (redComponent === undefined) return true + if (adding) return true + if (useRedOffCondition && isNaN(parseFloat(redThreshold))) return true + return false + } + const createInteractions = async () => { //the interactions to be made - //TOGGLE green ON when pressure within range of upper and lower if ( greenComponent !== undefined && redComponent !== undefined && pressureComponent !== undefined ) { - let greenToggle: pond.InteractionSettings = pond.InteractionSettings.create({ - sink: quack.ComponentID.create({ - type: greenComponent.type(), - address: greenComponent.settings.address, - addressType: greenComponent.settings.addressType - }), - source: quack.ComponentID.create({ - type: pressureComponent.type(), - address: pressureComponent.settings.address, - addressType: pressureComponent.settings.addressType - }), + + let lightInteractions: pond.InteractionSettings[] = [] + //making variables for the parameters of the interactions + const redSink = quack.ComponentID.create({ + type: redComponent.type(), + address: redComponent.settings.address, + addressType: redComponent.settings.addressType + }) + + const greenSink = quack.ComponentID.create({ + type: greenComponent.type(), + address: greenComponent.settings.address, + addressType: greenComponent.settings.addressType + }) + + const pressureSource = quack.ComponentID.create({ + type: pressureComponent.type(), + address: pressureComponent.settings.address, + addressType: pressureComponent.settings.addressType + }) + + const lightSchedule = pond.InteractionSchedule.create({ + timezone: moment.tz.guess(), + weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"], + timeOfDayStart: "00:00", + timeOfDayEnd: "24:00" + }) + + //TOGGLE green ON when pressure within range of upper and lower, this will always be the same regardless + lightInteractions.push(pond.InteractionSettings.create({ + sink: greenSink, + source: pressureSource, conditions: [ pond.InteractionCondition.create({ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, @@ -150,61 +189,146 @@ export default function GateDeviceInteraction(props: Props) { nodeOne: 2, nodeTwo: 1, subtype: 18, - schedule: pond.InteractionSchedule.create({ - timezone: moment.tz.guess(), - weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"], - timeOfDayStart: "00:00", - timeOfDayEnd: "24:00" - }), + schedule: lightSchedule, result: pond.InteractionResult.create({ type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE, value: 1 }) - }); + })) - //TOGGLE red OFF when pressure within range of upper and lower - let redToggle: pond.InteractionSettings = pond.InteractionSettings.create({ - sink: quack.ComponentID.create({ - type: redComponent.type(), - address: redComponent.settings.address, - addressType: redComponent.settings.addressType - }), - source: quack.ComponentID.create({ - type: pressureComponent.type(), - address: pressureComponent.settings.address, - addressType: pressureComponent.settings.addressType - }), - conditions: [ - pond.InteractionCondition.create({ - comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, - value: -highDelta, - measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE - }), - pond.InteractionCondition.create({ - comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, - value: -lowDelta, - measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + //if they want the red light off when below a certain pressure, it will need 4 seperate SET interactions + if(useRedOffCondition){ + //need to make sure the redthreshold is in pascals + let thresholdPascals = 0 + if(!isNaN(parseFloat(redThreshold))){ + if(getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS){ + thresholdPascals = parseFloat(redThreshold)*1000 + }else if (getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER){ + thresholdPascals = parseFloat(redThreshold)*249 + } + } + + //SET red on when above high + lightInteractions.push(pond.InteractionSettings.create({ + sink: redSink, + source: pressureSource, + conditions: [ + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: -highDelta, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }) + ], + nodeOne: 2, + nodeTwo: 1, + subtype: 18, + schedule: lightSchedule, + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET, + value: 1 }) - ], - nodeOne: 2, - nodeTwo: 1, - subtype: 18, - schedule: pond.InteractionSchedule.create({ - timezone: moment.tz.guess(), - weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"], - timeOfDayStart: "00:00", - timeOfDayEnd: "24:00" - }), - result: pond.InteractionResult.create({ - type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE, - value: 0 - }) - }); + })) + //SET red off when below high and above low + lightInteractions.push(pond.InteractionSettings.create({ + sink: redSink, + source: pressureSource, + conditions: [ + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: -highDelta, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }), + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: -lowDelta, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }) + ], + nodeOne: 2, + nodeTwo: 1, + subtype: 18, + schedule: lightSchedule, + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET, + value: 0 + }) + })) + //SET red on when below low and above threshold + lightInteractions.push(pond.InteractionSettings.create({ + sink: redSink, + source: pressureSource, + conditions: [ + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: -lowDelta, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }), + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: -thresholdPascals, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }), + ], + nodeOne: 2, + nodeTwo: 1, + subtype: 18, + schedule: lightSchedule, + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET, + value: 1 + }) + })) + //SET red off when below threshold + lightInteractions.push(pond.InteractionSettings.create({ + sink: redSink, + source: pressureSource, + conditions: [ + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: thresholdPascals, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }), + ], + subtype: 1, + schedule: lightSchedule, + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET, + value: 0 + }) + })) + }else{ + //otherwise use the regular single TOGGLE interaction + //TOGGLE red OFF when pressure within range of upper and lower + lightInteractions.push(pond.InteractionSettings.create({ + sink: redSink, + source: pressureSource, + conditions: [ + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN, + value: -highDelta, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }), + pond.InteractionCondition.create({ + comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN, + value: -lowDelta, + measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE + }) + ], + nodeOne: 2, + nodeTwo: 1, + subtype: 18, + schedule: lightSchedule, + result: pond.InteractionResult.create({ + type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE, + value: 0 + }) + })) + } let deviceID = Device.any(compDevice.device).id(); if (deviceID !== undefined) { let multi = pond.MultiInteractionSettings.create({ - interactions: [greenToggle, redToggle] + interactions: lightInteractions }); interactionsAPI .addMultiInteractions(deviceID, multi, as) @@ -230,11 +354,51 @@ export default function GateDeviceInteraction(props: Props) { }}> Set Interaction For Light Toggle - Your Delta Pressures, in pascals, will be: + {/* Your Delta Pressures, in pascals, will be: low = {lowDelta} high = {highDelta} {greenComponent ? "" : "Green LED Component not found"} - {redComponent ? "" : "Red LED Component not found"} + {redComponent ? "" : "Red LED Component not found"} */} + + This will set interactions on the pressure chain to toggle the green light on and the red light off + when the difference between the pressures falls within this range and vice versa: + +
+ Upper Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (highDelta/249.089).toFixed(2) + " iwg" : highDelta/1000 + " kPa"} + Lower Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (lowDelta/249.089).toFixed(2) + " iwg" : lowDelta/1000 + " kPa"} +
+ + If you would like to have the interactions set in such a way that if the average pressure falls below a given value set the use off condition and set the value to use + { + setUseRedOffCondition(checked) + }} + /> + } + label={Use Off Condition} + /> + + {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? "iwg" : "kPa"} + + ) + }} + onChange={e => { + setRedThreshold(e.target.value) + }} + /> +
diff --git a/src/gate/GateFlowGraph.tsx b/src/gate/GateFlowGraph.tsx index 6835b63..e6cdaf4 100644 --- a/src/gate/GateFlowGraph.tsx +++ b/src/gate/GateFlowGraph.tsx @@ -70,13 +70,19 @@ export default function GateFlowGraph(props: Props) { const classes = useStyles(); const [flowEvents, setFlowEvents] = useState([]); const [eventsLoading, setEventsLoading] = useState(false); - //these two constants could be entered by the user at time of retrieval + //these two constants could be entered by the user at time of retrieval or set in the gates settings const eventThreshold = 5; //the threshold that if crossed from one measurement to the next during a flow event will end the current flow event and start a new one - const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event - + //const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event + const [idleFlow, setIdleFlow] = useState(3.5) + useEffect(() => { if (loadingChartData) return; if (ambient && pressureComponent) { + let gateIdle = idleFlow + if(gate.settings.idleFlow){ + gateIdle = gate.settings.idleFlow + } + let recent: SSAreaDataPoint | undefined; setLoadingChartData(true); gateAPI @@ -110,11 +116,11 @@ export default function GateFlowGraph(props: Props) { /** determine runtime */ // set the start time if the values is greater than the idleFlow and start is not already set - if (val.airFlow >= idleFlow && !start) { + if (val.airFlow >= gateIdle && !start) { start = time; } // set the stop time when off or at the last measurements and the start time is set - if ((val.airFlow < idleFlow || i === resp.data.values.length - idleFlow) && start) { + if ((val.airFlow < gateIdle || i === resp.data.values.length - gateIdle) && start) { stop = time; } // if both start and stop are set calculate add the timeframe to the total runtime diff --git a/src/gate/GateList.tsx b/src/gate/GateList.tsx index bdf58c2..3e3b330 100644 --- a/src/gate/GateList.tsx +++ b/src/gate/GateList.tsx @@ -11,7 +11,7 @@ import ResponsiveTable, { Column } from "common/ResponsiveTable"; import AddGateFab from "./AddGateFab"; import GateSettings from "./GateSettings"; import { useGateAPI, useGlobalState } from "providers"; -import { CheckCircleOutline, DoNotDisturb, ErrorOutline, RemoveCircleOutline, Settings } from "@mui/icons-material"; +import { CheckCircleOutline, DoNotDisturb, ErrorOutline, HelpOutlineOutlined, Settings } from "@mui/icons-material"; import { cloneDeep } from "lodash"; import moment from "moment"; import { pond } from "protobuf-ts/pond"; @@ -94,13 +94,13 @@ export default function GateList(props: Props) { const displayPCAStatus = (state: pond.PCAState) => { switch(state){ case pond.PCAState.PCA_STATE_IN_BOUNDS: - return + return case pond.PCAState.PCA_STATE_OUT_BOUNDS: return case pond.PCAState.PCA_STATE_OFF: - return - default: return + default: + return } } diff --git a/src/gate/GateSettings.tsx b/src/gate/GateSettings.tsx index b4a1d92..c2ae4fc 100644 --- a/src/gate/GateSettings.tsx +++ b/src/gate/GateSettings.tsx @@ -76,6 +76,7 @@ export default function GateSettings(props: Props) { //user set identifier to be shown on the marker on the map const [terminalIdentifier, setTerminalIdentifier] = useState(""); const [gateIdentifier, setGateIdentifier] = useState(""); + const [idleLimit, setIdleLimit] = useState(0); const [hourlyPCA, setHourlyPCA] = useState(0); const [hourlyAPU, setHourlyAPU] = useState(0); @@ -140,6 +141,7 @@ export default function GateSettings(props: Props) { settings.thermalResistance = ductProps.thermalResistance; settings.lowerFlow = lowerFlowBound; settings.upperFlow = upperFlowBound; + settings.idleFlow = idleLimit; settings.ductName = ductName; settings.pcaType = pcaUnit; settings.letterIdentifier = terminalIdentifier; @@ -167,6 +169,7 @@ export default function GateSettings(props: Props) { thermalResistance: ductProps.thermalResistance, lowerFlow: lowerFlowBound, upperFlow: upperFlowBound, + idleFlow: idleLimit, ductName: ductName, pcaType: pcaUnit, letterIdentifier: terminalIdentifier, @@ -283,16 +286,18 @@ export default function GateSettings(props: Props) { return ( setGateName(e.target.value)} /> - + setLowerFlowBound(+e.target.value)} /> setUpperFlowBound(+e.target.value)} /> setIdleLimit(+e.target.value)} + /> + setPcaUnit(e.target.value)} /> setHourlyPCA(+e.target.value)} /> Date: Thu, 22 Jan 2026 15:55:15 -0600 Subject: [PATCH 2/2] updated the interaction api to have the new call to clear interactions, and am now using that call to clear the interactions from the pressure chain and when that response comes back successfully then will add the new set of interactions --- package-lock.json | 2 +- src/gate/GateDeviceInteraction.tsx | 39 ++++++++++---------------- src/providers/pond/interactionsAPI.tsx | 23 ++++++++++++++- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 367ad08..b4743f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10953,7 +10953,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#580cb59c08f9895384b5adc4853c480bf3bf3165", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#09c1448af13950a76e9097c9b374680390039c48", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/gate/GateDeviceInteraction.tsx b/src/gate/GateDeviceInteraction.tsx index c8e97ef..23669f3 100644 --- a/src/gate/GateDeviceInteraction.tsx +++ b/src/gate/GateDeviceInteraction.tsx @@ -39,7 +39,7 @@ const densityMap = new Map([ export default function GateDeviceInteraction(props: Props) { const { open, close, gate, compDevice, densityTemp } = props; const [{ user, as }] = useGlobalState(); - const [device, setDevice] = useState() + const [device, setDevice] = useState(Device.create()) const [lowDelta, setLowDelta] = useState(0); const [highDelta, setHighDelta] = useState(0); const [greenComponent, setGreenComponent] = useState(); @@ -52,6 +52,7 @@ export default function GateDeviceInteraction(props: Props) { //if the pressure is under this value then the red light should be off const [redThreshold, setRedThreshold] = useState("0") const { openSnack } = useSnackbar(); + const [pressureSource, setPressureSource] = useState(quack.ComponentID.create()) useEffect(() => { //math to determine what the delta pressures to set will be @@ -102,6 +103,13 @@ export default function GateDeviceInteraction(props: Props) { gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE ) { setPressureComponent(component); + setPressureSource( + quack.ComponentID.create({ + type: component.type(), + address: component.settings.address, + addressType: component.settings.addressType + }) + ) } } }); @@ -111,21 +119,6 @@ export default function GateDeviceInteraction(props: Props) { } }, [gate, densityTemp, user, compDevice]); - // useEffect(() => { - // //load current interactions for the device - // let deviceID = Device.any(compDevice.device).id(); - // interactionsAPI.listInteractionsByDevice(deviceID).then(resp => { - // setCurrentInteractions(resp); - // }); - // }, [compDevice.device, interactionsAPI]); - - // const removeCurrentInteractions = () => { - // let deviceID = Device.any(compDevice.device).id(); - // currentInteractions.forEach(interaction => { - // interactionsAPI.removeInteraction(deviceID, interaction.key()); - // }); - // }; - const buttonDisabled = () => { if (greenComponent === undefined) return true if (redComponent === undefined) return true @@ -157,12 +150,6 @@ export default function GateDeviceInteraction(props: Props) { addressType: greenComponent.settings.addressType }) - const pressureSource = quack.ComponentID.create({ - type: pressureComponent.type(), - address: pressureComponent.settings.address, - addressType: pressureComponent.settings.addressType - }) - const lightSchedule = pond.InteractionSchedule.create({ timezone: moment.tz.guess(), weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"], @@ -360,7 +347,7 @@ export default function GateDeviceInteraction(props: Props) { {greenComponent ? "" : "Green LED Component not found"} {redComponent ? "" : "Red LED Component not found"} */} - This will set interactions on the pressure chain to toggle the green light on and the red light off + This will clear existing interactions on the pressure chain and set new ones to toggle the green light on and the red light off when the difference between the pressures falls within this range and vice versa:
@@ -408,7 +395,11 @@ export default function GateDeviceInteraction(props: Props) { //TODO: Once we figure out how to fix the backend to handle rapid addition/removal of interactions //removeCurrentInteractions(); setAdding(true); - createInteractions(); + interactionsAPI.clearInteractions(device.id(), [pressureSource]).then(resp => { + createInteractions(); + }).catch(err => { + console.error(err) + }) }} disabled={buttonDisabled()}> {adding ? "Adding Interaction" : "Create Interaction"} diff --git a/src/providers/pond/interactionsAPI.tsx b/src/providers/pond/interactionsAPI.tsx index 26afb1e..5fa3705 100644 --- a/src/providers/pond/interactionsAPI.tsx +++ b/src/providers/pond/interactionsAPI.tsx @@ -36,6 +36,7 @@ export interface IInteractionsAPIContext { alerts: pond.AlertData[], otherTeam?: string ) => Promise>; + clearInteractions: (device: number, sources?: quack.ComponentID[], otherTeam?: string) => Promise> } export const InteractionsAPIContext = createContext( @@ -285,6 +286,25 @@ export default function InteractionProvider(props: PropsWithChildren) { }) }; + const clearInteractions = (device: number, sources?: quack.ComponentID[], otherTeam?: string): Promise> => { + const view = otherTeam ? otherTeam : as + let sourceArray: string[] = [] + if (sources){ + sources.forEach(source => { + sourceArray.push(componentIDToString(source)) + }) + } + let url = pondURL("/devices/"+ device + "/interactions/clear" + (view ? "?as=" + view : "") + (sources ? "&sources=" + sourceArray.toString() : "")) + return new Promise>((resolve, reject) => { + post(url).then(resp => { + resp.data = pond.ClearInteractionsResponse.fromObject(resp.data) + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + return ( ) { updateInteractionPondSettings, removeInteraction, listInteractionsByDevice, - listInteractionsByComponent + listInteractionsByComponent, + clearInteractions }}> {children}