frontend/src/pages/Heater.tsx

538 lines
18 KiB
TypeScript

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, as)
.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"],
undefined,
undefined,
as
)
.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, as]); //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", [], as)
.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, as)
.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"
], as)
.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"
justifyContent="space-between"
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 spacing={0}>
<Grid size={12}>
{displayHeaterState()}
</Grid>
<Grid size={12}>
<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>
);
}