set up the heater page and its subsequent imports
This commit is contained in:
parent
47d50194c5
commit
b573e10707
5 changed files with 1388 additions and 9 deletions
|
|
@ -16,6 +16,7 @@ import ConstructionSiteMap from "pages/ConstructionSiteMap";
|
|||
import Sites from "pages/Sites";
|
||||
import Site from "pages/Site";
|
||||
import Heaters from "pages/Heaters";
|
||||
import Heater from "pages/Heater";
|
||||
//import Site from "pages/Site";
|
||||
|
||||
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
||||
|
|
@ -215,14 +216,14 @@ export default function Router(props: Props) {
|
|||
path="" // "/settings/basic"
|
||||
element={<Heaters key={"Heaters page"} />}
|
||||
/>
|
||||
{/* <Route
|
||||
<Route
|
||||
path="/:heaterKey" // "/settings/basic"
|
||||
element={<Site />}
|
||||
element={<Heater />}
|
||||
/>
|
||||
<Route
|
||||
path="/:heaterKey/*" // "/settings/basic"
|
||||
element={<RelativeRoutes />}
|
||||
/> */}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
210
src/objectHeater/ObjectHeaterActions.tsx
Normal file
210
src/objectHeater/ObjectHeaterActions.tsx
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import {
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
import SettingsIcon from "@mui/icons-material/Settings";
|
||||
import MoreIcon from "@mui/icons-material/MoreVert";
|
||||
import React, { useState } from "react";
|
||||
import ObjectHeaterSettings from "./ObjectHeaterSettings";
|
||||
import { ObjectHeater } from "models/ObjectHeater";
|
||||
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
|
||||
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
|
||||
import ObjectUsers from "user/ObjectUsers";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { Component, Scope } from "models";
|
||||
import ObjectTeams from "teams/ObjectTeams";
|
||||
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
|
||||
import ShareObject from "user/ShareObject";
|
||||
import { blue } from "@mui/material/colors";
|
||||
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
|
||||
import { Share } from "@mui/icons-material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
shareIcon: {
|
||||
color: blue["500"],
|
||||
"&:hover": {
|
||||
color: blue["600"]
|
||||
}
|
||||
},
|
||||
removeIcon: {
|
||||
color: "var(--status-alert)"
|
||||
},
|
||||
red: {
|
||||
color: "var(--status-alert)"
|
||||
},
|
||||
blueIcon: {
|
||||
color: blue["500"],
|
||||
"&:hover": {
|
||||
color: blue["600"]
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
interface OpenState {
|
||||
users: boolean;
|
||||
teams: boolean;
|
||||
settings: boolean;
|
||||
removeSelf: boolean;
|
||||
share: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
heater: ObjectHeater;
|
||||
componentsByDevice?: Map<string, Component[]>;
|
||||
refreshCallback: () => void;
|
||||
permissions: pond.Permission[];
|
||||
immediateUpdate: (updatedHeater: ObjectHeater) => void;
|
||||
}
|
||||
|
||||
export default function ObjectHeaterActions(props: Props) {
|
||||
const classes = useStyles();
|
||||
const { heater, refreshCallback, permissions, componentsByDevice, immediateUpdate } = props;
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const [openState, setOpenState] = useState<OpenState>({
|
||||
users: false,
|
||||
teams: false,
|
||||
settings: false,
|
||||
removeSelf: false,
|
||||
share: false
|
||||
});
|
||||
|
||||
const groupMenu = () => {
|
||||
return (
|
||||
<Menu
|
||||
id="menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
keepMounted
|
||||
disableAutoFocusItem>
|
||||
{permissions.includes(pond.Permission.PERMISSION_SHARE) && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, share: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
dense>
|
||||
<ListItemIcon>
|
||||
<Share className={classes.blueIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Share" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, users: true });
|
||||
setAnchorEl(null);
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<ObjectUsersIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Users" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, teams: true });
|
||||
setAnchorEl(null);
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<ObjectTeamsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Teams" />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, removeSelf: true });
|
||||
setAnchorEl(null);
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<RemoveSelfIcon className={classes.red} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Leave" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogs = () => {
|
||||
const key = heater.key;
|
||||
const label = heater.name;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ObjectHeaterSettings
|
||||
componentsByDevice={componentsByDevice}
|
||||
heater={heater}
|
||||
open={openState.settings}
|
||||
close={newHeater => {
|
||||
if (newHeater) {
|
||||
immediateUpdate(newHeater);
|
||||
}
|
||||
setOpenState({ ...openState, settings: false });
|
||||
}}
|
||||
/>
|
||||
<ShareObject
|
||||
scope={{ kind: "objectHeater", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.share}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
|
||||
/>
|
||||
<ObjectUsers
|
||||
scope={{ kind: "objectHeater", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.users}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<ObjectTeams
|
||||
scope={{ kind: "objectHeater", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.teams}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<RemoveSelfFromObject
|
||||
scope={{ kind: "objectHeater", key: key } as Scope}
|
||||
path={"heaters"}
|
||||
label={label}
|
||||
isDialogOpen={openState.removeSelf}
|
||||
closeDialogCallback={() => {
|
||||
setOpenState({ ...openState, removeSelf: false });
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
||||
<Tooltip title="Settings">
|
||||
<IconButton onClick={() => setOpenState({ ...openState, settings: true })}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton
|
||||
aria-owns={anchorEl ? "groupMenu" : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
{dialogs()}
|
||||
{groupMenu()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
621
src/objectHeater/ObjectHeaterCharts.tsx
Normal file
621
src/objectHeater/ObjectHeaterCharts.tsx
Normal file
|
|
@ -0,0 +1,621 @@
|
|||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Grid2 as Grid,
|
||||
Theme,
|
||||
Typography,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import { useDeviceAPI, useGlobalState, useMutationAPI } from "providers";
|
||||
import { Component, Device } from "models";
|
||||
import moment from "moment";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import { GetDefaultDateRange } from "common/time/DateRange";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import MultiLineGraph, { LineData } from "charts/measurementCharts/MultiLineGraph";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import DateSelect from "common/time/DateSelect";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
ReferenceArea,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
TooltipProps,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from "recharts";
|
||||
import FuelDark from "assets/components/fuelDark.png";
|
||||
import FuelLight from "assets/components/fuelLight.png";
|
||||
import { useThemeType } from "hooks";
|
||||
import { GetComponentIcon } from "pbHelpers/ComponentType";
|
||||
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
import MaterialChartTooltip from "charts/MaterialChartTooltip";
|
||||
import { roundTo } from "utils";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
//import TimeBar from "common/time/TimeBar";
|
||||
|
||||
interface Props {
|
||||
device: pond.ComprehensiveDevice;
|
||||
linearMutations: pond.LinearMutation[];
|
||||
xDomain: string[] | number[];
|
||||
changeXDomain: (domain: string[] | number[]) => void;
|
||||
getFuelLevel?: (level: string) => void;
|
||||
}
|
||||
|
||||
interface GraphData {
|
||||
time: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface MutationData {
|
||||
mutation: pond.LinearMutation;
|
||||
data: pond.ValueAt[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
avatarIcon: {
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3)
|
||||
},
|
||||
card: {
|
||||
height: 550
|
||||
},
|
||||
cardHeader: {
|
||||
height: "20%"
|
||||
},
|
||||
cardContent: {
|
||||
height: "80%"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export default function ObjectHeaterCharts(props: Props) {
|
||||
const { device, linearMutations, getFuelLevel, xDomain, changeXDomain } = props;
|
||||
const deviceAPI = useDeviceAPI();
|
||||
const mutationAPI = useMutationAPI();
|
||||
const [tempHum, setTempHum] = useState<Component>();
|
||||
const [ambientTemps, setAmbientTemps] = useState<LineData[]>([]);
|
||||
const [heaterTemps, setHeaterTemps] = useState<pond.ExportedUnitMeasurement[]>([]);
|
||||
const [heaterState, setHeaterState] = useState<pond.ExportedUnitMeasurement[]>([]);
|
||||
const [tempOnly, setTempOnly] = useState<Component>();
|
||||
const [heater, setHeater] = useState<Component>();
|
||||
const [start, setStart] = useState(GetDefaultDateRange().start);
|
||||
const [end, setEnd] = useState(GetDefaultDateRange().end);
|
||||
const now = moment();
|
||||
const theme = useTheme();
|
||||
const [mutationData, setMutationData] = useState<MutationData[]>([]);
|
||||
const [{ user }] = useGlobalState();
|
||||
const themeType = useThemeType();
|
||||
const classes = useStyles();
|
||||
const [mutationsLoading, setMutationsLoading] = useState(false);
|
||||
const [refLeft, setRefLeft] = useState<number | undefined>();
|
||||
const [refRight, setRefRight] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!mutationsLoading) {
|
||||
setMutationsLoading(true);
|
||||
//make call to the backend to get measurements for the mutation
|
||||
let md: MutationData[] = [];
|
||||
linearMutations.forEach(mut => {
|
||||
mutationAPI.linearMutation(mut, start.toISOString(), end.toISOString()).then(resp => {
|
||||
let d: pond.ValueAt[] = resp.data.values;
|
||||
if (mut.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
||||
d = [];
|
||||
resp.data.values.forEach(valAt => {
|
||||
d.push(
|
||||
pond.ValueAt.create({
|
||||
time: valAt.time,
|
||||
value: valAt.value / 10
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
md.push({
|
||||
mutation: mut,
|
||||
data: d
|
||||
});
|
||||
if (md.length >= linearMutations.length) {
|
||||
setMutationsLoading(false);
|
||||
}
|
||||
setMutationData([...md]);
|
||||
if (getFuelLevel && mut.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
||||
if (resp.data.values.length > 0) {
|
||||
getFuelLevel(
|
||||
(resp.data.values[resp.data.values.length - 1].value / 10).toFixed(2) + "%"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [linearMutations, start, end, getFuelLevel, mutationAPI]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
let tempHum: Component | undefined = undefined;
|
||||
let tempOnly: Component | undefined = undefined;
|
||||
let heater: Component | undefined = undefined;
|
||||
|
||||
let dev = Device.any(device.device);
|
||||
let components = device.components;
|
||||
let measurementsFor: Component[] = [];
|
||||
components.forEach(comp => {
|
||||
let c = Component.any(comp);
|
||||
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_DHT) {
|
||||
tempHum = c;
|
||||
measurementsFor.push(c);
|
||||
}
|
||||
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_TEMPERATURE) {
|
||||
tempOnly = c;
|
||||
measurementsFor.push(c);
|
||||
}
|
||||
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT) {
|
||||
if (c.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER) {
|
||||
heater = c;
|
||||
measurementsFor.push(c);
|
||||
}
|
||||
}
|
||||
});
|
||||
setTempHum(tempHum);
|
||||
setTempOnly(tempOnly);
|
||||
setHeater(heater);
|
||||
deviceAPI
|
||||
.listJSONMeasurements(dev.id(), measurementsFor, start, end, 0, 0, "asc", "timestamp")
|
||||
.then(resp => {
|
||||
resp.data.components.forEach(comp => {
|
||||
if (comp.componentId === tempHum?.key()) {
|
||||
if (comp.measurementType === "Temperature") {
|
||||
let lineData: LineData[] = [];
|
||||
comp.exportedMeasurements.forEach(m => {
|
||||
lineData.push({
|
||||
timestamp: moment(m.timestamp).valueOf(),
|
||||
points: m.measurementValue.map((t, i) => ({ value: convertTemp(t), node: i }))
|
||||
});
|
||||
});
|
||||
setAmbientTemps(lineData);
|
||||
}
|
||||
}
|
||||
if (comp.componentId === tempOnly?.key()) {
|
||||
if (comp.measurementType === "Temperature") {
|
||||
setHeaterTemps(comp.exportedMeasurements);
|
||||
}
|
||||
}
|
||||
if (comp.componentId === heater?.key()) {
|
||||
if (comp.measurementType === "State") {
|
||||
setHeaterState(comp.exportedMeasurements);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [device, deviceAPI, start, end]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const updateDateRange = (newStartDate: any, newEndDate: any, live: boolean) => {
|
||||
setStart(newStartDate);
|
||||
setEnd(newEndDate);
|
||||
// setLive(live);
|
||||
// if (props.updateDate) {
|
||||
// props.updateDate(newStartDate, newEndDate, live);
|
||||
// }
|
||||
};
|
||||
|
||||
const convertTemp = (temp: number) => {
|
||||
let t = temp;
|
||||
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
t = temp * 1.8 + 32;
|
||||
}
|
||||
return t;
|
||||
};
|
||||
|
||||
//had to make custom chart for this in order to show the heater state and the temp of the ds18
|
||||
const heaterStateWithTemps = (tempComponent: Component, heaterComponent: Component) => {
|
||||
let tempDescriber = describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
tempComponent.type(),
|
||||
tempComponent.subType()
|
||||
);
|
||||
let heaterDescriber = describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN,
|
||||
heaterComponent.type(),
|
||||
heaterComponent.subType()
|
||||
);
|
||||
let tempData: LineData[] = [];
|
||||
heaterTemps.forEach(ht => {
|
||||
tempData.push({
|
||||
timestamp: moment(ht.timestamp).valueOf(),
|
||||
points: ht.measurementValue.map((t, i) => ({ value: convertTemp(t), node: i }))
|
||||
});
|
||||
});
|
||||
let stateData: LineData[] = [];
|
||||
heaterState.forEach(hs => {
|
||||
stateData.push({
|
||||
timestamp: moment(hs.timestamp).valueOf(),
|
||||
points: hs.measurementValue.map((s, i) => ({ value: s, node: i }))
|
||||
});
|
||||
});
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MultiLineGraph
|
||||
describer={tempDescriber}
|
||||
lineData={tempData}
|
||||
numLines={1}
|
||||
customHeight={"70%"}
|
||||
tooltip
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
changeXDomain(domain);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
<MultiLineGraph
|
||||
describer={heaterDescriber}
|
||||
lineData={stateData}
|
||||
numLines={1}
|
||||
customHeight={"30%"}
|
||||
tooltip
|
||||
yMin={0}
|
||||
yMax={1}
|
||||
hideY
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
changeXDomain(domain);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
//// TODO-CS: Make a mutator describer to put the get functions into ////
|
||||
const getMutationIcon = (mutation: pond.Mutator, themeType: string) => {
|
||||
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
||||
return themeType === "light" ? FuelDark : FuelLight;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const getMutationLabel = (mutation: pond.Mutator) => {
|
||||
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
||||
return "Fuel Level";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const getMutationColour = (mutation: pond.Mutator) => {
|
||||
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
||||
return "lightBlue";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const getMutationUnit = (mutation: pond.Mutator) => {
|
||||
if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) {
|
||||
return "%";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
//// /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ ////
|
||||
|
||||
const zoom = () => {
|
||||
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
|
||||
if (refLeft && refRight && refLeft !== refRight) {
|
||||
refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]);
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
changeXDomain(newDomain);
|
||||
}
|
||||
};
|
||||
|
||||
const mutationGraphCard = (mutation: pond.LinearMutation, data: pond.ValueAt[], key: string) => {
|
||||
let graphData: GraphData[] = [];
|
||||
let lastReading: pond.ValueAt | undefined;
|
||||
data.forEach(d => {
|
||||
graphData.push({
|
||||
time: moment(d.time).valueOf(),
|
||||
value: d.value
|
||||
});
|
||||
lastReading = d;
|
||||
});
|
||||
return (
|
||||
<Grid size={{
|
||||
xs:12,
|
||||
sm:12,
|
||||
md:4
|
||||
}}
|
||||
key={key}>
|
||||
<Card className={classes.card}>
|
||||
{graphData.length > 0 ? (
|
||||
<React.Fragment>
|
||||
<CardHeader
|
||||
className={classes.cardHeader}
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="square"
|
||||
src={getMutationIcon(mutation.mutation, themeType)}
|
||||
className={classes.avatarIcon}
|
||||
alt={"mutIcon"}
|
||||
/>
|
||||
}
|
||||
title={mutation.mutationName}
|
||||
titleTypographyProps={{ variant: "subtitle1" }}
|
||||
subheader={
|
||||
<Grid container>
|
||||
<Grid size={12}>
|
||||
<Typography variant="caption" color="textPrimary">
|
||||
<span>
|
||||
{getMutationLabel(mutation.mutation) + ": "}{" "}
|
||||
<span style={{ color: getMutationColour(mutation.mutation) }}>
|
||||
{lastReading?.value.toFixed(2) + getMutationUnit(mutation.mutation)}
|
||||
</span>{" "}
|
||||
<br />
|
||||
</span>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Typography color="textSecondary" variant="caption">
|
||||
{moment(lastReading?.time).fromNow()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
/>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<ResponsiveContainer width={"100%"} height={"100%"}>
|
||||
<AreaChart
|
||||
data={graphData}
|
||||
onMouseDown={(e: any) => {
|
||||
if (e) {
|
||||
setRefLeft(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(e: any) => {
|
||||
if (e) {
|
||||
setRefRight(e.activeLabel);
|
||||
}
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
setRefLeft(undefined);
|
||||
setRefRight(undefined);
|
||||
zoom();
|
||||
}}>
|
||||
<Tooltip
|
||||
animationEasing="ease-out"
|
||||
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
|
||||
labelFormatter={timestamp => moment(timestamp).format("lll")}
|
||||
content={(props: TooltipProps<any, any>) => {
|
||||
return (
|
||||
<MaterialChartTooltip
|
||||
{...props}
|
||||
valueFormatter={value =>
|
||||
roundTo(parseFloat(String(value)), 2).toString()
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
allowDataOverflow
|
||||
domain={xDomain}
|
||||
name="Time"
|
||||
tickFormatter={timestamp => {
|
||||
let t = moment(timestamp);
|
||||
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
|
||||
}}
|
||||
scale="time"
|
||||
type="number"
|
||||
tick={{ fill: theme.palette.text.primary }}
|
||||
stroke={theme.palette.divider}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
domain={
|
||||
mutation.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL
|
||||
? [0, 100]
|
||||
: ["auto", "auto"]
|
||||
}
|
||||
label={{
|
||||
value: mutation.mutationName,
|
||||
position: "insideLeft",
|
||||
angle: -90,
|
||||
fill: theme.palette.text.primary
|
||||
}}
|
||||
tick={{ fill: theme.palette.text.primary }}
|
||||
/>
|
||||
<Area dataKey={"value"} />
|
||||
{refLeft && refRight ? (
|
||||
<ReferenceArea x1={refLeft} x2={refRight} strokeOpacity={0.3} />
|
||||
) : null}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height="100%">
|
||||
<Typography style={{ fontWeight: 650 }}>
|
||||
No measurements for selected timeframe
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const mutationGraphs = () => {
|
||||
let graphCards: JSX.Element[] = [];
|
||||
if (mutationData.length === 0) {
|
||||
return (
|
||||
<Grid size={{
|
||||
xs:12,
|
||||
sm:12,
|
||||
md:4
|
||||
}}>
|
||||
<Card className={classes.card}>
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height={"100%"}>
|
||||
<Typography style={{ fontWeight: 650 }}>No Calculated Measurements Set</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
mutationData.forEach((mutation, i) => {
|
||||
graphCards.push(mutationGraphCard(mutation.mutation, mutation.data, "mutation" + i));
|
||||
});
|
||||
return graphCards;
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* <TimeBar updateDateRange={updateDateRange} startDate={start} endDate={end}/> */}
|
||||
<Grid container direction="row" spacing={1}>
|
||||
<Grid size={{
|
||||
xs:12,
|
||||
sm:12,
|
||||
md:4
|
||||
}}>
|
||||
{tempOnly && heater && (
|
||||
<Card className={classes.card}>
|
||||
{heaterTemps.length > 0 ?? heaterState.length > 0 ? (
|
||||
<React.Fragment>
|
||||
<Grid container className={classes.cardHeader}>
|
||||
{tempOnly && (
|
||||
<Grid size={6}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="square"
|
||||
src={GetComponentIcon(tempOnly.type(), tempOnly.subType(), themeType)}
|
||||
className={classes.avatarIcon}
|
||||
alt={"tempIcon"}
|
||||
/>
|
||||
}
|
||||
title={tempOnly.name()}
|
||||
subheader={
|
||||
<UnitMeasurementSummary
|
||||
component={tempOnly}
|
||||
reading={UnitMeasurement.convertLastMeasurement(
|
||||
tempOnly.lastMeasurement.map(um => UnitMeasurement.any(um, user))
|
||||
)}
|
||||
/>
|
||||
}
|
||||
titleTypographyProps={{ variant: "subtitle1" }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{heater && (
|
||||
<Grid size={6}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="square"
|
||||
src={GetComponentIcon(heater.type(), heater.subType(), themeType)}
|
||||
className={classes.avatarIcon}
|
||||
alt={"tempIcon"}
|
||||
/>
|
||||
}
|
||||
title={heater.name()}
|
||||
subheader={
|
||||
<UnitMeasurementSummary
|
||||
component={heater}
|
||||
reading={UnitMeasurement.convertLastMeasurement(
|
||||
heater.lastMeasurement.map(um => UnitMeasurement.create(um, user))
|
||||
)}
|
||||
/>
|
||||
}
|
||||
titleTypographyProps={{ variant: "subtitle1" }}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<CardContent className={classes.cardContent}>
|
||||
{heaterStateWithTemps(tempOnly, heater)}
|
||||
</CardContent>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height={400}>
|
||||
<Typography style={{ fontWeight: 650 }}>
|
||||
No measurements for selected timeframe
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid size={{
|
||||
xs:12,
|
||||
sm:12,
|
||||
md:4
|
||||
}}>
|
||||
{tempHum && (
|
||||
<Card className={classes.card}>
|
||||
{ambientTemps.length > 0 ? (
|
||||
<React.Fragment>
|
||||
<CardHeader
|
||||
className={classes.cardHeader}
|
||||
avatar={
|
||||
<Avatar
|
||||
variant="square"
|
||||
src={GetComponentIcon(tempHum.type(), tempHum.subType(), themeType)}
|
||||
className={classes.avatarIcon}
|
||||
alt={"tempIcon"}
|
||||
/>
|
||||
}
|
||||
title={tempHum.name()}
|
||||
subheader={
|
||||
<UnitMeasurementSummary
|
||||
component={tempHum}
|
||||
reading={UnitMeasurement.convertLastMeasurement(
|
||||
tempHum.lastMeasurement
|
||||
.filter(
|
||||
um => um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE
|
||||
)
|
||||
.map(um => UnitMeasurement.any(um, user))
|
||||
)}
|
||||
/>
|
||||
}
|
||||
titleTypographyProps={{ variant: "subtitle1" }}
|
||||
/>
|
||||
<CardContent className={classes.cardContent}>
|
||||
<MultiLineGraph
|
||||
describer={describeMeasurement(
|
||||
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
||||
tempHum.type(),
|
||||
tempHum.subType()
|
||||
)}
|
||||
lineData={ambientTemps}
|
||||
numLines={1}
|
||||
customHeight={"100%"}
|
||||
tooltip
|
||||
newXDomain={xDomain}
|
||||
multiGraphZoom={domain => {
|
||||
changeXDomain(domain);
|
||||
}}
|
||||
multiGraphZoomOut
|
||||
/>
|
||||
</CardContent>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height={400}>
|
||||
<Typography style={{ fontWeight: 650 }}>
|
||||
No measurements for selected timeframe
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Grid>
|
||||
{mutationGraphs()}
|
||||
</Grid>
|
||||
|
||||
<DateSelect updateDateRange={updateDateRange} startDate={start} endDate={end} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
534
src/pages/Heater.tsx
Normal file
534
src/pages/Heater.tsx
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid2 as Grid,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import DeviceLinkDrawer from "common/DeviceLinkDrawer";
|
||||
import ObjectControls from "common/ObjectControls";
|
||||
import ObjectHeaterActions from "objectHeater/ObjectHeaterActions";
|
||||
import { useDeviceAPI, useGlobalState, useObjectHeaterAPI, useUserAPI } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import PageContainer from "./PageContainer";
|
||||
import { ObjectHeater } from "models/ObjectHeater";
|
||||
//import { MatchParams } from "navigation/Routes";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { Component, Device, Scope } from "models";
|
||||
import { useSnackbar, useThemeType } from "hooks";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import ObjectHeaterCharts from "objectHeater/ObjectHeaterCharts";
|
||||
import HeaterDarkIcon from "assets/components/heaterDark.png";
|
||||
import HeaterLightIcon from "assets/components/heaterLight.png";
|
||||
import Link from "@mui/icons-material/Link";
|
||||
import { LinkOff } from "@mui/icons-material";
|
||||
import { UnitMeasurement } from "models/UnitMeasurement";
|
||||
import TemperatureIcon from "component/TemperatureIcon";
|
||||
import FuelIcon from "products/CommonIcons/fuelIcon";
|
||||
import moment from "moment";
|
||||
import Warning from "@mui/icons-material/Warning";
|
||||
import { getTemperatureUnit } from "utils";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function Heater() {
|
||||
const [openLinkDrawer, setOpenLinkDrawer] = useState(false);
|
||||
const { openSnack } = useSnackbar();
|
||||
const userAPI = useUserAPI();
|
||||
const heaterAPI = useObjectHeaterAPI();
|
||||
const deviceAPI = useDeviceAPI();
|
||||
const location = useLocation();
|
||||
//const match = useRouteMatch<MatchParams>();
|
||||
//const heaterID = match.params.heaterID;
|
||||
const heaterID = useParams<{ heaterKey: string }>()?.heaterKey ?? "";
|
||||
const [{ as, user }] = useGlobalState();
|
||||
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
||||
const [heater, setHeater] = useState<ObjectHeater>(ObjectHeater.create());
|
||||
const [loadingDevs, setLoadingDevs] = useState(false);
|
||||
const [devices, setDevices] = useState<Map<string, pond.ComprehensiveDevice>>(
|
||||
new Map<string, pond.ComprehensiveDevice>()
|
||||
);
|
||||
const [displayDevice, setDisplayDevice] = useState<pond.ComprehensiveDevice>();
|
||||
const [componentsByDevice, setComponentsByDevice] = useState<Map<string, Component[]>>();
|
||||
const [displayedMutations, setDisplayedMutations] = useState<pond.LinearMutation[]>([]);
|
||||
const [heaterComp, setHeaterComp] = useState<Component>();
|
||||
const [heaterTempComp, setHeaterTempComp] = useState<Component>();
|
||||
const [siteTempComp, setSiteTempComp] = useState<Component>();
|
||||
const [fuelLevel, setFuelLevel] = useState<string>("Unknown");
|
||||
const themeType = useThemeType();
|
||||
const [heaterStateUpToDate, setHeaterStateUpToDate] = useState(true);
|
||||
const [heaterTempUpToDate, setHeaterTempUpToDate] = useState(true);
|
||||
const [siteTempUpToDate, setSiteTempUpToDate] = useState(true);
|
||||
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
|
||||
const [unlinkDialogOpen, setUnlinkDialogOpen] = useState(false);
|
||||
|
||||
const zoomOut = () => {
|
||||
setXDomain(["dataMin", "dataMax"]);
|
||||
};
|
||||
|
||||
const setComponents = (compDev: pond.ComprehensiveDevice) => {
|
||||
compDev.components.forEach(component => {
|
||||
let c = Component.any(component);
|
||||
if (
|
||||
c.settings.type === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT &&
|
||||
c.settings.subtype === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER
|
||||
) {
|
||||
setHeaterComp(c);
|
||||
if (c.lastMeasurement[0] && c.lastMeasurement[0].timestamps[0]) {
|
||||
setHeaterStateUpToDate(
|
||||
moment().diff(moment(c.lastMeasurement[0].timestamps[0]), "milliseconds") <=
|
||||
c.settings.reportPeriodMs
|
||||
);
|
||||
}
|
||||
}
|
||||
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_TEMPERATURE) {
|
||||
setHeaterTempComp(c);
|
||||
if (c.lastMeasurement[0] && c.lastMeasurement[0].timestamps[0]) {
|
||||
setHeaterTempUpToDate(
|
||||
moment().diff(moment(c.lastMeasurement[0].timestamps[0]), "milliseconds") <=
|
||||
c.settings.reportPeriodMs
|
||||
);
|
||||
}
|
||||
}
|
||||
if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_DHT) {
|
||||
setSiteTempComp(c);
|
||||
if (c.lastMeasurement[0] && c.lastMeasurement[0].timestamps[0]) {
|
||||
setSiteTempUpToDate(
|
||||
moment().diff(moment(c.lastMeasurement[0].timestamps[0]), "milliseconds") <=
|
||||
c.settings.reportPeriodMs
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let key = heaterID;
|
||||
let kind = "objectHeater";
|
||||
if (as) {
|
||||
key = as;
|
||||
kind = "team";
|
||||
}
|
||||
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
|
||||
setPermissions(resp.permissions);
|
||||
});
|
||||
}, [as, heaterID, userAPI, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.state && location.state.heater) {
|
||||
setHeater(location.state.heater);
|
||||
if (location.state.devices) {
|
||||
let map: Map<string, Component[]> = new Map<string, Component[]>();
|
||||
let devMap = new Map<string, pond.ComprehensiveDevice>();
|
||||
location.state.devices.forEach((compDev: pond.ComprehensiveDevice) => {
|
||||
let device = Device.any(compDev.device);
|
||||
let components = compDev.components.map(c => Component.any(c));
|
||||
devMap.set(device.id().toString(), pond.ComprehensiveDevice.fromObject(compDev));
|
||||
map.set(device.id().toString(), components);
|
||||
setDisplayDevice(compDev);
|
||||
setComponents(compDev);
|
||||
});
|
||||
setDevices(devMap);
|
||||
setComponentsByDevice(map);
|
||||
setDisplayedMutations(location.state.heater.settings.mutations ?? []);
|
||||
}
|
||||
} else {
|
||||
//if the heater was not carried from the heaters page, ie. page was reloaded, we will need to load the heater from the backend
|
||||
heaterAPI
|
||||
.getObjectHeater(heaterID)
|
||||
.then(resp => {
|
||||
let h = ObjectHeater.any(resp.data.heater);
|
||||
setHeater(h);
|
||||
setDisplayedMutations(h.settings.mutations ?? []);
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
});
|
||||
|
||||
if (!loadingDevs) {
|
||||
setLoadingDevs(true);
|
||||
deviceAPI
|
||||
.list(
|
||||
100,
|
||||
0,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
true,
|
||||
[heaterID],
|
||||
["objectHeater"]
|
||||
)
|
||||
.then(resp => {
|
||||
setLoadingDevs(false);
|
||||
let map: Map<string, Component[]> = new Map<string, Component[]>();
|
||||
let devMap = new Map<string, pond.ComprehensiveDevice>();
|
||||
resp.data.comprehensiveDevices.forEach(compDev => {
|
||||
let device = Device.any(compDev.device);
|
||||
let components = compDev.components.map(c => Component.any(c));
|
||||
devMap.set(device.id().toString(), pond.ComprehensiveDevice.fromObject(compDev));
|
||||
map.set(device.id().toString(), components);
|
||||
setDisplayDevice(compDev);
|
||||
setComponents(compDev);
|
||||
});
|
||||
setDevices(devMap);
|
||||
setComponentsByDevice(map);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [location, heaterAPI, heaterID, deviceAPI]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const disconnectDisplayDevice = () => {
|
||||
if (displayDevice) {
|
||||
let deviceID = displayDevice.device?.settings?.deviceId;
|
||||
if (deviceID) {
|
||||
heaterAPI
|
||||
.updateLink(heater.key, "objectHeater", deviceID.toString(), "device", [])
|
||||
.then(resp => {
|
||||
if (deviceID) {
|
||||
devices.delete(deviceID.toString());
|
||||
setDisplayDevice(undefined);
|
||||
setComponentsByDevice(undefined);
|
||||
setHeaterComp(undefined);
|
||||
setHeaterTempComp(undefined);
|
||||
setSiteTempComp(undefined);
|
||||
setFuelLevel("Unknown");
|
||||
let updatedHeater = heater;
|
||||
updatedHeater.settings.mutations = [];
|
||||
heaterAPI
|
||||
.updateObjectHeater(heater.key, heater.name, updatedHeater.settings)
|
||||
.then(resp => {
|
||||
setHeater(updatedHeater);
|
||||
setDisplayedMutations([...updatedHeater.settings.mutations]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deviceDrawer = () => {
|
||||
return (
|
||||
<DeviceLinkDrawer
|
||||
open={openLinkDrawer}
|
||||
close={() => {
|
||||
setOpenLinkDrawer(false);
|
||||
}}
|
||||
linkedDevices={devices}
|
||||
updateLinkedDevices={(dev, linked) => {
|
||||
let deviceID = dev.device?.settings?.deviceId;
|
||||
if (deviceID) {
|
||||
if (linked) {
|
||||
heaterAPI
|
||||
.updateLink(heater.key, "objectHeater", deviceID.toString(), "device", [
|
||||
"read",
|
||||
"write",
|
||||
"grant",
|
||||
"revoke"
|
||||
])
|
||||
.then(resp => {
|
||||
if (deviceID) {
|
||||
devices.set(deviceID.toString(), dev);
|
||||
setDisplayDevice(dev);
|
||||
setOpenLinkDrawer(false);
|
||||
openSnack("Device Linked to Heater");
|
||||
setComponents(dev);
|
||||
let components: Map<string, Component[]> = new Map<string, Component[]>();
|
||||
components.set(
|
||||
deviceID.toString(),
|
||||
dev.components.map(c => Component.any(c))
|
||||
);
|
||||
setComponentsByDevice(components);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const displayHeaterState = () => {
|
||||
let state = "Unknown";
|
||||
if (heaterComp) {
|
||||
let lastReading = heaterComp.lastMeasurement[0];
|
||||
if (lastReading && lastReading.values.length > 0) {
|
||||
if (lastReading.values[0].values[0] === 0) {
|
||||
state = "Off";
|
||||
} else if (lastReading.values[0].values[0] === 1) {
|
||||
state = "On";
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography variant="body1">{state}</Typography>
|
||||
{!heaterStateUpToDate && (
|
||||
<Tooltip title="Component Not Reporting">
|
||||
<Warning style={{ color: "orange" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const tempUnit = () => {
|
||||
let unit = "°C";
|
||||
if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
||||
unit = "°F";
|
||||
}
|
||||
return unit;
|
||||
};
|
||||
|
||||
const displayHeaterTemp = () => {
|
||||
let temp = "Unknown";
|
||||
if (heaterTempComp) {
|
||||
let lastReading = UnitMeasurement.any(heaterTempComp.lastMeasurement[0], user);
|
||||
if (lastReading && lastReading.values[0] && lastReading.values[0].values[0]) {
|
||||
temp = lastReading.values[0].values[0].toFixed(2) + tempUnit();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TemperatureIcon heightWidth={20} />
|
||||
<Typography variant="body1" style={{ paddingLeft: 5, fontWeight: 650 }}>
|
||||
{temp}
|
||||
</Typography>
|
||||
{!heaterTempUpToDate && (
|
||||
<Tooltip title="Component Not Reporting">
|
||||
<Warning style={{ color: "orange" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const displaySiteTemp = () => {
|
||||
let temp = "Unknown";
|
||||
if (siteTempComp) {
|
||||
siteTempComp.lastMeasurement.forEach(um => {
|
||||
if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
|
||||
let lastReading = UnitMeasurement.any(um, user);
|
||||
if (lastReading && lastReading.values[0] && lastReading.values[0].values[0]) {
|
||||
temp = lastReading.values[0].values[0].toFixed(2) + tempUnit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TemperatureIcon heightWidth={20} />
|
||||
<Typography variant="body1" style={{ paddingLeft: 5, fontWeight: 650 }}>
|
||||
{temp}
|
||||
</Typography>
|
||||
{!siteTempUpToDate && (
|
||||
<Tooltip title="Component Not Reporting">
|
||||
<Warning style={{ color: "orange" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const unlinkDialog = () => {
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={unlinkDialogOpen}
|
||||
onClose={() => {
|
||||
setUnlinkDialogOpen(false);
|
||||
}}>
|
||||
<DialogTitle>Unlink Device</DialogTitle>
|
||||
<DialogContent>Remove device from heater?</DialogContent>
|
||||
<DialogActions>
|
||||
<Button>Cancel</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
disconnectDisplayDevice();
|
||||
setUnlinkDialogOpen(false);
|
||||
}}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const heaterInformation = () => {
|
||||
return (
|
||||
<Box marginBottom={2}>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
alignContent="center"
|
||||
alignItems="center">
|
||||
<Grid>
|
||||
<Box display="flex" alignItems="center">
|
||||
<img
|
||||
src={themeType === "light" ? HeaterDarkIcon : HeaterLightIcon}
|
||||
alt={"heaterIcon"}
|
||||
height={40}
|
||||
width={40}
|
||||
/>
|
||||
<Typography variant="h6" style={{ paddingLeft: 10 }}>
|
||||
{heater.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
<Grid>
|
||||
<Button onClick={zoomOut} variant="outlined" style={{ marginRight: 5 }}>
|
||||
Zoom Out
|
||||
</Button>
|
||||
{!displayDevice ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpenLinkDrawer(true);
|
||||
}}
|
||||
variant="contained"
|
||||
color="primary">
|
||||
<Link />
|
||||
<Typography variant="caption" style={{ paddingLeft: 5 }}>
|
||||
link device
|
||||
</Typography>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setUnlinkDialogOpen(true);
|
||||
}}
|
||||
variant="contained">
|
||||
<LinkOff />
|
||||
<Typography variant="caption" style={{ paddingLeft: 5 }}>
|
||||
unlink device
|
||||
</Typography>
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Box marginTop={2}>
|
||||
<Card style={{ width: "100%", padding: 10 }}>
|
||||
{displayDevice ? (
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
spacing={4}>
|
||||
<Grid size={{
|
||||
xs:6, sm:6, md:3
|
||||
}}
|
||||
container>
|
||||
<Grid container alignItems="center">
|
||||
{displayHeaterState()}
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Typography variant="caption">Heater State:</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs:6, sm:6, md:3}}>
|
||||
<Grid container alignItems="center">
|
||||
{displayHeaterTemp()}
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Typography variant="caption">Heater Temperature:</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs:6, sm:6, md:3}}>
|
||||
<Grid container alignItems="center">
|
||||
{displaySiteTemp()}
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Typography variant="caption">Site Temperature:</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid size={{ xs:6, sm:6, md:3}}>
|
||||
<Grid container alignItems="center">
|
||||
<FuelIcon />
|
||||
<Typography variant="body1" style={{ paddingLeft: 5, fontWeight: 650 }}>
|
||||
{fuelLevel}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Typography variant="caption">Fuel Level:</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Box>No Connected Devices Found</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const deviceInformation = () => {
|
||||
if (displayDevice) {
|
||||
if (loadingDevs) {
|
||||
return <Box>Loading Device</Box>;
|
||||
} else {
|
||||
return (
|
||||
<Box>
|
||||
<ObjectHeaterCharts
|
||||
device={displayDevice}
|
||||
linearMutations={displayedMutations}
|
||||
getFuelLevel={level => {
|
||||
setFuelLevel(level);
|
||||
}}
|
||||
xDomain={xDomain}
|
||||
changeXDomain={setXDomain}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<Box>
|
||||
<Card style={{ height: 350 }}>
|
||||
<Box display="flex" justifyContent="center" alignItems="center" height={"100%"}>
|
||||
Connect Device To Heater to View Data
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{deviceDrawer()}
|
||||
{unlinkDialog()}
|
||||
<Box padding={2}>
|
||||
<ObjectControls
|
||||
actions={
|
||||
<ObjectHeaterActions
|
||||
componentsByDevice={componentsByDevice}
|
||||
heater={heater}
|
||||
permissions={permissions}
|
||||
refreshCallback={() => {}}
|
||||
immediateUpdate={newHeater => {
|
||||
setHeater(newHeater);
|
||||
setDisplayedMutations([...newHeater.settings.mutations]);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
permissions={permissions}
|
||||
devices={displayDevice && [Device.any(displayDevice.device)]}
|
||||
/>
|
||||
{heaterInformation()}
|
||||
{deviceInformation()}
|
||||
</Box>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -32,45 +32,55 @@ export default function Heaters() {
|
|||
const [tablePage, setTablePage] = useState(0)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [searchText, setSearchText] = useState("")
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
objectHeaterAPI
|
||||
.listObjectHeatersPageData(500, 0, undefined, undefined, undefined, as)
|
||||
.listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as)
|
||||
.then(resp => {
|
||||
setTotal(resp.data.total)
|
||||
setHeaters(resp.data.heaterData);
|
||||
});
|
||||
}, [objectHeaterAPI, as]);
|
||||
}, [objectHeaterAPI, as, pageSize, tablePage, searchText]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
const goToHeater = (heater: ObjectHeater, devices: pond.ComprehensiveDevice[]) => {
|
||||
let path = "/objectHeaters/" + heater.key;
|
||||
navigate(path, {state: { heater: heater, devices: devices}})
|
||||
const goToHeater = (row: pond.ObjectHeaterData) => {
|
||||
let path = "/heaters/" + row.heater?.key;
|
||||
navigate(path, {state: { heater: row.heater, devices: row.devices}})
|
||||
};
|
||||
|
||||
const handleChange = (event: any) => {
|
||||
setPageSize(event.target.value);
|
||||
};
|
||||
|
||||
const columns = (): Column<pond.ObjectHeaterData>[] => {
|
||||
return [
|
||||
{
|
||||
title: "Heater",
|
||||
cellStyle: {padding: 2},
|
||||
render: row => (<Typography>{row.heater?.name}</Typography>)
|
||||
},
|
||||
{
|
||||
title: "Site",
|
||||
cellStyle: {padding: 2},
|
||||
render: row => (<Typography>{row.site?.settings?.siteName}</Typography>)
|
||||
},
|
||||
{
|
||||
title: "Make",
|
||||
cellStyle: {padding: 2},
|
||||
render: row => (<Typography>{row.heater?.settings?.make}</Typography>)
|
||||
},
|
||||
{
|
||||
title: "Model",
|
||||
cellStyle: {padding: 2},
|
||||
render: row => (<Typography>{row.heater?.settings?.model}</Typography>)
|
||||
},
|
||||
{
|
||||
title: "Device",
|
||||
cellStyle: {padding: 2},
|
||||
render: row => {
|
||||
let display: string | number | undefined = "No Device Found"
|
||||
if (row.devices.length > 0) {
|
||||
|
|
@ -89,13 +99,16 @@ export default function Heaters() {
|
|||
const desktopTable = () => {
|
||||
return (
|
||||
<ResponsiveTable<pond.ObjectHeaterData>
|
||||
title={"Heaters"}
|
||||
page={tablePage}
|
||||
setSearchText={(text) => {setSearchText(text)}}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
rows={heaters}
|
||||
columns={columns}
|
||||
setPage={(page)=>{setTablePage(page)}}
|
||||
handleRowsPerPageChange={()=>{}}
|
||||
handleRowsPerPageChange={handleChange}
|
||||
onRowClick={goToHeater}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue