538 lines
18 KiB
TypeScript
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>
|
|
);
|
|
}
|