diff --git a/package-lock.json b/package-lock.json index 38ab3e8..c150062 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#object_status", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -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#75f64f81d9b4ba7639d9978af37c361fca34c279", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c9ef7906fd97cda8ef4bd149ec4a796159a7c067", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index 3cf9d54..fc7a1ac 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#object_status", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/gate/GateDeviceInteraction.tsx b/src/gate/GateDeviceInteraction.tsx index ca05a58..23669f3 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(Device.create()) const [lowDelta, setLowDelta] = useState(0); const [highDelta, setHighDelta] = useState(0); const [greenComponent, setGreenComponent] = useState(); @@ -44,7 +47,12 @@ 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(); + const [pressureSource, setPressureSource] = useState(quack.ComponentID.create()) useEffect(() => { //math to determine what the delta pressures to set will be @@ -95,46 +103,64 @@ 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 + }) + ) } } }); + + if(compDevice.device){ + setDevice(Device.create(compDevice.device)) + } }, [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 + 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 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 +176,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 +341,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 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: + +
+ 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)} /> 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}