Files
EMS-ESP32/interface/src/project/SettingsCustomization.tsx
2022-04-03 13:44:54 +02:00

334 lines
10 KiB
TypeScript

import { FC, useState, useEffect, useCallback } from 'react';
import {
Button,
Table,
TableBody,
TableHead,
TableRow,
Typography,
Box,
MenuItem,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
ToggleButton,
ToggleButtonGroup
} from '@mui/material';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import { styled } from '@mui/material/styles';
import { useSnackbar } from 'notistack';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Cancel';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import FavoriteBorderOutlinedIcon from '@mui/icons-material/FavoriteBorderOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import { ButtonRow, FormLoader, ValidatedTextField, SectionContent } from '../components';
import * as EMSESP from './api';
import { extractErrorMessage } from '../utils';
import { DeviceShort, Devices, DeviceEntity } from './types';
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: '#607d8b'
// color: theme.palette.common.white,
// fontSize: 12
},
[`&.${tableCellClasses.body}`]: {
// fontSize: 12
}
}));
const SettingsCustomization: FC = () => {
const { enqueueSnackbar } = useSnackbar();
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>();
const [devices, setDevices] = useState<Devices>();
const [errorMessage, setErrorMessage] = useState<string>();
const [selectedDevice, setSelectedDevice] = useState<number>(0);
const [confirmReset, setConfirmReset] = useState<boolean>(false);
// eslint-disable-next-line
const [masks, setMasks] = useState(() => ['']);
const fetchDevices = useCallback(async () => {
try {
setDevices((await EMSESP.readDevices()).data);
} catch (error: any) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch device list'));
}
}, []);
const fetchDeviceEntities = async (unique_id: number) => {
try {
setDeviceEntities((await EMSESP.readDeviceEntities({ id: unique_id })).data);
} catch (error: any) {
setErrorMessage(extractErrorMessage(error, 'Problem fetching device entities'));
}
};
useEffect(() => {
fetchDevices();
}, [fetchDevices]);
function formatValue(value: any) {
if (typeof value === 'number') {
return new Intl.NumberFormat().format(value);
} else if (value === undefined) {
return '';
} else if (typeof value === 'boolean') {
return value ? 'true' : 'false';
}
return value;
}
const renderDeviceList = () => {
if (!devices) {
return <FormLoader errorMessage={errorMessage} />;
}
function compareDevices(a: DeviceShort, b: DeviceShort) {
if (a.s < b.s) {
return -1;
}
if (a.s > b.s) {
return 1;
}
return 0;
}
const changeSelectedDevice = (event: React.ChangeEvent<HTMLInputElement>) => {
const selected_device = parseInt(event.target.value, 10);
setSelectedDevice(selected_device);
fetchDeviceEntities(selected_device);
};
return (
<>
<Box color="warning.main">
<Typography variant="body2">Select a device and customize each of its entities using the options:</Typography>
<Typography mt={1} ml={2} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<FavoriteBorderOutlinedIcon color="success" sx={{ fontSize: 13 }} />
&nbsp;mark it as favorite to be listed at the top of the Dashboard
</Typography>
<Typography ml={2} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<EditOffOutlinedIcon color="action" sx={{ fontSize: 13 }} />
&nbsp;make it read-only, only if it has write operation available
</Typography>
<Typography ml={2} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<CommentsDisabledOutlinedIcon color="action" sx={{ fontSize: 13 }} />
&nbsp;excluded it from MQTT and API outputs
</Typography>
<Typography ml={2} mb={1} display="block" variant="body2" sx={{ alignItems: 'center', display: 'flex' }}>
<VisibilityOffOutlinedIcon color="action" sx={{ fontSize: 13 }} />
&nbsp;hide it from the Dashboard
</Typography>
</Box>
<ValidatedTextField
name="device"
label="EMS Device"
variant="outlined"
fullWidth
value={selectedDevice}
onChange={changeSelectedDevice}
margin="normal"
select
>
<MenuItem disabled key={0} value={0}>
Select a device...
</MenuItem>
{devices.devices.sort(compareDevices).map((device: DeviceShort, index) => (
<MenuItem key={index} value={device.i}>
{device.s}
</MenuItem>
))}
</ValidatedTextField>
</>
);
};
const saveCustomization = async () => {
if (deviceEntities && selectedDevice) {
const masked_entities = deviceEntities
// .filter((de) => de.m)
.map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.s);
try {
const response = await EMSESP.writeMaskedEntities({
id: selectedDevice,
entity_ids: masked_entities
});
if (response.status === 200) {
enqueueSnackbar('Customization saved', { variant: 'success' });
} else {
enqueueSnackbar('Customization save failed', { variant: 'error' });
}
} catch (error: any) {
enqueueSnackbar(extractErrorMessage(error, 'Problem sending entity list'), { variant: 'error' });
}
}
};
const renderDeviceData = () => {
if (devices?.devices.length === 0 || !deviceEntities) {
return;
}
const setMask = (de: DeviceEntity, newMask: string[]) => {
var new_mask = 0;
for (let entry of newMask) {
new_mask |= Number(entry);
}
de.m = new_mask;
setMasks(newMask);
};
const getMask = (de: DeviceEntity) => {
var new_masks = [];
if ((de.m & 1) === 1 || de.n === '') {
new_masks.push('1');
}
if ((de.m & 2) === 2) {
new_masks.push('2');
}
if ((de.m & 4) === 4 && de.w) {
new_masks.push('4');
}
if ((de.m & 8) === 8) {
new_masks.push('8');
}
return new_masks;
};
return (
<Table size="small" padding="normal">
<TableHead>
<TableRow>
<StyledTableCell align="center">OPTIONS</StyledTableCell>
<StyledTableCell align="left">ENTITY NAME (CODE)</StyledTableCell>
<StyledTableCell align="right">VALUE</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{deviceEntities.map((de) => (
<TableRow key={de.s} hover>
<StyledTableCell padding="none">
<ToggleButtonGroup
size="small"
color="secondary"
value={getMask(de)}
onChange={(event, mask) => {
setMask(de, mask);
}}
>
<ToggleButton value="8" color="success" disabled={(de.m & 1) !== 0 || de.n === ''}>
<FavoriteBorderOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
<ToggleButton value="4" disabled={!de.w}>
<EditOffOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
<ToggleButton value="2">
<CommentsDisabledOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
<ToggleButton value="1">
<VisibilityOffOutlinedIcon sx={{ fontSize: 14 }} />
</ToggleButton>
</ToggleButtonGroup>
</StyledTableCell>
<StyledTableCell>
{de.n}&nbsp;({de.s})
</StyledTableCell>
<StyledTableCell align="right">{formatValue(de.v)}</StyledTableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};
const resetCustomization = async () => {
try {
await EMSESP.resetCustomizations();
enqueueSnackbar('All customizations have been removed. Restarting...', { variant: 'info' });
} catch (error: any) {
enqueueSnackbar(extractErrorMessage(error, 'Problem resetting customizations'), { variant: 'error' });
} finally {
setConfirmReset(false);
}
};
const renderResetDialog = () => (
<Dialog open={confirmReset} onClose={() => setConfirmReset(false)}>
<DialogTitle>Reset</DialogTitle>
<DialogContent dividers>
Are you sure you want remove all customizations? EMS-ESP will then restart.
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
Cancel
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={resetCustomization}
autoFocus
color="error"
>
Reset
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
return (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
Device Entities
</Typography>
{renderDeviceList()}
{renderDeviceData()}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<ButtonRow>
<Button startIcon={<SaveIcon />} variant="outlined" color="primary" onClick={() => saveCustomization()}>
Save
</Button>
</ButtonRow>
</Box>
<ButtonRow>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
color="error"
onClick={() => setConfirmReset(true)}
>
Reset
</Button>
</ButtonRow>
</Box>
{renderResetDialog()}
</>
);
};
return (
<SectionContent title="User Customization" titleGutter>
{content()}
</SectionContent>
);
};
export default SettingsCustomization;