diff --git a/package-lock.json b/package-lock.json
index 8d82f84..486db5e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,7 +43,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#staging",
+ "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@@ -11967,7 +11967,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
- "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#bcf085706791036841f7047a5b37f00c18e54574",
+ "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#40e15b5ed18b10c97125260a2d53a81d1eec398e",
"dependencies": {
"protobufjs": "^6.8.8"
}
diff --git a/src/bin/BinControllerDisplay.tsx b/src/bin/BinControllerDisplay.tsx
new file mode 100644
index 0000000..32cbd94
--- /dev/null
+++ b/src/bin/BinControllerDisplay.tsx
@@ -0,0 +1,168 @@
+import { Component } from "models"
+import React, { useEffect, useState } from "react"
+import { makeStyles } from "@mui/styles";
+import { Box, Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
+import AerationFanIcon from "products/AgIcons/AerationFanIcon";
+import { quack } from "protobuf-ts/quack";
+import { useComponentAPI, useMobile, useSnackbar } from "hooks";
+import ButtonGroup from "common/ButtonGroup";
+import ObjectHeaterIcon from "products/Construction/ObjectHeaterIcon";
+import ResponsiveDialog from "common/ResponsiveDialog";
+
+
+interface Props {
+ deviceID: number
+ component: Component
+ currentState?: boolean
+}
+
+const useStyles = makeStyles(() => {
+ return ({
+ controllerDisplay: {
+ display: "flex",
+ justifyContent: "space-between",
+ paddingLeft: 5,
+ paddingRight: 5
+ },
+ })
+})
+
+export default function BinControllerDisplay(props: Props) {
+ const {component, deviceID, currentState} = props
+ const [controllerState, setControllerState] = useState(0)
+ const [openDialog, setOpenDialog] = useState(false)
+ const [newOutputState, setNewOutputState] = useState(0)
+ const {openSnack} = useSnackbar()
+ const componentAPI = useComponentAPI()
+ const isMobile = useMobile()
+ const classes = useStyles()
+
+ useEffect(()=>{
+ setControllerState(component.settings.defaultOutputState)
+ },[component])
+
+
+ const controllerIcon = () => {
+ if(component.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER){
+ return
+ }else{
+ return
+ }
+ }
+
+ const setController = () => {
+ let newSettings = component.settings
+ newSettings.defaultOutputState = newOutputState
+ componentAPI.update(deviceID, newSettings).then(resp => {
+ openSnack("Updated controllers output state")
+ setControllerState(newOutputState)
+ }).catch(err => {
+ openSnack("There was a problem updating the controller")
+ }).finally(() => {
+ setOpenDialog(false)
+ })
+ }
+
+ const controllerDialog = () => {
+ return (
+ {setOpenDialog(false)}}>
+ Set Controller State
+
+ This Action will change the output state of the controller
+
+
+
+
+
+
+ )
+ }
+
+ const componentOn = () => {
+ if(currentState){
+ return currentState
+ }else{
+ let state = false
+ component.status.lastGoodMeasurement.forEach(um => {
+ if(um.type === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN){
+ if(um.values.length > 0){
+ let readings = um.values
+ if(readings[readings.length-1].values.length > 0){
+ let nodes = readings[readings.length-1].values
+ if (nodes[nodes.length -1] === 1){
+ state = true
+ }
+ }
+ }
+ }
+ })
+ return state
+ }
+ }
+
+ return (
+
+ {controllerDialog()}
+
+
+
+ {controllerIcon()}
+
+
+ {component.name()}
+
+
+
+ {
+ setNewOutputState(0)
+ setOpenDialog(true)
+ }
+ },
+ {
+ title: "On",
+ function: () => {
+ setNewOutputState(1)
+ setOpenDialog(true)
+ }
+ },
+ {
+ title: "Off",
+ function: () => {
+ setNewOutputState(2)
+ setOpenDialog(true)
+ }
+ }
+ ]}
+ toggledButtons={[controllerState]}
+ toggle
+ />
+
+ {componentOn() ? "ON" : "OFF"}
+
+
+
+
+ )
+
+}
\ No newline at end of file
diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx
index 2e2f987..2da6f78 100644
--- a/src/bin/BinVisualizerV2.tsx
+++ b/src/bin/BinVisualizerV2.tsx
@@ -31,7 +31,7 @@ import { round } from "lodash";
import { Bin, Component, Device } from "models";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
-import { useMobile } from "hooks";
+import { useComponentAPI, useMobile } from "hooks";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState, useSnackbar } from "providers";
@@ -71,6 +71,7 @@ import { makeStyles } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
import ModeChangeDialog from "./conditioning/modeChangeDialog";
import CustomGrainSelector from "grain/CustomGrainSelector";
+import BinControllerDisplay from "./BinControllerDisplay";
const useStyles = makeStyles((theme: Theme) => {
return ({
@@ -180,7 +181,7 @@ interface Props {
bin: Bin;
loading: boolean;
components: Map;
- componentDevices?: Map;
+ componentDevices: Map;
devices: Device[];
plenums?: Plenum[];
ambient?: Ambient;
@@ -289,6 +290,8 @@ export default function BinVisualizer(props: Props) {
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
const [newGrainSettings, setNewGrainSettings] = useState()
+ const [filteredComponents, setFilteredComponents] = useState([])
+
useEffect(() => {
setModeTime(moment(bin.status.lastModeChange));
}, [bin.status.lastModeChange]);
@@ -1413,13 +1416,21 @@ export default function BinVisualizer(props: Props) {
coldestNodeTemp={valueDisplay === "low" ? lowNodeConditions?.tempC : undefined}
cableNodeClicked={(cable, fillNode) => {
if (cable) {
- let devId = componentDevices?.get(cable.key());
+ let devId = componentDevices.get(cable.key());
if (devId) {
let device = devMap.get(devId);
if (device) {
setSelectedCable(cable);
setSelectedNode(fillNode);
setCableDevice(device);
+ //need to set the filtered components so that components on other devices do not show up as options in interaction settings
+ let filtered: Component[] = []
+ components.forEach((component, key) => {
+ if(componentDevices.get(key) === devId){
+ filtered.push(component)
+ }
+ })
+ setFilteredComponents(filtered)
setOpenNodeDialog(true);
}
}
@@ -1544,54 +1555,24 @@ export default function BinVisualizer(props: Props) {
Controllers
- {bin.status.fans.map(fan => (
-
-
-
-
- {fan.name}
-
-
-
- {fan.state ? "ON" : "OFF"}
-
-
- ))}
- {bin.status.heaters.map(heater => (
-
-
-
-
- {heater.name}
-
-
-
- {heater.state ? "ON" : "OFF"}
-
-
- ))}
+ {bin.status.fans.map(fan => {
+ let fanComp = components.get(fan.key)
+ let device = componentDevices.get(fan.key)
+ if(fanComp && device){
+ return (
+
+ )
+ }
+ })}
+ {bin.status.heaters.map(heater => {
+ let heaterComp = components.get(heater.key)
+ let device = componentDevices.get(heater.key)
+ if(heaterComp && device){
+ return (
+
+ )
+ }
+ })}
);
@@ -2098,7 +2079,7 @@ export default function BinVisualizer(props: Props) {
{selectedCable && cableDevice && (
{
})
export default function ButtonGroup(props: Props){
- const { buttons, toggle, toggledButtons, multiToggle, textSize, disableAll } = props
+ const { buttons, toggle, toggledButtons, defaultToggle, multiToggle, textSize, disableAll } = props
const classes = useStyles()
const [currentToggle, setCurrentToggle] = useState([])
@@ -107,6 +111,8 @@ export default function ButtonGroup(props: Props){
let toggled: number [] = [0]
if(toggledButtons){
toggled = toggledButtons
+ } else if(defaultToggle){
+ toggled = [defaultToggle]
}
setCurrentToggle(toggled)
}
diff --git a/src/common/RelativeTimestamp.tsx b/src/common/RelativeTimestamp.tsx
new file mode 100644
index 0000000..811ded6
--- /dev/null
+++ b/src/common/RelativeTimestamp.tsx
@@ -0,0 +1,38 @@
+import { Typography } from "@mui/material";
+import moment from "moment";
+import { useEffect, useState } from "react";
+
+interface Props {
+ /** RFC3339 timestamp string */
+ timestamp: string;
+}
+
+/**
+ * Displays a timestamp as relative time (e.g. "a minute ago", "5 seconds ago").
+ * Uses a subtle, muted typography style.
+ * Refreshes every 60 seconds to keep the relative text current.
+ */
+export default function RelativeTimestamp({ timestamp }: Props) {
+ const [, setTick] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => setTick((t) => t + 1), 60000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const date = moment(timestamp);
+ if (!date.isValid()) return null;
+
+ return (
+
+ {date.fromNow()}
+
+ );
+}
diff --git a/src/common/StatusDust.tsx b/src/common/StatusDust.tsx
index a7e8e3a..4963269 100644
--- a/src/common/StatusDust.tsx
+++ b/src/common/StatusDust.tsx
@@ -3,10 +3,13 @@ import { green } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { Device } from "models";
import PulseBox from "./PulseBox";
+import RelativeTimestamp from "./RelativeTimestamp";
import { or } from "utils";
import { useEffect, useState } from "react";
+
interface Props {
- device: Device
+ device: Device;
+ showTimestamp?: boolean;
}
const useStyles = makeStyles((theme: Theme) => {
@@ -48,7 +51,7 @@ const useStyles = makeStyles((theme: Theme) => {
})
export default function StatusDust(props: Props) {
- const { device } = props;
+ const { device, showTimestamp } = props;
const classes = useStyles()
const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []);
@@ -165,6 +168,11 @@ export default function StatusDust(props: Props) {
+ {showTimestamp && device.status.sen5x?.timestamp && (
+
+
+
+ )}
diff --git a/src/common/StatusGas.tsx b/src/common/StatusGas.tsx
index dcf7ed3..b220d96 100644
--- a/src/common/StatusGas.tsx
+++ b/src/common/StatusGas.tsx
@@ -3,13 +3,14 @@ import { blue, deepOrange, teal } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { Device } from "models";
import PulseBox from "./PulseBox";
+import RelativeTimestamp from "./RelativeTimestamp";
import { or } from "utils";
import { useEffect, useState } from "react";
interface Props {
- device: Device
- // noDust?: boolean;
- gasType: "co2" | "co" | "no2" | "o2" | "all"
+ device: Device;
+ gasType: "co2" | "co" | "no2" | "o2" | "lel" | "h2s" | "all";
+ showTimestamp?: boolean;
}
const useStyles = makeStyles((theme: Theme) => {
@@ -23,7 +24,7 @@ const useStyles = makeStyles((theme: Theme) => {
marginRight: "auto",
margin: theme.spacing(1),
width: theme.spacing(14),
- height: theme.spacing(6),
+ minHeight: theme.spacing(6),
},
boxTall: {
display: "inline-block",
@@ -33,7 +34,7 @@ const useStyles = makeStyles((theme: Theme) => {
marginRight: "auto",
margin: theme.spacing(1),
width: theme.spacing(14),
- height: theme.spacing(8.5),
+ minHeight: theme.spacing(8.5),
},
carouselContainer: {
position: 'relative',
@@ -61,29 +62,33 @@ const useStyles = makeStyles((theme: Theme) => {
})
export default function StatusGas(props: Props) {
- const { device, gasType } = props;
+ const { device, gasType, showTimestamp } = props;
const classes = useStyles()
// console.log(noDust)
let colors: string[] = []
let messages: string[] = []
if (gasType === "all") {
- colors = or(device.status.o2?.overlays.map(overlay => overlay.color), []);
- colors.push(...or(device.status.co2?.overlays.map(overlay => overlay.color), []));
- colors.push(...or(device.status.no2?.overlays.map(overlay => overlay.color), []));
- colors.push(...or(device.status.co?.overlays.map(overlay => overlay.color), []));
- messages = or(device.status.o2?.overlays.map(overlay => overlay.message), []);
- messages.push(...or(device.status.co2?.overlays.map(overlay => overlay.message), []));
- messages.push(...or(device.status.no2?.overlays.map(overlay => overlay.message), []));
- messages.push(...or(device.status.co?.overlays.map(overlay => overlay.message), []));
+ colors = or(device.status.o2?.overlays?.map(overlay => overlay.color), []);
+ colors.push(...or(device.status.co2?.overlays?.map(overlay => overlay.color), []));
+ colors.push(...or(device.status.no2?.overlays?.map(overlay => overlay.color), []));
+ colors.push(...or(device.status.co?.overlays?.map(overlay => overlay.color), []));
+ colors.push(...or(device.status.lel?.overlays?.map(overlay => overlay.color), []));
+ colors.push(...or(device.status.h2s?.overlays?.map(overlay => overlay.color), []));
+ messages = or(device.status.o2?.overlays?.map(overlay => overlay.message), []);
+ messages.push(...or(device.status.co2?.overlays?.map(overlay => overlay.message), []));
+ messages.push(...or(device.status.no2?.overlays?.map(overlay => overlay.message), []));
+ messages.push(...or(device.status.co?.overlays?.map(overlay => overlay.message), []));
+ messages.push(...or(device.status.lel?.overlays?.map(overlay => overlay.message), []));
+ messages.push(...or(device.status.h2s?.overlays?.map(overlay => overlay.message), []));
} else {
- colors = or(device.status[gasType]?.overlays.map(overlay => overlay.color), []);
- messages = or(device.status[gasType]?.overlays.map(overlay => overlay.message), []);
+ colors = or(device.status[gasType]?.overlays?.map(overlay => overlay.color), []);
+ messages = or(device.status[gasType]?.overlays?.map(overlay => overlay.message), []);
}
const [color, setColor] = useState(colors.length > 0 ? colors[0] : "");
const [, setColorIndex] = useState(0)
- const gasTypes: ("o2" | "no2" | "co2" | "co")[] = ["o2", "no2", "co2", "co"]
+ const gasTypes: ("o2" | "no2" | "co2" | "co" | "lel" | "h2s")[] = ["o2", "no2", "co2", "co", "lel", "h2s"]
useEffect(() => {
const interval = setInterval(() => {
@@ -106,8 +111,7 @@ export default function StatusGas(props: Props) {
return
}
const interval = setInterval(() => {
- if (gasType === "all") setCurrentIndex((prevIndex) => (prevIndex + 1) % 4);
- else setCurrentIndex((prevIndex) => (prevIndex + 1) % 4);
+ if (gasType === "all") setCurrentIndex((prevIndex) => (prevIndex + 1) % gasTypes.length);
}, 6000);
return () => clearInterval(interval); // Cleanup on unmount
@@ -188,14 +192,14 @@ export default function StatusGas(props: Props) {
}
- {gas !== "o2" ?
-
- Gas: {device.status[gas]?.ppm.toFixed(1)}ppm
-
- :
+ {(gas === "o2" || gas === "lel") ?
Gas: {(device.status[gas]?.ppm!/10000).toFixed(1)}%
+ :
+
+ Gas: {device.status[gas]?.ppm.toFixed(1)}ppm
+
}
@@ -203,6 +207,11 @@ export default function StatusGas(props: Props) {
Volt: {device.status[gas]?.millivolts.toFixed(1)}mV
+ {showTimestamp && device.status[gas]?.timestamp && (
+
+
+
+ )}
diff --git a/src/common/StatusPlenum.tsx b/src/common/StatusPlenum.tsx
index fe2b00d..250660a 100644
--- a/src/common/StatusPlenum.tsx
+++ b/src/common/StatusPlenum.tsx
@@ -3,11 +3,13 @@ import { blue, orange } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { Device } from "models";
import PulseBox from "./PulseBox";
+import RelativeTimestamp from "./RelativeTimestamp";
import { or } from "utils";
import { useEffect, useState } from "react";
interface Props {
- device: Device
+ device: Device;
+ showTimestamp?: boolean;
}
const useStyles = makeStyles((theme: Theme) => {
@@ -25,7 +27,7 @@ const useStyles = makeStyles((theme: Theme) => {
})
export default function StatusPlenum(props: Props) {
- const { device } = props;
+ const { device, showTimestamp } = props;
const classes = useStyles()
const colors = or(device.status.plenum?.overlays.map(overlay => overlay.color), []);
@@ -117,6 +119,11 @@ export default function StatusPlenum(props: Props) {
{device.status.plenum?.humidity.toFixed(1)}%
+ {showTimestamp && device.status.plenum?.timestamp && (
+
+
+
+ )}
diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx
index fad0ecb..533fde2 100644
--- a/src/common/StatusSen5x.tsx
+++ b/src/common/StatusSen5x.tsx
@@ -3,12 +3,14 @@ import { blue, green, orange } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { Device } from "models";
import PulseBox from "./PulseBox";
+import RelativeTimestamp from "./RelativeTimestamp";
import { or } from "utils";
import { useEffect, useState } from "react";
interface Props {
- device: Device
+ device: Device;
noDust?: boolean;
+ showTimestamp?: boolean;
}
const useStyles = makeStyles((theme: Theme) => {
@@ -50,7 +52,7 @@ const useStyles = makeStyles((theme: Theme) => {
})
export default function StatusSen5x(props: Props) {
- const { device, noDust } = props;
+ const { device, noDust, showTimestamp } = props;
const classes = useStyles()
// console.log(noDust)
const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []);
@@ -200,6 +202,11 @@ export default function StatusSen5x(props: Props) {
}
+ {showTimestamp && device.status.sen5x?.timestamp && (
+
+
+
+ )}
diff --git a/src/device/DeviceOverview.tsx b/src/device/DeviceOverview.tsx
index 0170bde..0115568 100644
--- a/src/device/DeviceOverview.tsx
+++ b/src/device/DeviceOverview.tsx
@@ -1,5 +1,5 @@
import { Chip, Grid2 as Grid, Theme, Tooltip, Skeleton } from "@mui/material";
-import { DataUsage, SimCard, Launch } from "@mui/icons-material";
+import { DataUsage, SimCard, Launch, DeviceHub } from "@mui/icons-material";
import StatusChip from "common/StatusChip";
import DeviceTags from "device/DeviceTags";
import VersionChip from "device/VersionChip";
@@ -233,6 +233,17 @@ export default function DeviceOverview(props: Props) {
)}
+ {device.settings.needsBusControl && (
+
+
+ }
+ />
+
+
+ )}
{}
);
diff --git a/src/device/DeviceSettings.tsx b/src/device/DeviceSettings.tsx
index dea3cc7..a9277f5 100644
--- a/src/device/DeviceSettings.tsx
+++ b/src/device/DeviceSettings.tsx
@@ -235,6 +235,12 @@ export default function DeviceSettings(props: Props) {
setDeviceForm(updatedForm);
};
+ const toggleBusRequired = (event: any) => {
+ let updatedForm = Device.clone(deviceForm);
+ updatedForm.settings.needsBusControl = event.target.checked;
+ setDeviceForm(updatedForm);
+ };
+
const toggleSleeps = (event: any) => {
let updatedForm = Device.clone(deviceForm);
let updatedSleeps = event.target.checked;
@@ -500,7 +506,8 @@ export default function DeviceSettings(props: Props) {
)}
{user.hasAdmin() && (
-
+
+
+
+
+ }
+ disabled={!canEdit}
+ label="I2C Bus Required"
+ labelPlacement="end"
+ />
+
+
)}
diff --git a/src/gate/GateDevice.tsx b/src/gate/GateDevice.tsx
index f62dd25..a445b28 100644
--- a/src/gate/GateDevice.tsx
+++ b/src/gate/GateDevice.tsx
@@ -126,9 +126,24 @@ export default function GateDevice(props: Props) {
}
}, [comprehensiveDevice, gate, linkedCompList, user]);
- const gateComponentUpdate = (componentKey: string, gateCompType: pond.GateComponentType) => {
+ const gateComponentUpdate = (oldKey: string, componentKey: string, gateCompType: pond.GateComponentType) => {
+ let promises: any[] = []
+ if (oldKey !== "") {
+ //the promise for removing the pref from the old component
+ promises.push(gateAPI
+ .updatePrefs(
+ gate.key,
+ "component",
+ device.id() + ":" + oldKey,
+ pond.GateComponentType.GATE_COMPONENT_TYPE_UNKNOWN,
+ [gate.key],
+ ["gate"],
+ as
+ ))
+ }
if (componentKey !== "") {
- gateAPI
+ //the promise for adding the pref to the new component
+ promises.push(gateAPI
.updatePrefs(
gate.key,
"component",
@@ -137,11 +152,17 @@ export default function GateDevice(props: Props) {
[gate.key],
["gate"],
as
- )
- .then(resp => {
- openSnack("Component Prefence Updated");
- });
+ ))
}
+ Promise.all(promises).then(resp => {
+ openSnack("Component Prefence Updated");
+ let clone = cloneDeep(gate.preferences)
+ clone[oldKey] = pond.GateComponentType.GATE_COMPONENT_TYPE_UNKNOWN
+ clone[componentKey] = gateCompType
+ gate.preferences = clone
+
+ }).catch(
+ );
};
useEffect(() => {
@@ -205,7 +226,7 @@ export default function GateDevice(props: Props) {
style={{ fontSize: 8 }}
onChange={e => {
var key = e.target.value as string;
- gateComponentUpdate(key, pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT);
+ gateComponentUpdate(ambientKey, key, pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT);
setAmbientKey(key);
if (tempKey === key) {
setTempKey("");
@@ -238,6 +259,7 @@ export default function GateDevice(props: Props) {
style={{ fontSize: 8 }}
onChange={e => {
gateComponentUpdate(
+ tempKey,
e.target.value as string,
pond.GateComponentType.GATE_COMPONENT_TYPE_TEMP
);
@@ -273,6 +295,7 @@ export default function GateDevice(props: Props) {
style={{ fontSize: 8 }}
onChange={e => {
gateComponentUpdate(
+ pressureKey,
e.target.value as string,
pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
);
diff --git a/src/gate/GateDeviceInteraction.tsx b/src/gate/GateDeviceInteraction.tsx
index 23669f3..0303123 100644
--- a/src/gate/GateDeviceInteraction.tsx
+++ b/src/gate/GateDeviceInteraction.tsx
@@ -93,15 +93,14 @@ export default function GateDeviceInteraction(props: Props) {
compDevice.components.forEach(comp => {
let component = Component.any(comp);
//checks the address for the LED components to get the red and green LED's
-
if (component.locationString() === redAddr && component.subType() === 1) {
setRedComponent(component);
} else if (component.locationString() === greenAddr && component.subType() === 1) {
setGreenComponent(component);
- } else if (component.type() === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE) {
- if (
+ } else if (
gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
) {
+ console.log(component)
setPressureComponent(component);
setPressureSource(
quack.ComponentID.create({
@@ -111,13 +110,13 @@ export default function GateDeviceInteraction(props: Props) {
})
)
}
- }
+
});
if(compDevice.device){
setDevice(Device.create(compDevice.device))
}
- }, [gate, densityTemp, user, compDevice]);
+ }, [gate, densityTemp, user, compDevice, gate.preferences]);
const buttonDisabled = () => {
if (greenComponent === undefined) return true
@@ -129,13 +128,13 @@ export default function GateDeviceInteraction(props: Props) {
const createInteractions = async () => {
//the interactions to be made
-
if (
greenComponent !== undefined &&
redComponent !== undefined &&
pressureComponent !== undefined
) {
-
+ //get the number of nodes in the pressure components
+ let nodes = pressureComponent.numNodes()
let lightInteractions: pond.InteractionSettings[] = []
//making variables for the parameters of the interactions
const redSink = quack.ComponentID.create({
@@ -163,19 +162,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
- value: -highDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
- value: -lowDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
- nodeOne: 2,
- nodeTwo: 1,
- subtype: 18,
+ nodeOne: nodes,
+ subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
@@ -201,14 +199,13 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
- value: -highDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
- nodeOne: 2,
- nodeTwo: 1,
- subtype: 18,
+ nodeOne: nodes,
+ subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@@ -221,19 +218,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
- value: -highDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
- value: -lowDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
- nodeOne: 2,
- nodeTwo: 1,
- subtype: 18,
+ nodeOne: nodes,
+ subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@@ -246,19 +242,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
- value: -lowDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
- value: -thresholdPascals,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: thresholdPascals,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
],
- nodeOne: 2,
- nodeTwo: 1,
- subtype: 18,
+ nodeOne: nodes,
+ subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@@ -276,7 +271,8 @@ export default function GateDeviceInteraction(props: Props) {
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
],
- subtype: 1,
+ nodeOne: nodes,
+ subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET,
@@ -291,19 +287,18 @@ export default function GateDeviceInteraction(props: Props) {
source: pressureSource,
conditions: [
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
- value: -highDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
+ value: highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
- comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
- value: -lowDelta,
+ comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
+ value: lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
- nodeOne: 2,
- nodeTwo: 1,
- subtype: 18,
+ nodeOne: nodes,
+ subtype: nodes+1,
schedule: lightSchedule,
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
@@ -393,7 +388,6 @@ export default function GateDeviceInteraction(props: Props) {
color="primary"
onClick={() => {
//TODO: Once we figure out how to fix the backend to handle rapid addition/removal of interactions
- //removeCurrentInteractions();
setAdding(true);
interactionsAPI.clearInteractions(device.id(), [pressureSource]).then(resp => {
createInteractions();
@@ -401,7 +395,8 @@ export default function GateDeviceInteraction(props: Props) {
console.error(err)
})
}}
- disabled={buttonDisabled()}>
+ disabled={buttonDisabled()}
+ >
{adding ? "Adding Interaction" : "Create Interaction"}
diff --git a/src/gate/GateList.tsx b/src/gate/GateList.tsx
index 2cdec52..5910808 100644
--- a/src/gate/GateList.tsx
+++ b/src/gate/GateList.tsx
@@ -1,5 +1,5 @@
import { Gate } from "models/Gate";
-import { Box, Card, IconButton, List, ListItem, ListItemText, Theme, Typography } from "@mui/material";
+import { Box, Card, IconButton, List, ListItem, ListItemText, Theme, Tooltip, Typography } from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useMobile } from "hooks";
@@ -20,6 +20,7 @@ import { quack } from "protobuf-ts/quack";
import { getTemperatureUnit } from "utils";
import { teal } from "@mui/material/colors";
import { react } from "@babel/types";
+import { getDeviceStateHelper } from "pbHelpers/DeviceState";
interface Props {
//gates: Gate[];
@@ -42,6 +43,12 @@ const useStyles = makeStyles((theme: Theme) => ({
gateCard: {
marginBottom: 10,
paddingLeft: 15
+ },
+ cellContainer: {
+ margin: theme.spacing(1),
+ marginLeft: theme.spacing(2),
+ display: "flex",
+ width: theme.spacing(10)
}
}));
@@ -100,13 +107,33 @@ export default function GateList(props: Props) {
const displayPCAStatus = (state: pond.PCAState) => {
switch(state){
case pond.PCAState.PCA_STATE_IN_BOUNDS:
- return
+ return (
+
+
+ PCA in threshold
+
+ )
case pond.PCAState.PCA_STATE_OUT_BOUNDS:
- return
+ return (
+
+
+ PCA out of threshold
+
+ )
case pond.PCAState.PCA_STATE_OFF:
- return
+ return (
+
+
+ PCA off
+
+ )
default:
- return
+ return (
+
+
+ PCA device not found
+
+ )
}
}
@@ -166,7 +193,7 @@ export default function GateList(props: Props) {
const conditionDisplay = (gate: Gate) => {
let display = ""
if(gate.status.pcaState === pond.PCAState.PCA_STATE_OFF){
- display = "Inactive"
+ display = "PCA Off"
} else if (gate.status.pcaState === pond.PCAState.PCA_STATE_UNKNOWN){
display = "--"
} else { //the pca is currently active calulate the delta temp (outlet - ambient)
@@ -241,6 +268,19 @@ export default function GateList(props: Props) {
)
},
+ {
+ title: "Device State",
+ // cellStyle: {width: 100000},
+ // sortKey: "state",
+ render: gate => {
+ const deviceStateHelper = getDeviceStateHelper(gate.status.pcaDeviceState);
+ return (
+
+ {deviceStateHelper.icon}
+
+ )
+ }
+ },
{
title: "PCA Status",
render: gate => {
@@ -324,6 +364,7 @@ export default function GateList(props: Props) {
}
const gutter = (gate: Gate) => {
+ const deviceStateHelper = getDeviceStateHelper(gate.status.pcaDeviceState)
return(
@@ -334,6 +375,12 @@ export default function GateList(props: Props) {
Delta Temperature:
{conditionDisplay(gate)}
+
+ Device State:
+
+ {deviceStateHelper.icon}
+
+
{/* taking these out for now because we are not sure if the customer wants them */}
{/*
Estimated Temp:
diff --git a/src/hooks/useDeviceStatusStreams.ts b/src/hooks/useDeviceStatusStreams.ts
new file mode 100644
index 0000000..24ca8bb
--- /dev/null
+++ b/src/hooks/useDeviceStatusStreams.ts
@@ -0,0 +1,138 @@
+import { pond } from "protobuf-ts/pond";
+import { useEffect, useRef, useCallback, useMemo } from "react";
+
+/**
+ * Derives the WebSocket base URL from VITE_APP_API_URL.
+ * Matches the logic in useWebSocket.ts.
+ */
+function getWsBaseUrl(): string {
+ const apiUrl = import.meta.env.VITE_APP_API_URL;
+ if (apiUrl) {
+ return apiUrl.replace(/^http/, "ws");
+ }
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+ return `${protocol}//${window.location.host}`;
+}
+
+export interface UseDeviceStatusStreamsOptions {
+ /** Device IDs to stream status for (e.g. from the visible devices list) */
+ deviceIds: string[];
+ /** Called when a device status update is received */
+ onStatusUpdate: (deviceId: string, status: pond.DeviceStatus) => void;
+ /** Auth token passed as ?token= query param */
+ token?: string;
+ /** Whether the connections should be active. Default true. */
+ enabled?: boolean;
+}
+
+/**
+ * Subscribes to live device status streams for multiple devices.
+ * Uses the /v1/live/devices/:device/status endpoint per device.
+ * Readings (plenum, sen5x, co, co2, no2, o2, etc.) stream in real time.
+ */
+export function useDeviceStatusStreams(options: UseDeviceStatusStreamsOptions) {
+ const { deviceIds, onStatusUpdate, token, enabled = true } = options;
+
+ const onStatusUpdateRef = useRef(onStatusUpdate);
+ onStatusUpdateRef.current = onStatusUpdate;
+
+ const wsMapRef = useRef