set up the heater page and its subsequent imports

This commit is contained in:
csawatzky 2025-04-04 10:45:07 -06:00
parent 47d50194c5
commit b573e10707
5 changed files with 1388 additions and 9 deletions

View file

@ -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>
);

View 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>
);
}

View 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
View 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>
);
}

View file

@ -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}
/>
)
};