bin sensors add the bins

This commit is contained in:
Carter 2025-03-13 09:26:47 -06:00
parent c46cb5ce4b
commit b5071dfb40
9 changed files with 7346 additions and 20 deletions

View file

@ -28,7 +28,7 @@ import ObjectTeams from "teams/ObjectTeams";
// import BinDuplication from "./BinDuplication";
import BinsIcon from "products/Bindapt/BinsIcon";
import HelpIcon from "@mui/icons-material/Help";
// import BinSensors from "./BinSensors";
import BinSensors from "./BinSensors";
import { useGlobalState, useSnackbar, useUserAPI } from "providers";
import { useMobile } from "hooks";
import { makeStyles } from "@mui/styles";
@ -239,7 +239,7 @@ export default function BinActions(props: Props) {
setOpenState({ ...openState, settings: false });
}}
/>
{/* <BinSensors
<BinSensors
mode="update"
bin={bin}
canEdit={hasWritePermission}
@ -254,7 +254,7 @@ export default function BinActions(props: Props) {
}
setOpenState({ ...openState, sensors: false });
}}
/> */}
/>
<ShareObject
scope={binScope(key)}
label={label}

View file

@ -0,0 +1,926 @@
import {
Avatar,
Box,
darken,
Divider,
Grid2 as Grid,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
ListSubheader,
Menu,
MenuItem,
Theme,
Typography,
useTheme
} from "@mui/material";
import { Component } from "models";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { pond, quack } from "protobuf-ts/pond";
import { useBinAPI, useSnackbar, useGlobalState } from "providers";
import React, { useState, useEffect, useCallback } from "react";
import { getThemeType } from "theme";
import { Plenum } from "models/Plenum";
import { Controller } from "models/Controller";
import { GrainCable } from "models/GrainCable";
import { Pressure } from "models/Pressure";
import { Headspace } from "models/Headspace";
import { MoreVert } from "@mui/icons-material";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { Ambient } from "models/Ambient";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => {
return ({
bgItem: {
background: darken(theme.palette.background.paper, getThemeType() === "light" ? 0.05 : 0.25),
position: "relative",
width: "auto",
borderRadius: theme.spacing(0.5),
display: "flex",
flexDirection: "column",
padding: theme.spacing(0.5)
},
avatar: {
width: theme.spacing(3),
height: theme.spacing(3),
marginTop: "auto",
marginBottom: "auto"
},
avatarIcon: {
background: "transparent",
width: theme.spacing(3),
height: theme.spacing(3),
margin: "auto",
marginBottom: "auto"
},
spacingItem: {
paddingLeft: theme.spacing(0.75),
paddingRight: theme.spacing(1),
display: "flex",
flexDirection: "row",
justifyContent: "center"
},
pointerLeft: {
borderBottom: "1px solid",
borderLeft: "1px solid",
borderColor: theme.palette.text.primary,
width: theme.spacing(5.5),
height: theme.spacing(0.7)
},
pointerRight: {
borderBottom: "1px solid white",
borderLeft: "1px solid white",
borderRight: "1px solid white",
borderColor: theme.palette.text.primary,
width: theme.spacing(5.5),
height: theme.spacing(0.7)
},
spacer: {
paddingLeft: theme.spacing(0.25)
},
smallFont: {
fontSize: "10px"
},
bigSpacer: {
paddingLeft: theme.spacing(3.2)
}
});
});
interface Props {
bin: string;
components: Map<string, Component>;
preferences: Map<string, pond.BinComponentPreferences>;
setPreferences: React.Dispatch<
React.SetStateAction<Map<string, pond.BinComponentPreferences> | undefined>
>;
updateBinStatus: (componentKeys: string[], removed?: boolean) => void;
setComponents: React.Dispatch<React.SetStateAction<Map<string, Component>>>;
}
export default function BinComponentTypes(props: Props) {
const { bin, components, preferences, setPreferences, updateBinStatus } = props;
const classes = useStyles();
const theme = useTheme();
const binAPI = useBinAPI();
const snackbar = useSnackbar();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
//only used to determine what preferences are options for the selected component ie. DHT can be a plenum or a headspace
const [selectedComponentType, setSelectedComponentType] = useState<
quack.ComponentType | undefined
>(undefined);
const [componentUnassigned, setComponentUnnassigned] = useState(false);
const [unassigned, setUnassigned] = useState<Component[]>([]);
const [plenums, setPlenums] = useState<Plenum[]>([]);
const [ambients, setAmbients] = useState<Ambient[]>([]);
const [pressures, setPressures] = useState<Pressure[]>([]);
const [grainCables, setGrainCables] = useState<GrainCable[]>([]);
const [headspace, setHeadspace] = useState<Headspace[]>([]);
const [heaters, setHeaters] = useState<Controller[]>([]);
const [fans, setFans] = useState<Controller[]>([]);
const [lidars, setLidars] = useState<Component[]>([]);
const [headspaceCo2s, setHeadspaceCo2s] = useState<Component[]>([]);
const [tempUnit, setTempUnit] = useState<pond.TemperatureUnit>();
const [pressureUnit, setPressureUnit] = useState<pond.PressureUnit>();
const [selectedComponentKey, setSelectedComponentKey] = useState("");
const [selectedComponentSubtype, setSelectedComponentSubtype] = useState(0);
const [selectedComponentTopNode, setSelectedComponentTopNode] = useState(0);
const [{ user }] = useGlobalState();
useEffect(() => {
if (user.settings.temperatureUnit) {
setTempUnit(user.settings.temperatureUnit);
} else {
setTempUnit(pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS);
}
if (user.settings.pressureUnit) {
setPressureUnit(user.settings.pressureUnit);
} else {
setPressureUnit(pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER);
}
}, [user]);
const selectType = useCallback(
(component: string, type: pond.BinComponent, topNode?: number) => {
setAnchorEl(null);
let p = preferences.get(component);
if (p) {
p.type = type;
p.node = topNode ?? 0;
binAPI
.updateComponentPreferences(bin, component, p)
.then(() => {
snackbar.success("Component preferences updated");
preferences.set(component, p!);
setPreferences(new Map(preferences));
})
.catch(() => {
snackbar.error("Component preference update failed");
});
}
},
[bin, binAPI, preferences, setPreferences, snackbar]
);
useEffect(() => {
var plenums: Plenum[] = [];
var grainCables: GrainCable[] = [];
var heaters: Controller[] = [];
var fans: Controller[] = [];
var pressures: Pressure[] = [];
var headspace: Headspace[] = [];
var lidar: Component[] = [];
var unassigned: Component[] = [];
var ambients: Ambient[] = [];
var headspaceCo2s: Component[] = [];
components.forEach(comp => {
let pref = preferences.get(comp.key());
if (pref) {
if (pref.type) {
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM)
plenums.push(Plenum.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT)
ambients.push(Ambient.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE)
grainCables.push(GrainCable.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER)
heaters.push(Controller.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) fans.push(Controller.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_PRESSURE)
pressures.push(Pressure.create(comp));
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEADSPACE) {
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_DHT) {
headspace.push(Headspace.create(comp));
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) {
lidar.push(comp);
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) {
headspaceCo2s.push(comp);
}
}
} else {
unassigned.push(comp);
}
} else {
unassigned.push(comp);
}
});
setPlenums(plenums);
setAmbients(ambients);
setGrainCables(grainCables);
setHeaters(heaters);
setFans(fans);
setPressures(pressures);
setHeadspace(headspace);
setLidars(lidar);
setHeadspaceCo2s(headspaceCo2s);
setUnassigned(unassigned);
}, [components, preferences, setPlenums, setGrainCables, setHeaters, selectType, setPressures]);
const getOptions = () => {
let options: JSX.Element[] = [];
if (!componentUnassigned) {
options.push(
<MenuItem
key="remove"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_UNKNOWN);
updateBinStatus([selectedComponentKey], true);
}}>
Remove
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE
) {
options.push(
<MenuItem
key="ambientOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_AMBIENT);
}}>
Ambient
</MenuItem>
);
options.push(
<MenuItem
key="plenumOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PLENUM);
}}>
Plenum
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_DHT ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_LIDAR ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_CO2
) {
options.push(
<MenuItem
key="headspaceOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_HEADSPACE);
updateBinStatus([selectedComponentKey]);
}}>
Headspace
</MenuItem>
);
}
if (
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE ||
selectedComponentType === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE
) {
options.push(
<MenuItem
key="pressureOption"
onClick={() => {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_PRESSURE);
}}>
Plenum Pressure
</MenuItem>
);
}
if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
options.push(
<MenuItem
key="cableOption"
onClick={() => {
selectType(
selectedComponentKey,
pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE,
selectedComponentTopNode
);
}}>
Grain Cable
</MenuItem>
);
}
if (selectedComponentType === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT) {
options.push(
<MenuItem
key="controlOption"
onClick={() => {
if (
selectedComponentSubtype === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER
) {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_HEATER);
} else if (
selectedComponentSubtype ===
quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN
) {
selectType(selectedComponentKey, pond.BinComponent.BIN_COMPONENT_FAN);
}
}}>
Heaters & Fans
</MenuItem>
);
}
if (options.length === 0) {
return <MenuItem>No Available Options</MenuItem>;
} else {
return options;
}
};
const assignmentMenu = () => {
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{getOptions()}
</Menu>
);
};
const grainCableList = () => {
if (grainCables.length < 1) return null;
return (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Grain Cables
</ListSubheader>
<Divider />
{grainCables.map((cable, index) => {
let cIcon = GetComponentIcon(
cable.settings.type,
cable.settings.subtype,
theme.palette.mode
);
return (
<ListItem key={index} style={{ height: theme.spacing(8) }}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={cable.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{cable.name()}</ListItemText>
<ListItemSecondaryAction>
<Grid container direction="row" alignItems="center">
<Grid>
<Box className={classes.bgItem}>
<Box style={{ display: "flex", flexDirection: "row" }}>
<Box>
<Typography variant="body2" color="textSecondary">
{tempUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "°C"}
</Typography>
</Box>
<Box display={"flex"} alignItems="center" textAlign="center">
<Typography
variant="body2"
style={{ color: cable.tempColour(), marginLeft: theme.spacing(1.5) }}>
{isFinite(cable.minTemp(tempUnit)) && !isNaN(cable.minTemp(tempUnit))
? cable.minTemp(tempUnit).toFixed(1)
: "--"}
</Typography>
<Typography
variant="body2"
style={{ color: cable.tempColour(), marginLeft: theme.spacing(1.5) }}>
{isFinite(cable.aveTemp(tempUnit)) && !isNaN(cable.aveTemp(tempUnit))
? cable.aveTemp(tempUnit).toFixed(1)
: "--"}
</Typography>
<Typography
variant="body2"
style={{ color: cable.tempColour(), marginLeft: theme.spacing(1.5) }}>
{isFinite(cable.maxTemp(tempUnit)) && !isNaN(cable.maxTemp(tempUnit))
? cable.maxTemp(tempUnit).toFixed(1)
: "--"}
</Typography>
</Box>
</Box>
<Box marginY={0.25}>
<Divider style={{ background: "gold" }} />
</Box>
<Box style={{ display: "flex", flexDirection: "row" }}>
<Box>
<Typography
variant="body2"
color="textSecondary"
style={{ marginLeft: theme.spacing(0.4) }}>
%
</Typography>
</Box>
<Box display={"flex"} alignItems="center" textAlign="center">
<Typography
variant="body2"
style={{ color: cable.humidColour(), marginLeft: theme.spacing(1.5) }}>
{isFinite(cable.minHumidity()) && !isNaN(cable.minHumidity())
? cable.minHumidity().toFixed(1)
: "--"}
</Typography>
<Typography
variant="body2"
style={{ color: cable.humidColour(), marginLeft: theme.spacing(1.5) }}>
{isFinite(cable.aveHumidity()) && !isNaN(cable.aveHumidity())
? cable.aveHumidity().toFixed(1)
: "--"}
</Typography>
<Typography
variant="body2"
style={{ color: cable.humidColour(), marginLeft: theme.spacing(1.5) }}>
{isFinite(cable.maxHumidity()) && !isNaN(cable.maxHumidity())
? cable.maxHumidity().toFixed(1)
: "--"}
</Typography>
</Box>
</Box>
</Box>
</Grid>
<Grid >
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false);
setSelectedComponentType(cable.type());
setSelectedComponentKey(cable.key());
setSelectedComponentTopNode(cable.settings.grainFilledTo);
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</Grid>
</Grid>
</ListItemSecondaryAction>
</ListItem>
);
})}
</React.Fragment>
);
};
const controllerList = (heaters: Controller[], fans: Controller[]) => {
if (heaters.length < 1 && fans.length < 1) return null;
let controllers = heaters.concat(fans);
return (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Heaters & Fans
</ListSubheader>
<Divider />
{controllers.map((controller, index) => {
let cIcon = GetComponentIcon(
controller.settings.type,
controller.settings.subtype,
theme.palette.mode
);
return (
<ListItem key={index}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={controller.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{controller.name()}</ListItemText>
<ListItemSecondaryAction>
<Grid container direction="row" alignItems="center">
<Grid >
<Box className={classes.bgItem} padding={1}>
<div style={{ display: "flex", flexDirection: "row", margin: "auto" }}>
<Typography variant="body2" style={{ color: controller.color() }}>
{controller.on ? "On" : "Off"}
</Typography>
</div>
</Box>
</Grid>
<Grid >
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false);
setSelectedComponentType(controller.type());
setSelectedComponentSubtype(controller.subType());
setSelectedComponentKey(controller.key());
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</Grid>
</Grid>
</ListItemSecondaryAction>
</ListItem>
);
})}
</React.Fragment>
);
};
const pressureList = () => {
if (pressures.length < 1) return null;
return (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Plenum Pressure
</ListSubheader>
<Divider />
{pressures.map((component, index) => {
let cIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
theme.palette.mode
);
return (
<ListItem key={index}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={component.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{component.name()}</ListItemText>
<ListItemSecondaryAction>
<Grid container direction="row" alignItems="center">
<Grid>
<Box className={classes.bgItem} padding={1}>
<div style={{ display: "flex", flexDirection: "row", margin: "auto" }}>
<Typography variant="body2" style={{ color: Pressure.colour() }}>
{component.getPressureString(pressureUnit)}
</Typography>
</div>
</Box>
</Grid>
<Grid>
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false);
setSelectedComponentType(component.type());
setSelectedComponentKey(component.key());
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</Grid>
</Grid>
</ListItemSecondaryAction>
</ListItem>
);
})}
</React.Fragment>
);
};
const plenumList = (plenums: Plenum[]) => {
if (plenums.length < 1) return null;
return (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Plenums
</ListSubheader>
<Divider />
{plenums.map((component, index) => {
let cIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
theme.palette.mode
);
return (
<ListItem key={index}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={component.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{component.name()}</ListItemText>
<ListItemSecondaryAction>
<Grid container direction="row" alignItems="center">
<Grid>
<Box className={classes.bgItem} padding={1}>
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2" style={{ color: Plenum.tempColour() }}>
{component.getTempString(user.settings.temperatureUnit) + ","}
</Typography>
<Typography
variant="body2"
style={{
color: Plenum.humidColour(),
marginLeft: theme.spacing(1)
}}>
{component.getHumidityString()}
</Typography>
</div>
</Box>
</Grid>
<Grid>
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false);
setSelectedComponentType(component.type());
setSelectedComponentKey(component.key());
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</Grid>
</Grid>
</ListItemSecondaryAction>
</ListItem>
);
})}
</React.Fragment>
);
};
const ambientList = (ambients: Ambient[]) => {
if (ambients.length < 1) return null;
return (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Ambient
</ListSubheader>
<Divider />
{ambients.map((component, index) => {
let cIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
theme.palette.mode
);
return (
<ListItem key={index}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={component.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{component.name()}</ListItemText>
<ListItemSecondaryAction>
<Grid container direction="row" alignItems="center">
<Grid>
<Box className={classes.bgItem} padding={1}>
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2" style={{ color: Ambient.tempColour() }}>
{component.getTempString(user.settings.temperatureUnit) + ","}
</Typography>
<Typography
variant="body2"
style={{
color: Ambient.humidColour(),
marginLeft: theme.spacing(1)
}}>
{component.getHumidityString()}
</Typography>
</div>
</Box>
</Grid>
<Grid>
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false);
setSelectedComponentType(component.type());
setSelectedComponentKey(component.key());
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</Grid>
</Grid>
</ListItemSecondaryAction>
</ListItem>
);
})}
</React.Fragment>
);
};
const formatDistance = (distanceCM: number) => {
if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
return distanceCM / 100 + "m";
} else if (user.settings.distanceUnit === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
return (distanceCM / 30.48).toFixed(2) + "ft";
}
return distanceCM + "cm";
};
const headspaceList = (sensors: Headspace[], lidars: Component[], headspaceCo2s: Component[]) => {
if (sensors.length < 1 && lidars.length < 1 && headspaceCo2s.length < 1) return null;
let headspaceComponents = lidars.concat(headspaceCo2s);
return (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Headspace
</ListSubheader>
<Divider />
{sensors.map((component, index) => {
let cIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
theme.palette.mode
);
return (
<ListItem key={index}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={component.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{component.name()}</ListItemText>
<ListItemSecondaryAction>
<Grid container direction="row" alignItems="center">
<Grid >
<Box className={classes.bgItem} padding={1}>
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography variant="body2" style={{ color: Plenum.tempColour() }}>
{component.getTempString(user.settings.temperatureUnit) + ","}
</Typography>
<Typography
variant="body2"
style={{
color: Plenum.humidColour(),
marginLeft: theme.spacing(1)
}}>
{component.getHumidityString()}
</Typography>
</div>
</Box>
</Grid>
<Grid >
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false);
setSelectedComponentType(component.type());
setSelectedComponentKey(component.key());
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</Grid>
</Grid>
</ListItemSecondaryAction>
</ListItem>
);
})}
{headspaceComponents.map((component, index) => {
let cIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
theme.palette.mode
);
let colour = "white"; //default colour if there are no measurements to get the type from
let dString = "--";
if (
component.status.measurement[0] &&
component.status.measurement[0].values[0] &&
component.status.measurement[0].values[0].values[0]
) {
colour = describeMeasurement(
component.status.measurement[0].type,
component.type(),
component.subType()
).colour();
dString = formatDistance(component.status.measurement[0].values[0].values[0]);
}
return (
<ListItem key={index}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={component.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{component.name()}</ListItemText>
<ListItemSecondaryAction>
<Grid container direction="row" alignItems="center">
<Grid >
<Box className={classes.bgItem} padding={1}>
<div style={{ display: "flex", flexDirection: "row" }}>
<Typography
variant="body2"
style={{
color: colour
}}>
{dString}
</Typography>
</div>
</Box>
</Grid>
<Grid >
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(false);
setSelectedComponentType(component.type());
setSelectedComponentKey(component.key());
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</Grid>
</Grid>
</ListItemSecondaryAction>
</ListItem>
);
})}
</React.Fragment>
);
};
const unassignedList = (sensors: Component[]) => {
if (sensors.length < 1) return null;
return (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Unassigned Components
</ListSubheader>
<Divider />
{sensors.map((component, index) => {
let cIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
theme.palette.mode
);
return (
<ListItem key={index}>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={component.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{component.name()}</ListItemText>
<ListItemSecondaryAction>
<IconButton
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
setComponentUnnassigned(true);
setSelectedComponentType(component.type());
setSelectedComponentSubtype(component.subType());
setSelectedComponentKey(component.key());
setAnchorEl(event.currentTarget);
}}>
<MoreVert />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
);
})}
</React.Fragment>
);
};
const noComponents = () => {
return (
<Box style={{ margin: "auto" }} textAlign="center">
<Typography variant="subtitle1" color="textSecondary">
No attached components.
</Typography>
<Typography variant="subtitle1" color="textSecondary">
Go to settings to assign components from your devices.
</Typography>
</Box>
);
};
if (components.size < 1) return noComponents();
return (
<React.Fragment>
<List
//subheader={<ListSubheader>Bin Components</ListSubheader>}
style={{ padding: theme.spacing(1), width: "100%" }}>
{plenumList(plenums)}
{ambientList(ambients)}
{headspaceList(headspace, lidars, headspaceCo2s)}
{grainCableList()}
{controllerList(heaters, fans)}
{pressureList()}
{unassignedList(unassigned)}
{assignmentMenu()}
</List>
</React.Fragment>
);
}

605
src/bin/BinComponents.tsx Normal file
View file

@ -0,0 +1,605 @@
import {
Autocomplete,
Avatar,
Box,
Button,
CircularProgress,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
Grid2 as Grid,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemSecondaryAction,
ListItemText,
ListSubheader,
Tab,
Tabs,
TextField,
Theme,
Tooltip,
Typography,
useTheme
} from "@mui/material";
import { Component, Device } from "models";
import { pond } from "protobuf-ts/pond";
import React, { useCallback, useEffect, useState } from "react";
import { useComponentAPI, useDeviceAPI, useSnackbar } from "hooks";
// import { Autocomplete } from "@material-ui/lab";
import { useBinAPI } from "providers";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { CheckBox as CheckBoxIcon, CheckBoxOutlineBlank, Remove } from "@mui/icons-material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { quack } from "protobuf-ts/quack";
import { GetProductDefaults } from "products/DeviceProduct";
import DeviceWizard from "device/DeviceWizard";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => {
return ({
stepper: {
padding: theme.spacing(0.5)
},
secondaryColor: {
color: theme.palette.secondary.main
},
textSecondaryColor: {
color: theme.palette.text.secondary
},
bottomSpacing: {
marginBottom: theme.spacing(1)
},
tabs: {
width: "100%",
boxSizing: "border-box",
flexShrink: 1
},
tabSmall: {
width: "100%",
boxSizing: "border-box",
flexShrink: 1,
minWidth: "48px",
marginTop: 0,
paddingTop: 0
}
})
});
interface Props {
components?: Map<string, Component>;
setComponents?: React.Dispatch<React.SetStateAction<Map<string, Component>>>;
bin: string;
binGrain: pond.Grain;
updateBinStatus?: (componentKeys: string[], removed?: boolean) => void;
}
interface Option {
label: string;
id: number;
icon?: string;
}
export default function BinComponents(props: Props) {
const { components, bin, setComponents, updateBinStatus, binGrain } = props;
const classes = useStyles();
const binAPI = useBinAPI();
const theme = useTheme();
const deviceAPI = useDeviceAPI();
const componentAPI = useComponentAPI();
const snackbar = useSnackbar();
const [devicesLoading, setDevicesLoading] = useState<boolean>(false);
const [componentsLoading, setComponentsLoading] = useState<boolean>(false);
const [deviceOptions, setDeviceOptions] = useState<Option[]>([]);
const [devMap, setDevMap] = useState<Map<number, Device>>(new Map());
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
const [deviceComponents] = useState<Map<number, Component[]>>(new Map<number, Component[]>());
const [activeStep, setActiveStep] = useState(0);
const [componentToRemove, setComponentToRemove] = useState<Component | undefined>(undefined);
const [removeAllDialog, setRemoveAllDialog] = useState(false);
const [wizardOpen, setWizardOpen] = useState(false);
const [setupAllowed, setSetupAllowed] = useState(false);
const loadDevices = useCallback(() => {
let mounted = true;
setDevicesLoading(true);
deviceAPI
.list(100000, 0, "asc", "name")
.then(response => {
if (mounted) {
let data = response.data.devices ? response.data.devices : [];
let newDevMap: Map<number, Device> = new Map();
let devices: Option[] = data.map(d => {
let device = Device.create(pond.Device.fromObject(d));
newDevMap.set(device.id(), device);
return { id: device.id(), label: device.name() } as Option;
});
setDevMap(newDevMap);
setDeviceOptions(devices);
}
})
.catch(() => {
if (mounted) {
setDeviceOptions([]);
}
})
.finally(() => {
if (mounted) {
setDevicesLoading(false);
}
});
return () => {
mounted = false;
};
}, [deviceAPI]);
useEffect(() => {
loadDevices();
}, [loadDevices]);
const loadComponents = useCallback(
(forceLoad?: boolean) => {
if (selectedDevice < 1) return;
let comps = deviceComponents.get(selectedDevice);
if (comps === undefined || forceLoad) {
comps = [];
setComponentsLoading(true);
componentAPI
.list(selectedDevice, false, [selectedDevice.toString()], ["device"], true)
.then(resp => {
let d = pond.ListComponentsResponse.fromObject(resp.data);
d.components.forEach(comp => {
// Don't show modems
if (comp.settings?.type === quack.ComponentType.COMPONENT_TYPE_MODEM) return;
let c = Component.create(comp);
if (c.permissions.includes(pond.Permission.PERMISSION_WRITE)) {
setSetupAllowed(true);
}
comps!.push(c);
});
if (comps) deviceComponents.set(selectedDevice, comps);
})
.catch(err => {
snackbar.error(err);
})
.finally(() => {
setComponentsLoading(false);
});
}
},
[selectedDevice, componentAPI, deviceComponents, snackbar]
);
useEffect(() => {
loadComponents();
}, [loadComponents]);
// useEffect(() => {
// if (selectedDevice < 1) return;
// let comps = deviceComponents.get(selectedDevice);
// if (comps === undefined) {
// comps = [];
// setComponentsLoading(true);
// componentAPI
// .list(selectedDevice, false, [selectedDevice.toString()], ["device"], true)
// .then(resp => {
// let d = pond.ListComponentsResponse.fromObject(resp.data);
// d.components.forEach(comp => {
// // Don't show modems
// if (comp.settings?.type === quack.ComponentType.COMPONENT_TYPE_MODEM) return;
// let c = Component.create(comp)
// if(c.permissions.includes(pond.Permission.PERMISSION_WRITE)){
// setSetupAllowed(true)
// }
// comps!.push(c);
// });
// if (comps) deviceComponents.set(selectedDevice, comps);
// })
// .catch(err => {
// snackbar.error(err);
// })
// .finally(() => {
// setComponentsLoading(false);
// });
// }
// }, [selectedDevice, componentAPI, deviceComponents, snackbar]);
const removeComponent = (component: string) => {
binAPI.removeComponent(bin, component).then(() => {
if (components && setComponents) {
if (components.delete(component)) {
let newComponents = new Map(components);
setComponents(newComponents);
}
}
snackbar.info("Component removed from bin");
});
setComponentToRemove(undefined);
};
const removeComponentConfirmation = () => {
return (
<ResponsiveDialog
fullScreen={false}
open={componentToRemove !== undefined}
onClose={() => setComponentToRemove(undefined)}>
<DialogTitle>Remove {componentToRemove?.name()}?</DialogTitle>
<DialogContent>
<DialogContentText>
This will remove {componentToRemove?.name()} from this bin. If you don't have direct
access to this component or the device it is attached to, you will not be able to add it
back.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setComponentToRemove(undefined)} color="primary">
Cancel
</Button>
<Button onClick={() => removeComponent(componentToRemove!.key())} color="primary">
Remove
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const removeAllConfirmation = () => {
return (
<ResponsiveDialog
fullScreen={false}
open={removeAllDialog}
onClose={() => setRemoveAllDialog(false)}>
<DialogTitle>Remove All Components?</DialogTitle>
<DialogContent>
<DialogContentText>
This will remove All attached components from this bin. If you don't have direct access
to these components or the devices they are attached to, you will not be able to add
them back.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setRemoveAllDialog(false)} color="primary">
Cancel
</Button>
<Button onClick={() => removeAll()} color="primary">
Remove
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const determinePreset = (comp: Component, devProduct?: pond.DeviceProduct) => {
let preset = pond.BinComponent.BIN_COMPONENT_UNKNOWN;
if (devProduct) {
let defaults = GetProductDefaults(devProduct);
let plenumAddresses: string[] | undefined = defaults?.get("plenum");
let headspaceAddresses: string[] | undefined = defaults?.get("headspace");
let pressureAddresses: string[] | undefined = defaults?.get("pressure");
let heatersFansAddresses: string[] | undefined = defaults?.get("heatersFans");
if (plenumAddresses && plenumAddresses.includes(comp.locationString())) {
preset = pond.BinComponent.BIN_COMPONENT_PLENUM;
} else if (pressureAddresses && pressureAddresses.includes(comp.locationString())) {
preset = pond.BinComponent.BIN_COMPONENT_PRESSURE;
} else if (headspaceAddresses && headspaceAddresses.includes(comp.locationString())) {
preset = pond.BinComponent.BIN_COMPONENT_HEADSPACE;
} else if (heatersFansAddresses && heatersFansAddresses.includes(comp.locationString())) {
switch (comp.subType()) {
case quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER:
preset = pond.BinComponent.BIN_COMPONENT_HEATER;
break;
case quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_AERATION_FAN:
case quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_EXHAUST_FAN:
preset = pond.BinComponent.BIN_COMPONENT_FAN;
break;
}
} else if (comp.type() === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
//grain cables should be treated as cables by default no matter what the address is
preset = pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE;
} // else if ("is a pressure cable"){}
}
return preset;
};
const toggleComponent = (device: number, component: Component, checked: boolean) => {
if (checked) {
let devObj = devMap.get(device);
let preset = determinePreset(component, devObj?.settings.product);
let preferences = pond.BinComponentPreferences.create({
type: preset,
node: component.settings.grainFilledTo ?? 0
});
binAPI.addComponent(bin, device, component.key(), preferences).then(resp => {
if (components && setComponents) {
if (components.set(component.key(), component)) {
let newComponents = new Map(components);
setComponents(newComponents);
}
}
//if a grain cable was added to the bin update the components grain type to match the bin
if (preferences.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) {
let settings = component.settings;
settings.grainType = binGrain;
settings.defaultMutations = [pond.Mutator.MUTATOR_EMC];
componentAPI
.update(device, settings)
.then(resp => {
snackbar.info("Component added to bin, and updated cables grain type");
})
.catch(err => {
snackbar.info("Component added to bin, but failed to update grain type on cable");
});
} else {
snackbar.info("Component added to bin");
}
});
} else {
binAPI.removeComponent(bin, component.key()).then(resp => {
if (components && setComponents) {
if (components.delete(component.key())) {
let newComponents = new Map(components);
setComponents(newComponents);
}
}
snackbar.info("Component removed from bin");
});
}
};
const removeAll = () => {
binAPI.removeAllComponents(bin).then(resp => {
if (setComponents) {
setComponents(new Map());
}
if (components && updateBinStatus) {
updateBinStatus(Array.from(components.keys()), true);
}
snackbar.info("All components removed from bin");
setRemoveAllDialog(false);
});
};
const deviceComponentsDisplay = (device: number) => {
if (deviceComponents.get(device) && deviceComponents.get(device)!.length < 1) {
return (
<Typography
variant="subtitle2"
style={{
margin: theme.spacing(2),
display: "flex",
justifyContent: "center"
}}>
No Sensors
</Typography>
);
}
return (
<React.Fragment>
{deviceComponents.get(device)?.map((comp, index) => {
let cIcon = GetComponentIcon(
comp.settings.type,
comp.settings.subtype,
theme.palette.mode
);
let share = comp.permissions.includes(pond.Permission.PERMISSION_SHARE);
return (
<React.Fragment key={index}>
<Tooltip
title={
!share
? "User or team does not have share permissions on this device"
: "Share component with bin"
}
key={index}>
<div>
<ListItemButton
disabled={!share}
onClick={() =>
toggleComponent(device, comp, components?.get(comp.key()) === undefined)
}>
{cIcon && (
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={comp.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
)}
<ListItemText inset={cIcon === undefined}>
<Grid container direction="row" justifyContent="space-between">
<Grid >{comp.name()}</Grid>
<Grid >
{components?.get(comp.key()) !== undefined ? (
<CheckBoxIcon />
) : (
<CheckBoxOutlineBlank />
)}
</Grid>
</Grid>
</ListItemText>
</ListItemButton>
</div>
</Tooltip>
<Divider />
</React.Fragment>
);
})}
</React.Fragment>
);
};
const wizardDialog = () => {
let device = devMap.get(selectedDevice);
let components = deviceComponents.get(selectedDevice);
if (device && components) {
return (
<ResponsiveDialog
open={wizardOpen}
onClose={() => {
setWizardOpen(false);
}}>
<DeviceWizard
device={device}
components={components}
refreshCallback={() => {
loadComponents(true);
}}
/>
</ResponsiveDialog>
);
}
return;
};
const componentsByDevice = () => {
return (
<React.Fragment>
<Typography variant="subtitle2" align="center" color="primary" gutterBottom>
Bin Sensors
</Typography>
<Box marginY={1}>
<Autocomplete
disablePortal
options={deviceOptions}
getOptionLabel={option => option.label || ""}
onChange={(_, newValue) => {
setSelectedDevice(newValue ? newValue.id : -1);
}}
renderInput={params => <TextField {...params} variant="outlined" label="Device" />}
loading={devicesLoading}
/>
{selectedDevice < 0 ? (
<Typography
variant="subtitle2"
style={{
margin: theme.spacing(3),
display: "flex",
justifyContent: "center"
}}>
Select a device
</Typography>
) : componentsLoading ? (
<CircularProgress />
) : (
<React.Fragment>
{setupAllowed && (
<Button
variant="contained"
color="primary"
onClick={() => {
setWizardOpen(true);
}}
style={{ marginTop: 15 }}>
Device Setup
</Button>
)}
<List
disablePadding
subheader={
deviceComponents?.get(selectedDevice) && (
<React.Fragment>
<ListSubheader component="div" disableGutters>
Choose Sensors
</ListSubheader>
<Divider />
</React.Fragment>
)
}>
{deviceComponentsDisplay(selectedDevice)}
</List>
</React.Fragment>
)}
</Box>
</React.Fragment>
);
};
const attachedComponents = () => {
if (components === undefined) return;
let comps = [...components.values()];
return (
<React.Fragment>
<Button
onClick={() => {
setRemoveAllDialog(true);
}}>
Remove All Components
</Button>
<List>
<ListSubheader component="div" disableGutters>
This Bin's Sensors
</ListSubheader>
<Divider />
{comps.length < 1 && (
<Typography
variant="subtitle2"
style={{
margin: theme.spacing(2),
display: "flex",
justifyContent: "center"
}}>
No Sensors
</Typography>
)}
{comps.map((component, index) => {
let cIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
theme.palette.mode
);
return (
<React.Fragment key={index}>
<ListItem>
<ListItemAvatar>
<Avatar
variant="square"
src={cIcon}
alt={component.name()}
style={{ width: theme.spacing(3), height: theme.spacing(3) }}
/>
</ListItemAvatar>
<ListItemText inset={cIcon === undefined}>{component.name()}</ListItemText>
<ListItemSecondaryAction>
<IconButton onClick={() => setComponentToRemove(component)}>
<Remove />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</React.Fragment>
);
})}
</List>
</React.Fragment>
);
};
return (
<React.Fragment>
<Tabs
value={activeStep}
onChange={(_, value) => setActiveStep(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="bin tabs"
classes={{ root: classes.tabSmall }}>
<Tab label={"Your Devices"} className={classes.tabSmall} />
<Tab label={"Attached"} className={classes.tabSmall} />
</Tabs>
<Box width="100%" marginTop={activeStep === 0 ? 2 : 1}>
{activeStep === 0 ? componentsByDevice() : attachedComponents()}
</Box>
{removeComponentConfirmation()}
{removeAllConfirmation()}
{wizardDialog()}
</React.Fragment>
);
}

145
src/bin/BinSensors.tsx Normal file
View file

@ -0,0 +1,145 @@
import {
AppBar,
Box,
Button,
DialogActions,
DialogTitle,
Grid2 as Grid,
IconButton,
Toolbar,
Typography
} from "@mui/material";
import { Close } from "@mui/icons-material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useMobile } from "hooks";
import { Bin, Component } from "models";
import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
import BinComponents from "./BinComponents";
type Mode = "add" | "update" | "remove";
interface Props {
open: boolean;
onClose: (refresh?: boolean) => void;
mode?: Mode;
bin?: Bin;
canEdit: boolean;
openedBinYard?: string;
userID: string;
coords?: { longitude: number; latitude: number };
binYards?: pond.BinYardSettings[];
components?: Map<string, Component>;
setComponents?: React.Dispatch<React.SetStateAction<Map<string, Component>>>;
updateBinStatus?: (componentKeys: string[], removed?: boolean) => void;
}
export default function BinSensors(props: Props) {
const isMobile = useMobile();
const {
open,
bin,
onClose,
mode,
openedBinYard,
components,
setComponents,
updateBinStatus
} = props;
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (open) {
setInitialized(true);
} else {
setInitialized(false);
}
}, [bin, open, mode, openedBinYard]);
const close = (refresh?: boolean) => {
onClose(refresh);
};
const titleText = () => {
return "Add Sensors to " + (bin ? bin.name() : "Bin");
};
const title = () => {
if (isMobile) {
return (
<AppBar position="relative">
<Toolbar>
<IconButton edge="start" color="inherit" onClick={() => close()} aria-label="close">
<Close />
</IconButton>
<Typography variant="h6" style={{ flex: 1 }}>
{titleText()}
</Typography>
</Toolbar>
</AppBar>
);
}
return (
<React.Fragment>
<DialogTitle>{titleText()}</DialogTitle>
</React.Fragment>
);
};
const componentsForm = () => {
if (bin) {
return (
<Box padding={1}>
<BinComponents
components={components ? components : undefined}
setComponents={setComponents}
bin={bin.key()}
binGrain={bin.grain()}
updateBinStatus={updateBinStatus}
/>
</Box>
);
}
return null;
};
const actions = () => {
return (
<DialogActions>
<Grid container justifyContent="space-between" direction="row">
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
{!isMobile && (
<React.Fragment>
<Button onClick={() => close()} aria-label="Cancel">
Done
</Button>
</React.Fragment>
)}
</Grid>
</Grid>
</DialogActions>
);
};
if (!open || !initialized) {
return null;
}
return (
<ResponsiveDialog
maxWidth={"xs"}
fullWidth
fullScreen={isMobile}
open={open}
scroll="paper"
onClose={() => {
onClose();
}}>
{title()}
{componentsForm()}
{actions()}
</ResponsiveDialog>
);
}

View file

@ -156,8 +156,6 @@ export default function BinTour() {
return steps;
};
console.log(user.status.finishedBinIntro)
// if this intro has been done, return null
if (user.status.finishedBinIntro.length > 1) return null;

267
src/device/DeviceWizard.tsx Normal file
View file

@ -0,0 +1,267 @@
import {
Avatar,
Box,
Card,
CardContent,
CardHeader,
List,
ListItem,
ListItemIcon,
ListItemText,
ListSubheader,
Menu,
MenuItem,
Theme,
Tooltip,
Typography
} from "@mui/material";
import ComponentSettings from "component/ComponentSettings";
import { Component, Device } from "models";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import {
DeviceAvailabilityMap,
FindAvailablePositions,
OffsetAvailabilityMap
} from "pbHelpers/DeviceAvailability";
import { GetDeviceProductIcon, GetDeviceProductLabel } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useComponentAPI, useSnackbar } from "providers";
import React, { useState } from "react";
import DeviceSVG, { PortInformation } from "./deviceSVG";
import { makeStyles } from "@mui/styles";
interface Props {
device: Device;
//permissions: pond.Permission[];
components: Component[];
refreshCallback: () => void;
}
const useStyles = makeStyles((theme: Theme) => {
return ({
card: {
// position: "relative",
// display: "flex",
// height: "100%",
// flexDirection: "column",
// overflow: "visible"
minHeight: "200px"
},
cardHeader: {
padding: theme.spacing(1),
paddingLeft: theme.spacing(2)
},
cardContent: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
padding: theme.spacing(1),
paddingTop: 0
},
avatarIcon: {
width: theme.spacing(3),
height: theme.spacing(3)
}
})
});
export default function DeviceWizard(props: Props) {
const { device, components, refreshCallback } = props;
const [componentDialogOpen, setComponentDialogOpen] = useState(false);
const [availableOffsets, setAvailableOffsets] = useState<OffsetAvailabilityMap>(new Map());
const [availablePositions, setAvailablePositions] = useState<DeviceAvailabilityMap>(new Map());
const compAPI = useComponentAPI();
const [menuAnchor, setMenuAnchor] = useState<null | Element>(null);
const [portComponents, setPortComponents] = useState<Component[]>([]);
const [componentToUpdate, setComponentToUpdate] = useState<Component | undefined>();
const { error, success } = useSnackbar();
const [ComponentSettingsMode, setComponentSettingsMode] = useState<"add" | "remove" | "update">(
"add"
);
const classes = useStyles();
const [selectedPort, setSelectedPort] = useState<PortInformation>();
//const [addressType, setAddressType] = useState<quack.AddressType>(quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY);
const wizardMenu = () => {
return (
<Menu
id="groupMenu"
anchorEl={menuAnchor}
open={Boolean(menuAnchor)}
onClose={() => setMenuAnchor(null)}
keepMounted
disableAutoFocusItem>
<MenuItem
key={"add"}
onClick={() => {
setComponentSettingsMode("add");
setComponentToUpdate(undefined);
setComponentDialogOpen(true);
}}>
Add Component
</MenuItem>
{/* <MenuItem key={"list_error"} onClick={() => {
}}>
List Errors
</MenuItem> */}
{selectedPort && selectedPort.autoDetectable && (
<Tooltip title="Used to Detect Grain Cables">
<MenuItem
key={"auto"}
onClick={() => {
setAutoDetect();
}}>
Auto Detect
</MenuItem>
</Tooltip>
)}
{portComponents.length > 0 && (
<List>
<ListSubheader>Current Components</ListSubheader>
{portComponents.map(comp => (
<ListItem
key={comp.key()}
onClick={() => {
setComponentToUpdate(comp);
setComponentSettingsMode("update");
setComponentDialogOpen(true);
}}>
<ListItemIcon>
<Avatar
className={classes.avatarIcon}
alt={comp.subTypeName() + "Icon"}
src={GetComponentIcon(comp.type(), comp.subType())}
/>
</ListItemIcon>
<ListItemText>{comp.name()}</ListItemText>
</ListItem>
))}
</List>
)}
</Menu>
);
};
/**
* sets an autodetect component on the port that was selected
* TODO: when the auto detect gets expanded out of being a grain cable component this will have to be re-factored to use the new component type since it is hard coded
*/
const setAutoDetect = () => {
if (selectedPort) {
let componentSettings = pond.ComponentSettings.create();
componentSettings.type = quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE; // use the grain cable component
componentSettings.subtype = quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_DIAG; //use the port diagnostic subtype
componentSettings.addressType = quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY;
componentSettings.address = selectedPort.address; // the address of the selected port
componentSettings.measurementPeriodMs = 600000; //set the measurement period to 10 mins
componentSettings.reportPeriodMs = 600000;
componentSettings.name = "Port " + selectedPort.label + " Auto Detect";
compAPI
.add(device.id(), componentSettings)
.then(resp => {
success("Added Auto Detect Component to Port");
})
.catch(err => {
error("Failed to Add Auto Detect Component to Port");
})
.finally(() => {
refreshCallback();
});
}
};
/**
* function that restricts the pin availabilty in the availability map to the pin of the port that was selected
* does nothing for I2C ports as the restrictions for that are handled by the component type
* @param port port that was selected
* @param availability availability map of the device
* @returns modified availability map
*/
const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => {
let currentAvailability = availability;
switch (port.addressType) {
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
currentAvailability.set(port.addressType, [{ address: port.address, label: port.label }]);
}
return currentAvailability;
};
const findPortComponents = (components: Component[], port: PortInformation) => {
let pc: Component[] = [];
switch (port.addressType) {
case quack.AddressType.ADDRESS_TYPE_I2C:
components.forEach(comp => {
if (comp.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C) {
pc.push(comp);
}
});
break;
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
components.forEach(comp => {
if (comp.settings.address === port.address) {
pc.push(comp);
}
});
}
return pc;
};
const setupCard = () => {
return (
<Card className={classes.card} raised>
<CardHeader
avatar={
<Avatar
variant="square"
src={GetDeviceProductIcon(device.settings)}
className={classes.avatarIcon}
alt={"devIcon"}
/>
}
title={GetDeviceProductLabel(device.settings.product) + " - ID:" + device.id()}
className={classes.cardHeader}
/>
<CardContent style={{ paddingTop: 0 }}>
<Typography variant="caption">
Select a port on the device below to modify components
</Typography>
<Box paddingLeft={"15%"} paddingRight={"15%"} paddingTop={"20px"}>
<DeviceSVG
device={device}
openMenu={(e, port) => {
let available = FindAvailablePositions(components, device.settings.product);
setAvailableOffsets(available.offsetAvailability);
setAvailablePositions(adjustAvailablePositions(port, available.availability));
//setAddressType(port.addressType);
setSelectedPort(port);
setPortComponents(findPortComponents(components, port));
setMenuAnchor(e.currentTarget);
}}
/>
</Box>
</CardContent>
</Card>
);
};
return (
<React.Fragment>
{setupCard()}
<ComponentSettings
mode={ComponentSettingsMode}
component={componentToUpdate}
device={device}
isDialogOpen={componentDialogOpen}
closeDialogCallback={() => setComponentDialogOpen(false)}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={refreshCallback}
canEdit
addressTypeRestriction={selectedPort && selectedPort.addressType}
/>
{wizardMenu()}
</React.Fragment>
);
}

5246
src/device/deviceSVG.tsx Normal file

File diff suppressed because one or more lines are too long

139
src/models/Headspace.ts Normal file
View file

@ -0,0 +1,139 @@
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/pond";
import { or } from "utils/types";
import { cloneDeep } from "lodash";
import { Component } from "models";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
export class Headspace {
public settings: pond.ComponentSettings = pond.ComponentSettings.create();
public status: pond.ComponentStatus = pond.ComponentStatus.create();
public temperature: number = -127;
public humidity: number = 200;
public static create(comp: Component): Headspace {
let my = new Headspace();
my.settings = comp.settings;
my.status = comp.status;
if (comp.status.measurement.length > 0) {
comp.status.measurement.forEach(um => {
if (um.values[0]) {
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
my.temperature = um.values[0].values[0];
}
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
my.humidity = um.values[0].values[0];
}
}
});
} else {
let temp = comp.status?.lastMeasurement?.measurement?.humidityAndTemperature?.celciusTimes_10;
let hum =
comp.status?.lastMeasurement?.measurement?.humidityAndTemperature
?.relativeHumidityTimes_100;
if (temp === undefined) {
if (comp.status?.lastMeasurement?.measurement?.humidityAndTemperature) {
let humAndTemp = comp.status?.lastMeasurement?.measurement?.humidityAndTemperature as any;
temp = humAndTemp["celciusTimes10"];
hum = humAndTemp["relativeHumidityTimes100"];
}
}
my.temperature = or(temp, -1270) / 10;
my.humidity = or(hum, 20000) / 100;
}
return my;
}
public static createPond(comp: pond.Component): Headspace {
let my = new Headspace();
my.settings = comp.settings ? comp.settings : pond.ComponentSettings.create();
my.status = comp.status ? comp.status : pond.ComponentStatus.create();
let temp = comp.status?.lastMeasurement?.measurement?.humidityAndTemperature?.celciusTimes_10;
let hum =
comp.status?.lastMeasurement?.measurement?.humidityAndTemperature?.relativeHumidityTimes_100;
if (temp === undefined) {
if (comp.status?.lastMeasurement?.measurement?.humidityAndTemperature) {
let humAndTemp = comp.status?.lastMeasurement?.measurement?.humidityAndTemperature as any;
temp = humAndTemp["celciusTimes10"];
hum = humAndTemp["relativeHumidityTimes100"];
}
}
my.temperature = or(temp, -1270) / 10;
my.humidity = or(hum, 20000) / 100;
return my;
}
public static any(data: any): Headspace {
let comp = pond.Component.fromObject(cloneDeep(data));
let my = Headspace.createPond(comp);
if (data && data.status && data.status.lastMeasurement) {
my.status.lastMeasurement = data.status.lastMeasurement;
}
return my;
}
public update(other: Headspace) {
this.settings = other.settings;
this.status = other.status;
}
public static humidColour() {
return describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
quack.ComponentType.COMPONENT_TYPE_DHT
).colour();
}
public static tempColour() {
return describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE).colour();
}
public name(): string {
return this.settings.name !== "" ? this.settings.name : "Component " + this.key();
}
public key(): string {
return this.settings.key;
}
public location(): quack.ComponentID {
return quack.ComponentID.fromObject({
type: this.settings.type,
addressType: this.settings.addressType,
address: this.settings.address
});
}
public locationString(): string {
return (
or(this.settings.type, 0).toString() +
"-" +
or(this.settings.addressType, 0).toString() +
"-" +
or(this.settings.address, 0).toString()
);
}
public type(): quack.ComponentType {
return this.settings.type;
}
public subType(): number {
return this.settings.subtype;
}
public getTempString(unit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS): string {
if (this.temperature < -126) return "NaN";
if (unit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT)
return (this.temperature * (9 / 5) + 32).toFixed(2) + "°F";
return this.temperature.toFixed(2) + "°C";
}
public getHumidityString(): string {
if (this.humidity > 199) return "NaN";
return this.humidity.toFixed(2) + "%";
}
}

View file

@ -49,7 +49,7 @@ import Chat from "chat/Chat";
import TasksIcon from "products/Construction/TasksIcon";
// import TaskViewer from "tasks/TaskViewer";
import FieldsIcon from "products/AgIcons/FieldsIcon";
// import BinComponentTypes from "bin/BinComponentTypes";
import BinComponentTypes from "bin/BinComponentTypes";
import { Plenum } from "models/Plenum";
import { GrainCable } from "models/GrainCable";
// import { Controller } from "models/Controller";
@ -218,7 +218,7 @@ export default function Bin(props: Props) {
setValue(newValue);
};
const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({
const StyledToggle = styled(ToggleButton)(() => ({
root: {
backgroundColor: "transparent",
overflow: "visible",
@ -297,11 +297,11 @@ export default function Bin(props: Props) {
let attachedDevIds: number[] = [];
if (resp.data.componentDevices)
Object.keys(resp.data.componentDevices).forEach(k => {
// newComponentDevices.set(k, parseInt(resp.data.componentDevices[k]));
// //make a list of all of the ids for devices that components are currently attached
// if (!attachedDevIds.includes(parseInt(resp.data.componentDevices[k]))) {
// attachedDevIds.push(parseInt(resp.data.componentDevices[k]));
// }
newComponentDevices.set(k, resp.data.componentDevices[k]);
//make a list of all of the ids for devices that components are currently attached
if (!attachedDevIds.includes(resp.data.componentDevices[k])) {
attachedDevIds.push(resp.data.componentDevices[k]);
}
});
setComponentDevices(newComponentDevices);
@ -329,12 +329,12 @@ export default function Bin(props: Props) {
}
if (resp.data.components) {
let compMap: Map<string, Component> = new Map();
// resp.data.components.forEach((comp: Component, index: number, arr: Component[]) => {
// if (comp && comp.settings) {
// let c = Component.any(comp);
// compMap.set(comp.settings.key, c);
// }
// });
resp.data.components.forEach((comp: pond.Component) => {
if (comp && comp.settings) {
let c = Component.any(comp);
compMap.set(comp.settings.key, c);
}
});
setComponents(compMap);
}
if (resp.data.presets) {
@ -803,14 +803,14 @@ export default function Bin(props: Props) {
<Typography style={{ fontWeight: 650 }}>Bin Components</Typography>
</AccordionSummary>
<AccordionDetails>
{/* <BinComponentTypes
<BinComponentTypes
bin={bin.key()}
updateBinStatus={updateStatus}
components={components}
setComponents={setComponents}
preferences={preferences}
setPreferences={setPreferences}
/> */}
/>
</AccordionDetails>
</Accordion>
</Card>