Merge branch 'object_status' into staging_environment

This commit is contained in:
csawatzky 2026-01-22 16:12:44 -06:00
commit cc4d0d5736
7 changed files with 295 additions and 93 deletions

4
package-lock.json generated
View file

@ -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"
}

View file

@ -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",

View file

@ -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<number, number>([
export default function GateDeviceInteraction(props: Props) {
const { open, close, gate, compDevice, densityTemp } = props;
const [{ user, as }] = useGlobalState();
const [device, setDevice] = useState<Device>(Device.create())
const [lowDelta, setLowDelta] = useState(0);
const [highDelta, setHighDelta] = useState(0);
const [greenComponent, setGreenComponent] = useState<Component>();
@ -44,7 +47,12 @@ export default function GateDeviceInteraction(props: Props) {
const [pressureComponent, setPressureComponent] = useState<Component>();
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>(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) {
}}>
<DialogTitle>Set Interaction For Light Toggle</DialogTitle>
<DialogContent>
Your Delta Pressures, in pascals, will be:
{/* Your Delta Pressures, in pascals, will be:
<Typography>low = {lowDelta}</Typography>
<Typography>high = {highDelta}</Typography>
<Typography>{greenComponent ? "" : "Green LED Component not found"}</Typography>
<Typography>{redComponent ? "" : "Red LED Component not found"}</Typography>
<Typography>{redComponent ? "" : "Red LED Component not found"}</Typography> */}
<Typography>
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:
</Typography>
<br />
<Typography>Upper Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (highDelta/249.089).toFixed(2) + " iwg" : highDelta/1000 + " kPa"}</Typography>
<Typography>Lower Limit: {getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? (lowDelta/249.089).toFixed(2) + " iwg" : lowDelta/1000 + " kPa"}</Typography>
<br />
<Typography>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</Typography>
<FormControlLabel
control={
<Checkbox
checked={useRedOffCondition}
onChange={(e, checked) => {
setUseRedOffCondition(checked)
}}
/>
}
label={<Typography>Use Off Condition</Typography>}
/>
<TextField
type="number"
fullWidth
disabled={!useRedOffCondition}
value={redThreshold}
error={isNaN(parseFloat(redThreshold))}
helperText={isNaN(parseFloat(redThreshold)) ? "Must be a valid number" : ""}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getPressureUnit() === pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER ? "iwg" : "kPa"}
</InputAdornment>
)
}}
onChange={e => {
setRedThreshold(e.target.value)
}}
/>
</DialogContent>
<DialogActions>
<Button
@ -244,9 +395,13 @@ 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={greenComponent === undefined || redComponent === undefined || adding}>
disabled={buttonDisabled()}>
{adding ? "Adding Interaction" : "Create Interaction"}
</Button>
</DialogActions>

View file

@ -70,13 +70,19 @@ export default function GateFlowGraph(props: Props) {
const classes = useStyles();
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
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

View file

@ -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";
@ -98,9 +98,9 @@ export default function GateList(props: Props) {
case pond.PCAState.PCA_STATE_OUT_BOUNDS:
return <ErrorOutline sx={{color: "red"}} />
case pond.PCAState.PCA_STATE_OFF:
return <RemoveCircleOutline />
default:
return <DoNotDisturb />
default:
return <HelpOutlineOutlined />
}
}

View file

@ -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<number>(0);
const [hourlyPCA, setHourlyPCA] = useState<number>(0);
const [hourlyAPU, setHourlyAPU] = useState<number>(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 (
<React.Fragment>
<TextField
margin="dense"
label="Gate Name"
fullWidth
value={gateName}
onChange={e => setGateName(e.target.value)}
/>
<Select
<TextField
margin="dense"
id="terminal"
label="Terminal"
fullWidth
displayEmpty
select
value={terminalKey}
onChange={e => {
setTerminalKey(e.target.value as string);
@ -305,8 +310,9 @@ export default function GateSettings(props: Props) {
{terminal.name}
</MenuItem>
))}
</Select>
</TextField>
<TextField
margin="dense"
label="Low Mass Air Flow"
fullWidth
type="number"
@ -314,19 +320,30 @@ export default function GateSettings(props: Props) {
onChange={e => setLowerFlowBound(+e.target.value)}
/>
<TextField
margin="dense"
label="High Mass Air Flow"
fullWidth
type="number"
type="dense"
value={upperFlowBound}
onChange={e => setUpperFlowBound(+e.target.value)}
/>
<TextField
margin="dense"
label="Idle Mass Air Flow"
fullWidth
type="number"
value={idleLimit}
onChange={e => setIdleLimit(+e.target.value)}
/>
<TextField
margin="dense"
label="PCA Unit"
fullWidth
value={pcaUnit}
onChange={e => setPcaUnit(e.target.value)}
/>
<TextField
margin="dense"
style={{ width: "45%" }}
select
label="Terminal Identifier"
@ -340,6 +357,7 @@ export default function GateSettings(props: Props) {
{tiOptions}
</TextField>
<TextField
margin="dense"
style={{ width: "45%", marginLeft: "10%" }}
label="Gate Identifier"
select
@ -351,6 +369,7 @@ export default function GateSettings(props: Props) {
{numberOptions}
</TextField>
<TextField
margin="dense"
label="Hourly PCA Cost"
fullWidth
type="number"
@ -361,6 +380,7 @@ export default function GateSettings(props: Props) {
onChange={e => setHourlyPCA(+e.target.value)}
/>
<TextField
margin="dense"
label="Hourly APU Cost"
fullWidth
type="number"

View file

@ -36,6 +36,7 @@ export interface IInteractionsAPIContext {
alerts: pond.AlertData[],
otherTeam?: string
) => Promise<AxiosResponse<pond.SetAlertInteractionsResponse>>;
clearInteractions: (device: number, sources?: quack.ComponentID[], otherTeam?: string) => Promise<AxiosResponse<pond.ClearInteractionsResponse>>
}
export const InteractionsAPIContext = createContext<IInteractionsAPIContext>(
@ -285,6 +286,25 @@ export default function InteractionProvider(props: PropsWithChildren<Props>) {
})
};
const clearInteractions = (device: number, sources?: quack.ComponentID[], otherTeam?: string): Promise<AxiosResponse<pond.ClearInteractionsResponse>> => {
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<AxiosResponse<pond.ClearInteractionsResponse>>((resolve, reject) => {
post<pond.ClearInteractionsResponse>(url).then(resp => {
resp.data = pond.ClearInteractionsResponse.fromObject(resp.data)
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
}
return (
<InteractionsAPIContext.Provider
value={{
@ -296,7 +316,8 @@ export default function InteractionProvider(props: PropsWithChildren<Props>) {
updateInteractionPondSettings,
removeInteraction,
listInteractionsByDevice,
listInteractionsByComponent
listInteractionsByComponent,
clearInteractions
}}>
{children}
</InteractionsAPIContext.Provider>