bin sensors add the bins
This commit is contained in:
parent
c46cb5ce4b
commit
b5071dfb40
9 changed files with 7346 additions and 20 deletions
|
|
@ -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}
|
||||
|
|
|
|||
926
src/bin/BinComponentTypes.tsx
Normal file
926
src/bin/BinComponentTypes.tsx
Normal 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
605
src/bin/BinComponents.tsx
Normal 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
145
src/bin/BinSensors.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
267
src/device/DeviceWizard.tsx
Normal 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
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
139
src/models/Headspace.ts
Normal 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) + "%";
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue