mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 00:39:50 +03:00
Merge remote-tracking branch 'origin/v3.4' into dev
This commit is contained in:
30
interface/src/project/Dashboard.tsx
Normal file
30
interface/src/project/Dashboard.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { FC } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
|
||||
|
||||
import DashboardStatus from './DashboardStatus';
|
||||
import DashboardData from './DashboardData';
|
||||
|
||||
const Dashboard: FC = () => {
|
||||
useLayoutTitle('Dashboard');
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="data" label="Devices & Sensors" />
|
||||
<Tab value="status" label="Status" />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="data" element={<DashboardData />} />
|
||||
<Route path="status" element={<DashboardStatus />} />
|
||||
<Route path="/*" element={<Navigate replace to="data" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
825
interface/src/project/DashboardData.tsx
Normal file
825
interface/src/project/DashboardData.tsx
Normal file
@@ -0,0 +1,825 @@
|
||||
import { FC, useState, useContext, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
MenuItem,
|
||||
InputAdornment,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Grid,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
|
||||
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
|
||||
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import parseMilliseconds from 'parse-ms';
|
||||
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import SendIcon from '@mui/icons-material/TrendingFlat';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
|
||||
import DeviceIcon from './DeviceIcon';
|
||||
|
||||
import { IconContext } from 'react-icons';
|
||||
|
||||
import { AuthenticatedContext } from '../contexts/authentication';
|
||||
|
||||
import { ButtonRow, FormLoader, ValidatedTextField, SectionContent, MessageBox } from '../components';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { numberValue, updateValue, extractErrorMessage, useRest } from '../utils';
|
||||
|
||||
import {
|
||||
SensorData,
|
||||
CoreData,
|
||||
DeviceData,
|
||||
DeviceValue,
|
||||
DeviceValueUOM,
|
||||
DeviceValueUOM_s,
|
||||
AnalogTypes,
|
||||
Sensor,
|
||||
Analog
|
||||
} from './types';
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
[`&.${tableCellClasses.head}`]: {
|
||||
backgroundColor: theme.palette.common.black,
|
||||
color: theme.palette.common.white,
|
||||
fontSize: 14
|
||||
}
|
||||
}));
|
||||
|
||||
const StyledTableRow = styled(TableRow)(({ theme }) => ({
|
||||
'&:nth-of-type(odd)': {
|
||||
backgroundColor: theme.palette.action.hover
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.info.light
|
||||
}
|
||||
}));
|
||||
|
||||
const DashboardData: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<CoreData>({ read: EMSESP.readCoreData });
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [deviceData, setDeviceData] = useState<DeviceData>();
|
||||
const [sensorData, setSensorData] = useState<SensorData>();
|
||||
const [deviceValue, setDeviceValue] = useState<DeviceValue>();
|
||||
const [sensor, setSensor] = useState<Sensor>();
|
||||
const [analog, setAnalog] = useState<Analog>();
|
||||
const [selectedDevice, setSelectedDevice] = useState<number>();
|
||||
const [deviceDialog, setDeviceDialog] = useState<number>(-1);
|
||||
|
||||
const desktopWindow = useMediaQuery('(min-width:600px)');
|
||||
|
||||
const refreshData = () => {
|
||||
if (analog || sensor || deviceValue) {
|
||||
return;
|
||||
}
|
||||
loadData();
|
||||
|
||||
if (selectedDevice === 0) {
|
||||
fetchSensorData();
|
||||
} else if (selectedDevice) {
|
||||
fetchDeviceData(selectedDevice);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => refreshData(), 60000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, [analog, sensor, deviceValue, selectedDevice]);
|
||||
|
||||
const fetchDeviceData = async (unique_id: number) => {
|
||||
try {
|
||||
setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data);
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching device data'), { variant: 'error' });
|
||||
} finally {
|
||||
setSelectedDevice(unique_id);
|
||||
setSensorData(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSensorData = async () => {
|
||||
try {
|
||||
setSensorData((await EMSESP.readSensorData()).data);
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching sensor data'), { variant: 'error' });
|
||||
} finally {
|
||||
setSelectedDevice(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||
` ${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? suffix : ''} `;
|
||||
|
||||
const formatDuration = (duration_min: number) => {
|
||||
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += pluralize(days, 'day');
|
||||
}
|
||||
if (hours) {
|
||||
formatted += pluralize(hours, 'hour');
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += pluralize(minutes, 'minute');
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
function formatValue(value: any, uom: number) {
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
}
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDuration(value * 60) : '0 hours';
|
||||
case DeviceValueUOM.MINUTES:
|
||||
return value ? formatDuration(value) : '0 minutes';
|
||||
case DeviceValueUOM.NONE:
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat().format(value);
|
||||
}
|
||||
return value;
|
||||
case DeviceValueUOM.DEGREES:
|
||||
case DeviceValueUOM.DEGREES_R:
|
||||
case DeviceValueUOM.FAHRENHEIT:
|
||||
return (
|
||||
new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 1
|
||||
}).format(value) +
|
||||
' ' +
|
||||
DeviceValueUOM_s[uom]
|
||||
);
|
||||
case DeviceValueUOM.SECONDS:
|
||||
return pluralize(value, DeviceValueUOM_s[uom]);
|
||||
default:
|
||||
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
|
||||
}
|
||||
}
|
||||
|
||||
const sendDeviceValue = async () => {
|
||||
if (selectedDevice && deviceValue) {
|
||||
try {
|
||||
const response = await EMSESP.writeValue({
|
||||
id: selectedDevice,
|
||||
devicevalue: deviceValue
|
||||
});
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Write command failed', { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Write access denied', { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Write command sent', { variant: 'success' });
|
||||
}
|
||||
setDeviceValue(undefined);
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem writing value'), { variant: 'error' });
|
||||
} finally {
|
||||
setDeviceValue(undefined);
|
||||
fetchDeviceData(selectedDevice);
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderDeviceValueDialog = () => {
|
||||
if (deviceValue) {
|
||||
return (
|
||||
<Dialog open={deviceValue !== undefined} onClose={() => setDeviceValue(undefined)}>
|
||||
<DialogTitle>Change Value</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{deviceValue.l && (
|
||||
<ValidatedTextField
|
||||
name="v"
|
||||
label={deviceValue.n}
|
||||
value={deviceValue.v}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
select
|
||||
onChange={updateValue(setDeviceValue)}
|
||||
>
|
||||
{deviceValue.l.map((val) => (
|
||||
<MenuItem value={val}>{val}</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
)}
|
||||
{!deviceValue.l && (
|
||||
<ValidatedTextField
|
||||
name="v"
|
||||
label={deviceValue.n}
|
||||
value={deviceValue.u ? Intl.NumberFormat().format(deviceValue.v) : deviceValue.v}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
type={deviceValue.u ? 'number' : 'text'}
|
||||
onChange={updateValue(setDeviceValue)}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">{DeviceValueUOM_s[deviceValue.u]}</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setDeviceValue(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SendIcon />}
|
||||
variant="outlined"
|
||||
type="submit"
|
||||
onClick={() => sendDeviceValue()}
|
||||
color="warning"
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addAnalogSensor = () => {
|
||||
setAnalog({ i: 0, n: '', u: 0, v: 0, o: 0, t: 0, f: 1 });
|
||||
};
|
||||
|
||||
const sendSensor = async () => {
|
||||
if (sensor) {
|
||||
try {
|
||||
const response = await EMSESP.writeSensor({
|
||||
id_str: sensor.is,
|
||||
name: sensor.n,
|
||||
offset: sensor.o
|
||||
});
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Sensor change failed', { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Access denied', { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Sensor updated', { variant: 'success' });
|
||||
}
|
||||
setSensor(undefined);
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem updating sensor'), { variant: 'error' });
|
||||
} finally {
|
||||
setSensor(undefined);
|
||||
fetchSensorData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderSensorDialog = () => {
|
||||
if (sensor) {
|
||||
return (
|
||||
<Dialog open={sensor !== undefined} onClose={() => setSensor(undefined)}>
|
||||
<DialogTitle>Edit Temperature Sensor</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
||||
<Typography variant="body2">Sensor ID {sensor.is}</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="n"
|
||||
label="Name"
|
||||
value={sensor.n}
|
||||
autoFocus
|
||||
sx={{ width: '30ch' }}
|
||||
onChange={updateValue(setSensor)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="Offset"
|
||||
value={numberValue(sensor.o)}
|
||||
sx={{ width: '12ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setSensor)}
|
||||
inputProps={{ min: '-5', max: '5', step: '0.1' }}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">°C</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setSensor(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
variant="outlined"
|
||||
type="submit"
|
||||
onClick={() => sendSensor()}
|
||||
color="warning"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDeviceDialog = () => {
|
||||
if (data && data.devices.length > 0 && deviceDialog !== -1) {
|
||||
return (
|
||||
<Dialog open={deviceDialog !== -1} onClose={() => setDeviceDialog(-1)}>
|
||||
<DialogTitle>Device Details</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<List dense={true}>
|
||||
<ListItem>
|
||||
<ListItemText primary="Type" secondary={data.devices[deviceDialog].t} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Name" secondary={data.devices[deviceDialog].n} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Brand" secondary={data.devices[deviceDialog].b} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Device ID"
|
||||
secondary={'0x' + ('00' + data.devices[deviceDialog].d.toString(16).toUpperCase()).slice(-2)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Product ID" secondary={data.devices[deviceDialog].p} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="Version" secondary={data.devices[deviceDialog].v} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={() => setDeviceDialog(-1)} color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDeviceData = (index: number) => {
|
||||
if (selectedDevice === index) {
|
||||
setSelectedDevice(undefined);
|
||||
} else {
|
||||
fetchDeviceData(index);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSensorData = () => {
|
||||
if (sensorData) {
|
||||
setSensorData(undefined);
|
||||
} else {
|
||||
fetchSensorData();
|
||||
}
|
||||
};
|
||||
|
||||
const renderCoreData = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
|
||||
{data.devices.length === 0 && <MessageBox my={2} level="warning" message="Scanning for EMS devices..." />}
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<StyledTableCell padding="checkbox" align="left" colSpan={2}>
|
||||
TYPE
|
||||
</StyledTableCell>
|
||||
{desktopWindow && <StyledTableCell>DESCRIPTION</StyledTableCell>}
|
||||
<StyledTableCell align="center">ENTITIES</StyledTableCell>
|
||||
<StyledTableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.devices.map((device, index) => (
|
||||
<TableRow
|
||||
hover
|
||||
selected={device.i === selectedDevice}
|
||||
key={index}
|
||||
onClick={() => device.e && toggleDeviceData(device.i)}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<DeviceIcon type={device.t} />
|
||||
</TableCell>
|
||||
<TableCell>{device.t}</TableCell>
|
||||
{desktopWindow && <TableCell>{device.n}</TableCell>}
|
||||
<TableCell align="center">{device.e}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => setDeviceDialog(index)}>
|
||||
<InfoOutlinedIcon color="info" fontSize="small" sx={{ verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{(data.active_sensors > 0 || data.analog_enabled) && (
|
||||
<TableRow hover selected={sensorData !== undefined} onClick={() => toggleSensorData()}>
|
||||
<TableCell>
|
||||
<DeviceIcon type="Sensor" />
|
||||
</TableCell>
|
||||
<TableCell>Sensors</TableCell>
|
||||
{desktopWindow && <TableCell>Attached EMS-ESP Sensors</TableCell>}
|
||||
<TableCell align="center">{data.active_sensors}</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => addAnalogSensor()} disabled={!data.analog_enabled}>
|
||||
<AddCircleOutlineOutlinedIcon fontSize="small" sx={{ verticalAlign: 'middle' }} />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</IconContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDeviceData = () => {
|
||||
if (data?.devices.length === 0 || !deviceData || !selectedDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendCommand = (dv: DeviceValue) => {
|
||||
if (dv.c && me.admin) {
|
||||
setDeviceValue(dv);
|
||||
}
|
||||
};
|
||||
|
||||
const renderNameCell = (dv: DeviceValue) => {
|
||||
if (dv.v === undefined && dv.c) {
|
||||
return (
|
||||
<StyledTableCell component="th" scope="row" sx={{ color: 'yellow' }}>
|
||||
command: {dv.n}
|
||||
</StyledTableCell>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StyledTableCell component="th" scope="row">
|
||||
{dv.n}
|
||||
</StyledTableCell>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="primary">
|
||||
{deviceData.label}
|
||||
</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<StyledTableCell padding="checkbox" style={{ width: 18 }}></StyledTableCell>
|
||||
<StyledTableCell align="left">ENTITY NAME/COMMAND</StyledTableCell>
|
||||
<StyledTableCell align="right">VALUE</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{deviceData.data.map((dv, i) => (
|
||||
<StyledTableRow key={i} onClick={() => sendCommand(dv)}>
|
||||
<StyledTableCell padding="checkbox">
|
||||
{dv.c && me.admin && (
|
||||
<IconButton size="small" aria-label="Edit">
|
||||
<EditIcon color="primary" fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</StyledTableCell>
|
||||
{renderNameCell(dv)}
|
||||
<StyledTableCell align="right">{formatValue(dv.v, dv.u)}</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const updateSensor = (sensordata: Sensor) => {
|
||||
if (sensordata && me.admin) {
|
||||
setSensor(sensordata);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAnalog = (analogdata: Analog) => {
|
||||
if (me.admin) {
|
||||
setAnalog(analogdata);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDallasData = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="primary">
|
||||
Temperature Sensors
|
||||
</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<StyledTableCell padding="checkbox" style={{ width: 18 }}></StyledTableCell>
|
||||
<StyledTableCell align="left">NAME</StyledTableCell>
|
||||
<StyledTableCell align="right">TEMPERATURE</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sensorData?.sensors.map((sensor_data) => (
|
||||
<StyledTableRow key={sensor_data.n} onClick={() => updateSensor(sensor_data)}>
|
||||
<StyledTableCell padding="checkbox">
|
||||
{me.admin && (
|
||||
<IconButton edge="start" size="small" aria-label="Edit">
|
||||
<EditIcon color="primary" fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell component="th" scope="row">
|
||||
{sensor_data.n}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell align="right">{formatValue(sensor_data.t, sensor_data.u)}</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderAnalogData = () => (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="primary">
|
||||
Analog Sensors
|
||||
</Typography>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<StyledTableCell padding="checkbox" style={{ width: 18 }}></StyledTableCell>
|
||||
<StyledTableCell>GPIO</StyledTableCell>
|
||||
<StyledTableCell>NAME</StyledTableCell>
|
||||
<StyledTableCell>TYPE</StyledTableCell>
|
||||
<StyledTableCell align="right">VALUE</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sensorData?.analogs.map((analog_data) => (
|
||||
<StyledTableRow key={analog_data.i} onClick={() => updateAnalog(analog_data)}>
|
||||
<StyledTableCell padding="checkbox">
|
||||
{me.admin && (
|
||||
<IconButton edge="start" size="small" aria-label="Edit">
|
||||
<EditIcon color="primary" fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell component="th" scope="row">
|
||||
{analog_data.i}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>{analog_data.n}</StyledTableCell>
|
||||
<StyledTableCell>{AnalogTypes[analog_data.t]}</StyledTableCell>
|
||||
<StyledTableCell align="right">{formatValue(analog_data.v, analog_data.u)}</StyledTableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
|
||||
const sendRemoveAnalog = async () => {
|
||||
if (analog) {
|
||||
try {
|
||||
const response = await EMSESP.writeAnalog({
|
||||
id: analog.i,
|
||||
name: analog.n,
|
||||
offset: analog.o,
|
||||
factor: analog.f,
|
||||
uom: analog.u,
|
||||
type: -1
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Analog deletion failed', { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Access denied', { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Analog sensor removed', { variant: 'success' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog sensor'), { variant: 'error' });
|
||||
} finally {
|
||||
setAnalog(undefined);
|
||||
fetchSensorData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendAnalog = async () => {
|
||||
if (analog) {
|
||||
try {
|
||||
const response = await EMSESP.writeAnalog({
|
||||
id: analog.i,
|
||||
name: analog.n,
|
||||
offset: analog.o,
|
||||
factor: analog.f,
|
||||
uom: analog.u,
|
||||
type: analog.t
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
enqueueSnackbar('Analog sensor update failed', { variant: 'error' });
|
||||
} else if (response.status === 403) {
|
||||
enqueueSnackbar('Access denied', { variant: 'error' });
|
||||
} else {
|
||||
enqueueSnackbar('Analog sensor updated', { variant: 'success' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog'), { variant: 'error' });
|
||||
} finally {
|
||||
setAnalog(undefined);
|
||||
fetchSensorData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderAnalogDialog = () => {
|
||||
if (analog) {
|
||||
return (
|
||||
<Dialog open={analog !== undefined} onClose={() => setAnalog(undefined)}>
|
||||
<DialogTitle>Edit Analog Sensor</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="i"
|
||||
label="GPIO"
|
||||
value={numberValue(analog.i)}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
autoFocus
|
||||
onChange={updateValue(setAnalog)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="n"
|
||||
label="Name"
|
||||
value={analog.n}
|
||||
sx={{ width: '20ch' }}
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField name="t" label="Type" value={analog.t} select onChange={updateValue(setAnalog)}>
|
||||
{AnalogTypes.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
{analog.t === 3 && (
|
||||
<>
|
||||
<Grid item>
|
||||
<ValidatedTextField name="u" label="UoM" value={analog.u} select onChange={updateValue(setAnalog)}>
|
||||
{DeviceValueUOM_s.map((val, i) => (
|
||||
<MenuItem key={i} value={i}>
|
||||
{val}
|
||||
</MenuItem>
|
||||
))}
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item></Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="o"
|
||||
label="Offset"
|
||||
value={numberValue(analog.o)}
|
||||
sx={{ width: '20ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '0', max: '3300', step: '1' }}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">mV</InputAdornment>
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="f"
|
||||
label="Factor"
|
||||
value={numberValue(analog.f)}
|
||||
sx={{ width: '20ch' }}
|
||||
type="number"
|
||||
variant="outlined"
|
||||
onChange={updateValue(setAnalog)}
|
||||
inputProps={{ min: '-100', max: '100', step: '0.1' }}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<Box color="warning.main" mt={2}>
|
||||
<Typography variant="body2">Warning: be careful when assigning a GPIO!</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
||||
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={() => sendRemoveAnalog()}>
|
||||
Remove
|
||||
</Button>
|
||||
</Box>
|
||||
<Button
|
||||
startIcon={<CancelIcon />}
|
||||
variant="outlined"
|
||||
onClick={() => setAnalog(undefined)}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
variant="outlined"
|
||||
type="submit"
|
||||
onClick={() => sendAnalog()}
|
||||
color="warning"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderCoreData()}
|
||||
{renderDeviceData()}
|
||||
{renderDeviceDialog()}
|
||||
{sensorData && sensorData.sensors.length > 0 && renderDallasData()}
|
||||
{sensorData && sensorData.analogs.length > 0 && renderAnalogData()}
|
||||
{renderDeviceValueDialog()}
|
||||
{renderSensorDialog()}
|
||||
{renderAnalogDialog()}
|
||||
<ButtonRow>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
|
||||
Refresh
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Device and Sensor Data" titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardData;
|
||||
259
interface/src/project/DashboardStatus.tsx
Normal file
259
interface/src/project/DashboardStatus.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import { FC, useState, useContext, useEffect } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Theme,
|
||||
useTheme,
|
||||
Box,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle
|
||||
} from '@mui/material';
|
||||
|
||||
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
|
||||
|
||||
import { AuthenticatedContext } from '../contexts/authentication';
|
||||
|
||||
import { ButtonRow, FormLoader, SectionContent } from '../components';
|
||||
|
||||
import { Status, busConnectionStatus } from './types';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
import { extractErrorMessage, useRest } from '../utils';
|
||||
|
||||
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||
|
||||
const busStatusHighlight = ({ status }: Status, theme: Theme) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.warning.main;
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
const busStatus = ({ status }: Status) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return 'Connected';
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return 'Tx issues - try a different Tx-Mode';
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return 'Disconnected';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
const pluralize = (count: number, noun: string) =>
|
||||
`${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? 's' : ''}`;
|
||||
|
||||
const formatDuration = (duration_sec: number) => {
|
||||
if (duration_sec === 0) {
|
||||
return ' ';
|
||||
}
|
||||
const roundTowardsZero = duration_sec > 0 ? Math.floor : Math.ceil;
|
||||
return (
|
||||
', ' +
|
||||
roundTowardsZero(duration_sec / 86400) +
|
||||
'd ' +
|
||||
(roundTowardsZero(duration_sec / 3600) % 24) +
|
||||
'h ' +
|
||||
(roundTowardsZero(duration_sec / 60) % 60) +
|
||||
'm ' +
|
||||
(roundTowardsZero(duration_sec) % 60) +
|
||||
's'
|
||||
);
|
||||
};
|
||||
|
||||
const formatRow = (name: string, success: number, fail: number, quality: number) => {
|
||||
if (success === 0 && fail === 0) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell sx={{ color: 'gray' }}>{name}</TableCell>
|
||||
<TableCell />
|
||||
<TableCell />
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell>{Intl.NumberFormat().format(success)}</TableCell>
|
||||
<TableCell>{Intl.NumberFormat().format(fail)}</TableCell>
|
||||
{showQuality(quality)}
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const showQuality = (quality: number) => {
|
||||
if (quality === 0) {
|
||||
return <TableCell />;
|
||||
}
|
||||
|
||||
if (quality === 100) {
|
||||
return <TableCell sx={{ color: '#00FF7F' }}>{quality}%</TableCell>;
|
||||
}
|
||||
|
||||
if (quality >= 95) {
|
||||
return <TableCell sx={{ color: 'orange' }}>{quality}%</TableCell>;
|
||||
} else {
|
||||
return <TableCell sx={{ color: 'red' }}>{quality}%</TableCell>;
|
||||
}
|
||||
};
|
||||
|
||||
const DashboardStatus: FC = () => {
|
||||
const { loadData, data, errorMessage } = useRest<Status>({ read: EMSESP.readStatus });
|
||||
|
||||
const theme = useTheme();
|
||||
const [confirmScan, setConfirmScan] = useState<boolean>(false);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => loadData(), 30000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const scan = async () => {
|
||||
try {
|
||||
await EMSESP.scanDevices();
|
||||
enqueueSnackbar('Scanning for devices...', { variant: 'info' });
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem initiating scan'), { variant: 'error' });
|
||||
} finally {
|
||||
setConfirmScan(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderScanDialog = () => (
|
||||
<Dialog open={confirmScan} onClose={() => setConfirmScan(false)}>
|
||||
<DialogTitle>EMS Device Scan</DialogTitle>
|
||||
<DialogContent dividers>Are you sure you want to initiate a full device scan of the EMS bus?</DialogContent>
|
||||
<DialogActions>
|
||||
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary" autoFocus>
|
||||
Scan
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: busStatusHighlight(data, theme) }}>
|
||||
<DirectionsBusIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="EMS Bus Status" secondary={busStatus(data) + formatDuration(data.uptime)} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<DeviceHubIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Active Devices & Sensors"
|
||||
secondary={
|
||||
pluralize(data.num_devices, 'EMS Device') +
|
||||
', ' +
|
||||
pluralize(data.num_sensors, 'Temperature Sensor') +
|
||||
', ' +
|
||||
pluralize(data.num_analogs, 'Analog Sensor')
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Box m={3}></Box>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell></TableCell>
|
||||
<TableCell>SUCCESS</TableCell>
|
||||
<TableCell>FAIL</TableCell>
|
||||
<TableCell>QUALITY</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{formatRow('EMS Telegrams Received (Rx)', data.rx_received, data.rx_fails, data.rx_quality)}
|
||||
{formatRow('EMS Reads (Tx)', data.tx_reads, data.tx_read_fails, data.tx_read_quality)}
|
||||
{formatRow('EMS Writes (Tx)', data.tx_writes, data.tx_write_fails, data.tx_write_quality)}
|
||||
{formatRow('Temperature Sensor Reads', data.sensor_reads, data.sensor_fails, data.sensor_quality)}
|
||||
{formatRow('Analog Sensor Reads', data.analog_reads, data.analog_fails, data.analog_quality)}
|
||||
{formatRow('MQTT Publishes', data.mqtt_count, data.mqtt_fails, data.mqtt_quality)}
|
||||
{formatRow('API Calls', data.api_calls, data.api_fails, data.api_quality)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</List>
|
||||
{renderScanDialog()}
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
||||
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Box>
|
||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<PermScanWifiIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
disabled={!me.admin}
|
||||
onClick={() => setConfirmScan(true)}
|
||||
>
|
||||
Scan for new devices
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="EMS Bus & Activity Status" titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardStatus;
|
||||
42
interface/src/project/DeviceIcon.tsx
Normal file
42
interface/src/project/DeviceIcon.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
||||
import { MdOutlineSensors } from 'react-icons/md';
|
||||
import { FaSolarPanel } from 'react-icons/fa';
|
||||
import { MdThermostatAuto } from 'react-icons/md';
|
||||
import { AiOutlineControl } from 'react-icons/ai';
|
||||
import { GiHeatHaze } from 'react-icons/gi';
|
||||
import { TiFlowSwitch } from 'react-icons/ti';
|
||||
import { VscVmConnect } from 'react-icons/vsc';
|
||||
import { AiOutlineGateway } from 'react-icons/ai';
|
||||
|
||||
interface DeviceIconProps {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const DeviceIcon: FC<DeviceIconProps> = ({ type }) => {
|
||||
switch (type) {
|
||||
case 'Boiler':
|
||||
return <CgSmartHomeBoiler />;
|
||||
case 'Sensor':
|
||||
return <MdOutlineSensors />;
|
||||
case 'Solar':
|
||||
return <FaSolarPanel />;
|
||||
case 'Thermostat':
|
||||
return <MdThermostatAuto />;
|
||||
case 'Mixer':
|
||||
return <AiOutlineControl />;
|
||||
case 'Heatpump':
|
||||
return <GiHeatHaze />;
|
||||
case 'Switch':
|
||||
return <TiFlowSwitch />;
|
||||
case 'Connect':
|
||||
return <VscVmConnect />;
|
||||
case 'Gateway':
|
||||
return <AiOutlineGateway />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default DeviceIcon;
|
||||
@@ -1,22 +0,0 @@
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
type BoardProfiles = {
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
export const BOARD_PROFILES: BoardProfiles = {
|
||||
S32: 'BBQKees Gateway S32',
|
||||
E32: 'BBQKees Gateway E32',
|
||||
NODEMCU: 'NodeMCU 32S',
|
||||
'MH-ET': 'MH-ET Live D1 Mini',
|
||||
LOLIN: 'Lolin D32',
|
||||
OLIMEX: 'Olimex ESP32-EVB'
|
||||
};
|
||||
|
||||
export function boardProfileSelectItems() {
|
||||
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||
<MenuItem key={code} value={code}>
|
||||
{BOARD_PROFILES[code]}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { PROJECT_PATH } from '../api';
|
||||
import { MenuAppBar } from '../components';
|
||||
import { AuthenticatedRoute } from '../authentication';
|
||||
|
||||
import EMSESPStatusController from './EMSESPStatusController';
|
||||
import EMSESPDataController from './EMSESPDataController';
|
||||
import EMSESPHelp from './EMSESPHelp';
|
||||
|
||||
class EMSESP extends Component<RouteComponentProps> {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Dashboard">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value={`/${PROJECT_PATH}/data`} label="Devices & Sensors" />
|
||||
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
||||
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/data`}
|
||||
component={EMSESPDataController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/status`}
|
||||
component={EMSESPStatusController}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/help`}
|
||||
component={EMSESPHelp}
|
||||
/>
|
||||
<Redirect to={`/${PROJECT_PATH}/data`} />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EMSESP;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPDataForm from './EMSESPDataForm';
|
||||
import { EMSESPData } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_DATA_ENDPOINT = ENDPOINT_ROOT + 'data';
|
||||
|
||||
type EMSESPDataControllerProps = RestControllerProps<EMSESPData>;
|
||||
|
||||
class EMSESPDataController extends Component<EMSESPDataControllerProps> {
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="Devices & Sensors">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={(formProps) => <EMSESPDataForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default restController(EMSESP_DATA_ENDPOINT, EMSESPDataController);
|
||||
@@ -1,692 +0,0 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
|
||||
import parseMilliseconds from 'parse-ms';
|
||||
|
||||
import { Decoder } from '@msgpack/msgpack';
|
||||
const decoder = new Decoder();
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableContainer,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
isWidthDown,
|
||||
Button,
|
||||
Tooltip,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
Dialog,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import ListIcon from '@material-ui/icons/List';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
import { RestFormProps, FormButton, extractEventValue } from '../components';
|
||||
|
||||
import {
|
||||
EMSESPData,
|
||||
EMSESPDeviceData,
|
||||
Device,
|
||||
DeviceValue,
|
||||
DeviceValueUOM,
|
||||
DeviceValueUOM_s,
|
||||
Sensor
|
||||
} from './EMSESPtypes';
|
||||
|
||||
import ValueForm from './ValueForm';
|
||||
import SensorForm from './SensorForm';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
|
||||
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices';
|
||||
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData';
|
||||
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue';
|
||||
export const WRITE_SENSOR_ENDPOINT = ENDPOINT_ROOT + 'writeSensor';
|
||||
|
||||
const StyledTableRow = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
'&:nth-child(even)': {
|
||||
backgroundColor: '#4e4e4e'
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.info.light
|
||||
}
|
||||
},
|
||||
|
||||
selected: {
|
||||
backgroundColor: theme.palette.common.white
|
||||
}
|
||||
})
|
||||
)(TableRow);
|
||||
|
||||
const StyledTableRowHeader = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
head: {
|
||||
backgroundColor: theme.palette.common.black
|
||||
}
|
||||
})
|
||||
)(TableRow);
|
||||
|
||||
const StyledTooltip = withStyles((theme: Theme) => ({
|
||||
tooltip: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: 'white',
|
||||
boxShadow: theme.shadows[1],
|
||||
fontSize: 11,
|
||||
border: '1px solid #dadde9'
|
||||
}
|
||||
}))(Tooltip);
|
||||
|
||||
function compareDevices(a: Device, b: Device) {
|
||||
if (a.t < b.t) {
|
||||
return -1;
|
||||
}
|
||||
if (a.t > b.t) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface EMSESPDataFormState {
|
||||
confirmScanDevices: boolean;
|
||||
processing: boolean;
|
||||
deviceData?: EMSESPDeviceData;
|
||||
selectedDevice?: number;
|
||||
edit_devicevalue?: DeviceValue;
|
||||
edit_Sensor?: Sensor;
|
||||
}
|
||||
|
||||
type EMSESPDataFormProps = RestFormProps<EMSESPData> &
|
||||
AuthenticatedContextProps &
|
||||
WithWidthProps;
|
||||
|
||||
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||
` ${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? suffix : ''} `;
|
||||
|
||||
export const formatDuration = (duration_min: number) => {
|
||||
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
|
||||
let formatted = '';
|
||||
if (days) {
|
||||
formatted += pluralize(days, 'day');
|
||||
}
|
||||
if (hours) {
|
||||
formatted += pluralize(hours, 'hour');
|
||||
}
|
||||
if (minutes) {
|
||||
formatted += pluralize(minutes, 'minute');
|
||||
}
|
||||
return formatted;
|
||||
};
|
||||
|
||||
function formatValue(value: any, uom: number) {
|
||||
switch (uom) {
|
||||
case DeviceValueUOM.HOURS:
|
||||
return value ? formatDuration(value * 60) : '0 hours';
|
||||
case DeviceValueUOM.MINUTES:
|
||||
return value ? formatDuration(value) : '0 minutes';
|
||||
case DeviceValueUOM.NONE:
|
||||
if (typeof value === 'number') {
|
||||
return new Intl.NumberFormat().format(value);
|
||||
}
|
||||
return value;
|
||||
case DeviceValueUOM.DEGREES:
|
||||
// always show with one decimal place
|
||||
return (
|
||||
new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 1
|
||||
}).format(value) +
|
||||
' ' +
|
||||
DeviceValueUOM_s[uom]
|
||||
);
|
||||
case DeviceValueUOM.TIMES:
|
||||
case DeviceValueUOM.SECONDS:
|
||||
return pluralize(value, DeviceValueUOM_s[uom]);
|
||||
default:
|
||||
return (
|
||||
new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EMSESPDataForm extends Component<
|
||||
EMSESPDataFormProps,
|
||||
EMSESPDataFormState
|
||||
> {
|
||||
state: EMSESPDataFormState = {
|
||||
confirmScanDevices: false,
|
||||
processing: false
|
||||
};
|
||||
|
||||
handleDeviceValueChange = (name: keyof DeviceValue) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({
|
||||
edit_devicevalue: {
|
||||
...this.state.edit_devicevalue!,
|
||||
[name]: extractEventValue(event)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cancelEditingDeviceValue = () => {
|
||||
this.setState({ edit_devicevalue: undefined });
|
||||
};
|
||||
|
||||
doneEditingDeviceValue = () => {
|
||||
const { edit_devicevalue, selectedDevice } = this.state;
|
||||
|
||||
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: selectedDevice,
|
||||
devicevalue: edit_devicevalue
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar('Write command sent to device', {
|
||||
variant: 'success'
|
||||
});
|
||||
this.handleRowClick(selectedDevice);
|
||||
} else if (response.status === 204) {
|
||||
this.props.enqueueSnackbar('Write command failed', {
|
||||
variant: 'error'
|
||||
});
|
||||
} else if (response.status === 403) {
|
||||
this.props.enqueueSnackbar('Write access denied', {
|
||||
variant: 'error'
|
||||
});
|
||||
} else {
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||
variant: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
if (edit_devicevalue) {
|
||||
this.setState({ edit_devicevalue: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
sendCommand = (dv: DeviceValue) => {
|
||||
if (dv.c && this.props.authenticatedContext.me.admin) {
|
||||
this.setState({ edit_devicevalue: dv });
|
||||
}
|
||||
};
|
||||
|
||||
handleSensorChange = (name: keyof Sensor) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
this.setState({
|
||||
edit_Sensor: {
|
||||
...this.state.edit_Sensor!,
|
||||
[name]: extractEventValue(event)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cancelEditingSensor = () => {
|
||||
this.setState({ edit_Sensor: undefined });
|
||||
};
|
||||
|
||||
doneEditingSensor = () => {
|
||||
const { edit_Sensor } = this.state;
|
||||
|
||||
redirectingAuthorizedFetch(WRITE_SENSOR_ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
// because input field with type=number doens't like negative values, force it here
|
||||
sensor: {
|
||||
no: edit_Sensor?.n, // no
|
||||
id: edit_Sensor?.i, // id
|
||||
temp: edit_Sensor?.t, // temp
|
||||
offset: Number(edit_Sensor?.o) // offset
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar('Sensor updated', {
|
||||
variant: 'success'
|
||||
});
|
||||
this.props.loadData();
|
||||
} else if (response.status === 204) {
|
||||
this.props.enqueueSnackbar('Sensor change failed', {
|
||||
variant: 'error'
|
||||
});
|
||||
} else if (response.status === 403) {
|
||||
this.props.enqueueSnackbar('Write access denied', {
|
||||
variant: 'error'
|
||||
});
|
||||
} else {
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||
variant: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
if (edit_Sensor) {
|
||||
this.setState({ edit_Sensor: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
sendSensor = (sn: Sensor) => {
|
||||
if (this.props.authenticatedContext.me.admin) {
|
||||
this.setState({ edit_Sensor: sn });
|
||||
}
|
||||
};
|
||||
|
||||
noDevices = () => {
|
||||
return this.props.data.devices.length === 0;
|
||||
};
|
||||
|
||||
noSensors = () => {
|
||||
return this.props.data.sensors.length === 0;
|
||||
};
|
||||
|
||||
noDeviceData = () => {
|
||||
return (this.state.deviceData?.data || []).length === 0;
|
||||
};
|
||||
|
||||
renderDevices() {
|
||||
const { width, data } = this.props;
|
||||
return (
|
||||
<TableContainer>
|
||||
<Typography variant="h6" color="primary">
|
||||
EMS Devices
|
||||
</Typography>
|
||||
<p></p>
|
||||
{!this.noDevices() && (
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'normal'}
|
||||
>
|
||||
<TableBody>
|
||||
{data.devices.sort(compareDevices).map((device) => (
|
||||
<TableRow
|
||||
hover
|
||||
key={device.i}
|
||||
onClick={() => this.handleRowClick(device.i)}
|
||||
>
|
||||
<TableCell>
|
||||
<StyledTooltip
|
||||
title={
|
||||
'DeviceID:0x' +
|
||||
('00' + device.d.toString(16).toUpperCase()).slice(-2) +
|
||||
' ProductID:' +
|
||||
device.p +
|
||||
' Version:' +
|
||||
device.v
|
||||
}
|
||||
placement="right-end"
|
||||
>
|
||||
<Button
|
||||
startIcon={<ListIcon />}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
{device.t}
|
||||
</Button>
|
||||
</StyledTooltip>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{device.b + ' ' + device.n}{' '}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{this.noDevices() && (
|
||||
<Box
|
||||
bgcolor="error.main"
|
||||
color="error.contrastText"
|
||||
p={2}
|
||||
mt={2}
|
||||
mb={2}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
No EMS devices found. Check the connections and for possible Tx
|
||||
errors.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
renderSensorData() {
|
||||
const { data } = this.props;
|
||||
const me = this.props.authenticatedContext.me;
|
||||
return (
|
||||
<TableContainer>
|
||||
<p></p>
|
||||
<Typography variant="h6" color="primary" paragraph>
|
||||
Sensors
|
||||
</Typography>
|
||||
{!this.noSensors() && (
|
||||
<Table size="small" padding="normal">
|
||||
<TableHead>
|
||||
<StyledTableRowHeader>
|
||||
<TableCell padding="checkbox" style={{ width: 18 }}></TableCell>
|
||||
<TableCell>Dallas Sensor #</TableCell>
|
||||
<TableCell align="left">ID / Name</TableCell>
|
||||
<TableCell align="right">Temperature</TableCell>
|
||||
</StyledTableRowHeader>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.sensors.map((sensorData) => (
|
||||
<StyledTableRow
|
||||
key={sensorData.n}
|
||||
onClick={() => this.sendSensor(sensorData)}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
{me.admin && (
|
||||
<StyledTooltip title="edit" placement="left-end">
|
||||
<IconButton edge="start" size="small" aria-label="Edit">
|
||||
<EditIcon color="primary" fontSize="small" />
|
||||
</IconButton>
|
||||
</StyledTooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{sensorData.n}
|
||||
</TableCell>
|
||||
<TableCell align="left">{sensorData.i}</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatValue(sensorData.t, DeviceValueUOM.DEGREES)}
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{this.noSensors() && (
|
||||
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||
<Typography variant="body1">
|
||||
<i>no Dallas temperature sensors were detected</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
renderAnalogData() {
|
||||
const { data } = this.props;
|
||||
return (
|
||||
<TableContainer>
|
||||
<p></p>
|
||||
{data.analog > 0 && (
|
||||
<Table size="small" padding="normal">
|
||||
<TableHead>
|
||||
<StyledTableRowHeader>
|
||||
<TableCell padding="normal" style={{ width: 18 }}></TableCell>
|
||||
<TableCell>Sensor Type</TableCell>
|
||||
<TableCell align="right">Value</TableCell>
|
||||
</StyledTableRowHeader>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell padding="normal"> </TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
Analog Input
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatValue(data.analog, DeviceValueUOM.MV)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
renderScanDevicesDialog() {
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.confirmScanDevices}
|
||||
onClose={this.onScanDevicesRejected}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
>
|
||||
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
Are you sure you want to start a scan on the EMS bus for all new
|
||||
devices?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={this.onScanDevicesRejected}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
onClick={this.onScanDevicesConfirmed}
|
||||
disabled={this.state.processing}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Start Scan
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
onScanDevices = () => {
|
||||
this.setState({ confirmScanDevices: true });
|
||||
};
|
||||
|
||||
onScanDevicesRejected = () => {
|
||||
this.setState({ confirmScanDevices: false });
|
||||
};
|
||||
|
||||
onScanDevicesConfirmed = () => {
|
||||
this.setState({ processing: true });
|
||||
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.props.enqueueSnackbar('Device scan is starting...', {
|
||||
variant: 'info'
|
||||
});
|
||||
this.setState({ processing: false, confirmScanDevices: false });
|
||||
} else {
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
|
||||
variant: 'error'
|
||||
});
|
||||
this.setState({ processing: false, confirmScanDevices: false });
|
||||
});
|
||||
};
|
||||
|
||||
handleRowClick = (device: any) => {
|
||||
this.setState({ selectedDevice: device, deviceData: undefined });
|
||||
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: device }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
})
|
||||
.then((arrayBuffer) => {
|
||||
const json: any = decoder.decode(arrayBuffer);
|
||||
this.setState({ deviceData: json });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || 'Problem getting device data',
|
||||
{ variant: 'error' }
|
||||
);
|
||||
this.setState({ deviceData: undefined });
|
||||
});
|
||||
};
|
||||
|
||||
renderDeviceData() {
|
||||
const { deviceData } = this.state;
|
||||
const { width } = this.props;
|
||||
|
||||
if (this.noDevices()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deviceData) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<p></p>
|
||||
<Box bgcolor="info.main" p={1} mt={1} mb={1}>
|
||||
<Typography variant="body1" color="initial">
|
||||
{deviceData.type} Data
|
||||
</Typography>
|
||||
</Box>
|
||||
{!this.noDeviceData() && (
|
||||
<TableContainer>
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'normal'}
|
||||
>
|
||||
<TableBody>
|
||||
{deviceData.data.map((item, i) => (
|
||||
<StyledTableRow
|
||||
key={i}
|
||||
onClick={() => this.sendCommand(item)}
|
||||
>
|
||||
<TableCell padding="checkbox" style={{ width: 18 }}>
|
||||
{item.c && this.props.authenticatedContext.me.admin && (
|
||||
<StyledTooltip
|
||||
title="change value"
|
||||
placement="left-end"
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
size="small"
|
||||
aria-label="Edit"
|
||||
>
|
||||
<EditIcon color="primary" fontSize="small" />
|
||||
</IconButton>
|
||||
</StyledTooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell padding="none" component="th" scope="row">
|
||||
{item.n}
|
||||
</TableCell>
|
||||
<TableCell padding="none" align="right">
|
||||
{formatValue(item.v, item.u)}
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{this.noDeviceData() && (
|
||||
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||
<Typography variant="body1">
|
||||
<i>no data available for this device</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { edit_devicevalue, edit_Sensor } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<br></br>
|
||||
{this.renderDevices()}
|
||||
{this.renderDeviceData()}
|
||||
{this.renderSensorData()}
|
||||
{this.renderAnalogData()}
|
||||
<br></br>
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1} padding={1}>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</Box>
|
||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
onClick={this.onScanDevices}
|
||||
>
|
||||
Scan Devices
|
||||
</FormButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{this.renderScanDevicesDialog()}
|
||||
{edit_devicevalue && (
|
||||
<ValueForm
|
||||
devicevalue={edit_devicevalue}
|
||||
onDoneEditing={this.doneEditingDeviceValue}
|
||||
onCancelEditing={this.cancelEditingDeviceValue}
|
||||
handleValueChange={this.handleDeviceValueChange}
|
||||
/>
|
||||
)}
|
||||
{edit_Sensor && (
|
||||
<SensorForm
|
||||
sensor={edit_Sensor}
|
||||
onDoneEditing={this.doneEditingSensor}
|
||||
onCancelEditing={this.cancelEditingSensor}
|
||||
handleSensorChange={this.handleSensorChange}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(withWidth()(EMSESPDataForm));
|
||||
@@ -1,131 +0,0 @@
|
||||
import { Component } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Link,
|
||||
ListItemAvatar
|
||||
} from '@material-ui/core';
|
||||
import { SectionContent } from '../components';
|
||||
|
||||
import CommentIcon from '@material-ui/icons/CommentTwoTone';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBookTwoTone';
|
||||
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||
import StarIcon from '@material-ui/icons/Star';
|
||||
import DownloadIcon from '@material-ui/icons/GetApp';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
|
||||
import { API_ENDPOINT_ROOT } from '../api';
|
||||
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
|
||||
class EMSESPHelp extends Component {
|
||||
onDownload = (endpoint: string) => {
|
||||
redirectingAuthorizedFetch(API_ENDPOINT_ROOT + 'system/' + endpoint)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error(
|
||||
'Device returned unexpected response code: ' + response.status
|
||||
);
|
||||
})
|
||||
.then((json) => {
|
||||
const a = document.createElement('a');
|
||||
const filename = 'emsesp_system_' + endpoint + '.txt';
|
||||
a.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
a.setAttribute('download', filename);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="EMS-ESP Help" titleGutter>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<MenuBookIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For help and information on the latest updates visit the{' '}
|
||||
<Link href="https://emsesp.github.io/docs" color="primary">
|
||||
{'online documentation'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<CommentIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For live community chat join our{' '}
|
||||
<Link href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||
{'Discord'} server
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<GitHubIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
To report an issue or request a feature go to{' '}
|
||||
<Link
|
||||
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
|
||||
color="primary"
|
||||
>
|
||||
{'GitHub'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
||||
<FormButton
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => this.onDownload('info')}
|
||||
>
|
||||
download system info
|
||||
</FormButton>
|
||||
<FormButton
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => this.onDownload('settings')}
|
||||
>
|
||||
download all settings
|
||||
</FormButton>
|
||||
</Box>
|
||||
|
||||
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
|
||||
<Typography variant="h6">
|
||||
EMS-ESP is free and open-source.
|
||||
<br></br>Please consider supporting this project by giving it a{' '}
|
||||
<StarIcon style={{ color: '#fdff3a' }} /> on our{' '}
|
||||
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
|
||||
{'GitHub page'}
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</Box>
|
||||
<br></br>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EMSESPHelp;
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Tabs, Tab } from '@material-ui/core';
|
||||
|
||||
import { PROJECT_PATH } from '../api';
|
||||
import { MenuAppBar } from '../components';
|
||||
import { AuthenticatedRoute } from '../authentication';
|
||||
|
||||
import EMSESPSettingsController from './EMSESPSettingsController';
|
||||
|
||||
class EMSESP extends Component<RouteComponentProps> {
|
||||
handleTabChange = (path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Settings">
|
||||
<Tabs
|
||||
value={this.props.match.url}
|
||||
onChange={(e, path) => this.handleTabChange(path)}
|
||||
variant="fullWidth"
|
||||
>
|
||||
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path={`/${PROJECT_PATH}/settings`}
|
||||
component={EMSESPSettingsController}
|
||||
/>
|
||||
<Redirect to={`/${PROJECT_PATH}/settings`} />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EMSESP;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPSettingsForm from './EMSESPSettingsForm';
|
||||
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
|
||||
import { EMSESPSettings } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings';
|
||||
|
||||
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
||||
|
||||
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default restController(
|
||||
EMSESP_SETTINGS_ENDPOINT,
|
||||
EMSESPSettingsController
|
||||
);
|
||||
@@ -1,644 +0,0 @@
|
||||
import { Component } from 'react';
|
||||
|
||||
import {
|
||||
ValidatorForm,
|
||||
TextValidator,
|
||||
SelectValidator
|
||||
} from 'react-material-ui-form-validator';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
Typography,
|
||||
Box,
|
||||
Link,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
Grid
|
||||
} from '@material-ui/core';
|
||||
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import {
|
||||
redirectingAuthorizedFetch,
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
BlockFormControlLabel
|
||||
} from '../components';
|
||||
|
||||
import { isIPv4, optional, isHostname, or } from '../validators';
|
||||
|
||||
import { EMSESPSettings } from './EMSESPtypes';
|
||||
|
||||
import { boardProfileSelectItems } from './EMSESPBoardProfiles';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile';
|
||||
|
||||
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
|
||||
AuthenticatedContextProps &
|
||||
WithWidthProps;
|
||||
|
||||
interface EMSESPSettingsFormState {
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
class EMSESPSettingsForm extends Component<EMSESPSettingsFormProps> {
|
||||
state: EMSESPSettingsFormState = {
|
||||
processing: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule(
|
||||
'isOptionalIPorHost',
|
||||
optional(or(isIPv4, isHostname))
|
||||
);
|
||||
}
|
||||
|
||||
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { data, setData } = this.props;
|
||||
setData({
|
||||
...data,
|
||||
board_profile: event.target.value
|
||||
});
|
||||
|
||||
if (event.target.value === 'CUSTOM') return;
|
||||
|
||||
this.setState({ processing: true });
|
||||
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code: event.target.value }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error('Unexpected response code: ' + response.status);
|
||||
})
|
||||
.then((json) => {
|
||||
this.props.enqueueSnackbar('Profile loaded', { variant: 'success' });
|
||||
setData({
|
||||
...data,
|
||||
led_gpio: json.led_gpio,
|
||||
dallas_gpio: json.dallas_gpio,
|
||||
rx_gpio: json.rx_gpio,
|
||||
tx_gpio: json.tx_gpio,
|
||||
pbutton_gpio: json.pbutton_gpio,
|
||||
phy_type: json.phy_type,
|
||||
board_profile: event.target.value
|
||||
});
|
||||
this.setState({ processing: false });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
error.message || 'Problem fetching board profile',
|
||||
{ variant: 'warning' }
|
||||
);
|
||||
this.setState({ processing: false });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, saveData, handleValueChange } = this.props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData}>
|
||||
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
|
||||
<Typography variant="body1">
|
||||
<i>
|
||||
visit the
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://emsesp.github.io/docs/#/Configure-firmware?id=ems-esp-settings"
|
||||
color="primary"
|
||||
>
|
||||
{'online documentation'}
|
||||
</Link>
|
||||
for details explaining each setting
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary">
|
||||
EMS Bus
|
||||
</Typography>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={5}>
|
||||
<SelectValidator
|
||||
name="tx_mode"
|
||||
label="Tx Mode"
|
||||
value={data.tx_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('tx_mode')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>Off</MenuItem>
|
||||
<MenuItem value={1}>EMS</MenuItem>
|
||||
<MenuItem value={2}>EMS+</MenuItem>
|
||||
<MenuItem value={3}>HT3</MenuItem>
|
||||
<MenuItem value={4}>Hardware</MenuItem>
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<SelectValidator
|
||||
name="ems_bus_id"
|
||||
label="Bus ID"
|
||||
value={data.ems_bus_id}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('ems_bus_id')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
|
||||
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
|
||||
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
|
||||
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary">
|
||||
Board Profile
|
||||
</Typography>
|
||||
|
||||
<Box color="warning.main" p={0} mt={0} mb={0}>
|
||||
<Typography variant="body2">
|
||||
<i>
|
||||
Select a pre-configured board layout to automatically set the GPIO
|
||||
pins. Select "Custom..." to view or manually edit the values.
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<SelectValidator
|
||||
name="board_profile"
|
||||
label="Board Profile"
|
||||
value={data.board_profile}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={this.changeBoardProfile}
|
||||
margin="normal"
|
||||
>
|
||||
{boardProfileSelectItems()}
|
||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||
Custom...
|
||||
</MenuItem>
|
||||
</SelectValidator>
|
||||
|
||||
{data.board_profile === 'CUSTOM' && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="rx_gpio"
|
||||
label="Rx GPIO"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.rx_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange('rx_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="tx_gpio"
|
||||
label="Tx GPIO"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.tx_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange('tx_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="pbutton_gpio"
|
||||
label="Button GPIO"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.pbutton_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange('pbutton_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="dallas_gpio"
|
||||
label="Dallas GPIO (0=disabled)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.dallas_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange('dallas_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:40',
|
||||
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||
]}
|
||||
errorMessages={[
|
||||
'GPIO is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 40',
|
||||
'Not a valid GPIO'
|
||||
]}
|
||||
name="led_gpio"
|
||||
label="LED GPIO (0=disabled)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.led_gpio}
|
||||
type="number"
|
||||
onChange={handleValueChange('led_gpio')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<SelectValidator
|
||||
name="phy_type"
|
||||
label="PHY Module Type"
|
||||
value={data.phy_type}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('phy_type')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={0}>No Ethernet</MenuItem>
|
||||
<MenuItem value={1}>LAN8720</MenuItem>
|
||||
<MenuItem value={2}>TLK110</MenuItem>
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary">
|
||||
General Options
|
||||
</Typography>
|
||||
|
||||
{data.led_gpio !== 0 && (
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.hide_led}
|
||||
onChange={handleValueChange('hide_led')}
|
||||
value="hide_led"
|
||||
/>
|
||||
}
|
||||
label="Hide LED"
|
||||
/>
|
||||
)}
|
||||
|
||||
{data.dallas_gpio !== 0 && (
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.dallas_parasite}
|
||||
onChange={handleValueChange('dallas_parasite')}
|
||||
value="dallas_parasite"
|
||||
/>
|
||||
}
|
||||
label="Use Dallas Sensor parasite power"
|
||||
/>
|
||||
)}
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.analog_enabled}
|
||||
onChange={handleValueChange('analog_enabled')}
|
||||
value="analog_enabled"
|
||||
/>
|
||||
}
|
||||
label="Enable ADC"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.low_clock}
|
||||
onChange={handleValueChange('low_clock')}
|
||||
value="low_clock"
|
||||
/>
|
||||
}
|
||||
label="Run at a lower CPU clock speed"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.notoken_api}
|
||||
onChange={handleValueChange('notoken_api')}
|
||||
value="notoken_api"
|
||||
/>
|
||||
}
|
||||
label="Bypass Access Token authorization on API calls"
|
||||
/>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_timer}
|
||||
onChange={handleValueChange('shower_timer')}
|
||||
value="shower_timer"
|
||||
/>
|
||||
}
|
||||
label="Enable Shower Timer"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_alert}
|
||||
onChange={handleValueChange('shower_alert')}
|
||||
value="shower_alert"
|
||||
/>
|
||||
}
|
||||
label="Enable Shower Alert"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary">
|
||||
Formatting Options
|
||||
</Typography>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={4}>
|
||||
<SelectValidator
|
||||
name="bool_format"
|
||||
label="Boolean Format"
|
||||
value={data.bool_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('bool_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
||||
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
|
||||
<MenuItem value={3}>true/false</MenuItem>
|
||||
<MenuItem value={4}>1/0</MenuItem>
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<SelectValidator
|
||||
name="enum_format"
|
||||
label="Enum Format"
|
||||
value={data.enum_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('enum_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>Text</MenuItem>
|
||||
<MenuItem value={2}>Number</MenuItem>
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<SelectValidator
|
||||
name="dallas_format"
|
||||
label="Dallas Sensor Format"
|
||||
value={data.dallas_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('dallas_format')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={1}>ID</MenuItem>
|
||||
<MenuItem value={2}>Number</MenuItem>
|
||||
<MenuItem value={3}>Name</MenuItem>
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<br></br>
|
||||
<Typography variant="h6" color="primary">
|
||||
Syslog
|
||||
</Typography>
|
||||
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.syslog_enabled}
|
||||
onChange={handleValueChange('syslog_enabled')}
|
||||
value="syslog_enabled"
|
||||
/>
|
||||
}
|
||||
label="Enable Syslog"
|
||||
/>
|
||||
|
||||
{data.syslog_enabled && (
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justifyContent="flex-start"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Grid item xs={5}>
|
||||
<TextValidator
|
||||
validators={['isOptionalIPorHost']}
|
||||
errorMessages={['Not a valid IPv4 address or hostname']}
|
||||
name="syslog_host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_host}
|
||||
onChange={handleValueChange('syslog_host')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
'Port is required',
|
||||
'Must be a number',
|
||||
'Must be greater than 0 ',
|
||||
'Max value is 65535'
|
||||
]}
|
||||
name="syslog_port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_port}
|
||||
type="number"
|
||||
onChange={handleValueChange('syslog_port')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<SelectValidator
|
||||
name="syslog_level"
|
||||
label="Log Level"
|
||||
value={data.syslog_level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('syslog_level')}
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value={-1}>OFF</MenuItem>
|
||||
<MenuItem value={3}>ERR</MenuItem>
|
||||
<MenuItem value={5}>NOTICE</MenuItem>
|
||||
<MenuItem value={6}>INFO</MenuItem>
|
||||
<MenuItem value={7}>DEBUG</MenuItem>
|
||||
<MenuItem value={8}>ALL</MenuItem>
|
||||
</SelectValidator>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextValidator
|
||||
validators={[
|
||||
'required',
|
||||
'isNumber',
|
||||
'minNumber:0',
|
||||
'maxNumber:65535'
|
||||
]}
|
||||
errorMessages={[
|
||||
'Syslog Mark is required',
|
||||
'Must be a number',
|
||||
'Must be 0 or higher',
|
||||
'Max value is 10'
|
||||
]}
|
||||
name="syslog_mark_interval"
|
||||
label="Mark Interval (seconds, 0=off)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_mark_interval}
|
||||
type="number"
|
||||
onChange={handleValueChange('syslog_mark_interval')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Grid>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.trace_raw}
|
||||
onChange={handleValueChange('trace_raw')}
|
||||
value="trace_raw"
|
||||
/>
|
||||
}
|
||||
label="Output EMS telegrams as hexadecimal bytes"
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<br></br>
|
||||
<FormActions>
|
||||
<FormButton
|
||||
startIcon={<SaveIcon />}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withAuthenticatedContext(withWidth()(EMSESPSettingsForm));
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes';
|
||||
|
||||
export const isConnected = ({ status }: EMSESPStatus) =>
|
||||
status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||
|
||||
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.warning.main;
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return theme.palette.success.main;
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return theme.palette.error.main;
|
||||
default:
|
||||
return theme.palette.warning.main;
|
||||
}
|
||||
};
|
||||
|
||||
export const busStatus = ({ status }: EMSESPStatus) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||
return 'Connected';
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return 'Tx Errors';
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return 'Disconnected';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const qualityHighlight = (value: number, theme: Theme) => {
|
||||
if (value >= 95) {
|
||||
return theme.palette.success.main;
|
||||
}
|
||||
|
||||
return theme.palette.error.main;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
restController,
|
||||
RestControllerProps,
|
||||
RestFormLoader,
|
||||
SectionContent
|
||||
} from '../components';
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPStatusForm from './EMSESPStatusForm';
|
||||
import { EMSESPStatus } from './EMSESPtypes';
|
||||
|
||||
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'emsespStatus';
|
||||
|
||||
type EMSESPStatusControllerProps = RestControllerProps<EMSESPStatus>;
|
||||
|
||||
class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title="EMS Status" titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={(formProps) => <EMSESPStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default restController(EMSESP_STATUS_ENDPOINT, EMSESPStatusController);
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
withWidth,
|
||||
WithWidthProps,
|
||||
isWidthDown
|
||||
} from '@material-ui/core';
|
||||
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
|
||||
import {
|
||||
RestFormProps,
|
||||
FormActions,
|
||||
FormButton,
|
||||
HighlightAvatar
|
||||
} from '../components';
|
||||
|
||||
import { busStatus, busStatusHighlight, isConnected } from './EMSESPStatus';
|
||||
|
||||
import { EMSESPStatus } from './EMSESPtypes';
|
||||
|
||||
function formatNumber(num: number) {
|
||||
return new Intl.NumberFormat().format(num);
|
||||
}
|
||||
|
||||
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> &
|
||||
WithTheme &
|
||||
WithWidthProps;
|
||||
|
||||
class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
createListItems() {
|
||||
const { data, theme, width } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<HighlightAvatar color={busStatusHighlight(data, theme)}>
|
||||
<DeviceHubIcon />
|
||||
</HighlightAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Connection Status"
|
||||
secondary={busStatus(data)}
|
||||
/>
|
||||
</ListItem>
|
||||
{isConnected(data) && (
|
||||
<TableContainer>
|
||||
<Table
|
||||
size="small"
|
||||
padding={isWidthDown('xs', width!) ? 'none' : 'normal'}
|
||||
>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Telegrams Received</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatNumber(data.rx_received)} (quality{' '}
|
||||
{data.rx_quality}%)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Telegrams Sent</TableCell>
|
||||
<TableCell align="right">
|
||||
{formatNumber(data.tx_sent)} (quality {data.tx_quality}
|
||||
%)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<List>{this.createListItems()}</List>
|
||||
<FormActions>
|
||||
<FormButton
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={this.props.loadData}
|
||||
>
|
||||
Refresh
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(withWidth()(EMSESPStatusForm));
|
||||
@@ -1,120 +0,0 @@
|
||||
export interface EMSESPSettings {
|
||||
tx_mode: number;
|
||||
ems_bus_id: number;
|
||||
syslog_enabled: boolean;
|
||||
syslog_level: number;
|
||||
syslog_mark_interval: number;
|
||||
syslog_host: string;
|
||||
syslog_port: number;
|
||||
master_thermostat: number;
|
||||
shower_timer: boolean;
|
||||
shower_alert: boolean;
|
||||
rx_gpio: number;
|
||||
tx_gpio: number;
|
||||
phy_type: number;
|
||||
dallas_gpio: number;
|
||||
dallas_parasite: boolean;
|
||||
led_gpio: number;
|
||||
hide_led: boolean;
|
||||
low_clock: boolean;
|
||||
notoken_api: boolean;
|
||||
analog_enabled: boolean;
|
||||
pbutton_gpio: number;
|
||||
trace_raw: boolean;
|
||||
board_profile: string;
|
||||
bool_format: number;
|
||||
dallas_format: number;
|
||||
enum_format: number;
|
||||
}
|
||||
|
||||
export enum busConnectionStatus {
|
||||
BUS_STATUS_CONNECTED = 0,
|
||||
BUS_STATUS_TX_ERRORS = 1,
|
||||
BUS_STATUS_OFFLINE = 2
|
||||
}
|
||||
|
||||
export interface EMSESPStatus {
|
||||
status: busConnectionStatus;
|
||||
rx_received: number;
|
||||
tx_sent: number;
|
||||
rx_quality: number;
|
||||
tx_quality: number;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
i: number; // id
|
||||
t: string; // type
|
||||
b: string; // brand
|
||||
n: string; // name
|
||||
d: number; // deviceid
|
||||
p: number; // productid
|
||||
v: string; // version
|
||||
}
|
||||
|
||||
export interface Sensor {
|
||||
n: number; // np
|
||||
i: string; // id
|
||||
t: number; // temp
|
||||
o: number; // offset
|
||||
}
|
||||
|
||||
export interface EMSESPData {
|
||||
devices: Device[];
|
||||
sensors: Sensor[];
|
||||
analog: number;
|
||||
}
|
||||
|
||||
export interface DeviceValue {
|
||||
v: any; // value, in any format
|
||||
u: number; // uom
|
||||
n: string; // name
|
||||
c: string; // command
|
||||
l: string[]; // list
|
||||
}
|
||||
|
||||
export interface EMSESPDeviceData {
|
||||
type: string;
|
||||
data: DeviceValue[];
|
||||
}
|
||||
|
||||
export enum DeviceValueUOM {
|
||||
NONE = 0,
|
||||
DEGREES,
|
||||
PERCENT,
|
||||
LMIN,
|
||||
KWH,
|
||||
WH,
|
||||
HOURS,
|
||||
MINUTES,
|
||||
UA,
|
||||
BAR,
|
||||
KW,
|
||||
W,
|
||||
KB,
|
||||
SECONDS,
|
||||
DBM,
|
||||
MV,
|
||||
TIMES,
|
||||
OCLOCK
|
||||
}
|
||||
|
||||
export const DeviceValueUOM_s = [
|
||||
'',
|
||||
'°C',
|
||||
'%',
|
||||
'l/min',
|
||||
'kWh',
|
||||
'Wh',
|
||||
'hours',
|
||||
'minutes',
|
||||
'uA',
|
||||
'bar',
|
||||
'kW',
|
||||
'W',
|
||||
'KB',
|
||||
'second',
|
||||
'dBm',
|
||||
'mV',
|
||||
'time',
|
||||
"o'clock"
|
||||
];
|
||||
27
interface/src/project/Help.tsx
Normal file
27
interface/src/project/Help.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { FC } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
|
||||
|
||||
import HelpInformation from './HelpInformation';
|
||||
|
||||
const Help: FC = () => {
|
||||
useLayoutTitle('Help');
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="information" label="EMS-ESP Help" />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="information" element={<HelpInformation />} />
|
||||
<Route path="/*" element={<Navigate replace to="information" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
||||
171
interface/src/project/HelpInformation.tsx
Normal file
171
interface/src/project/HelpInformation.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { FC, useContext } from 'react';
|
||||
|
||||
import { Typography, Button, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@mui/material';
|
||||
|
||||
import { SectionContent, ButtonRow } from '../components';
|
||||
|
||||
import { AuthenticatedContext } from '../contexts/authentication';
|
||||
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import CommentIcon from '@mui/icons-material/CommentTwoTone';
|
||||
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import StarIcon from '@mui/icons-material/Star';
|
||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
|
||||
import { extractErrorMessage } from '../utils';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
|
||||
const HelpInformation: FC = () => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
const onDownload = async (endpoint: string) => {
|
||||
try {
|
||||
const response = await EMSESP.API({
|
||||
device: 'system',
|
||||
entity: endpoint,
|
||||
id: 0
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
enqueueSnackbar('API call failed', { variant: 'error' });
|
||||
} else {
|
||||
const json = response.data;
|
||||
const a = document.createElement('a');
|
||||
const filename = 'emsesp_' + endpoint + '.txt';
|
||||
a.href = URL.createObjectURL(
|
||||
new Blob([JSON.stringify(json, null, 2)], {
|
||||
type: 'text/plain'
|
||||
})
|
||||
);
|
||||
a.setAttribute('download', filename);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
enqueueSnackbar('File downloaded', { variant: 'info' });
|
||||
}
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Application Information & Support" titleGutter>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<TuneIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For a help on each of the Application Settings see
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://emsesp.github.io/docs/#/Configure-firmware?id=ems-esp-settings"
|
||||
color="primary"
|
||||
>
|
||||
{'Configuring EMS-ESP'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<MenuBookIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For general information about EMS-ESP visit the online
|
||||
<Link target="_blank" href="https://emsesp.github.io/docs" color="primary">
|
||||
{'Documentation'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<CommentIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
For live community chat join our
|
||||
<Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||
{'Discord'}
|
||||
</Link>
|
||||
server
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<GitHubIcon />
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
To report an issue or request a feature, please do via
|
||||
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">
|
||||
{'GitHub'}
|
||||
</Link>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{me.admin && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Export Data
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography variant="body2">
|
||||
Download the current system information, application settings and any customizations using the buttons
|
||||
below.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => onDownload('info')}
|
||||
>
|
||||
system info
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => onDownload('settings')}
|
||||
>
|
||||
settings
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<DownloadIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => onDownload('customizations')}
|
||||
>
|
||||
customizations
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box bgcolor="secondary.info" border={1} p={1} mt={4}>
|
||||
<Typography align="center" variant="h6">
|
||||
EMS-ESP is a free and open-source project.
|
||||
<br></br>Please consider supporting us by giving it a
|
||||
<StarIcon style={{ color: '#fdff3a' }} /> on
|
||||
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
|
||||
{'GitHub'}
|
||||
</Link>
|
||||
!
|
||||
</Typography>
|
||||
</Box>
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpInformation;
|
||||
@@ -1,54 +1,31 @@
|
||||
import { Component } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { FC, useContext } from 'react';
|
||||
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
|
||||
import { List } from '@mui/material';
|
||||
|
||||
import TuneIcon from '@material-ui/icons/Tune';
|
||||
import DashboardIcon from '@material-ui/icons/Dashboard';
|
||||
import { AuthenticatedContext } from '../contexts/authentication';
|
||||
|
||||
import {
|
||||
withAuthenticatedContext,
|
||||
AuthenticatedContextProps
|
||||
} from '../authentication';
|
||||
import { PROJECT_PATH } from '../api/env';
|
||||
|
||||
type ProjectProps = AuthenticatedContextProps & RouteComponentProps;
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import LayoutMenuItem from '../components/layout/LayoutMenuItem';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
|
||||
class ProjectMenu extends Component<ProjectProps> {
|
||||
render() {
|
||||
const { authenticatedContext } = this.props;
|
||||
const path = this.props.match.url;
|
||||
return (
|
||||
<List>
|
||||
<ListItem
|
||||
to="/ems-esp/"
|
||||
selected={
|
||||
path.startsWith('/ems-esp/status') ||
|
||||
path.startsWith('/ems-esp/data') ||
|
||||
path.startsWith('/ems-esp/help')
|
||||
}
|
||||
button
|
||||
component={Link}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<DashboardIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Dashboard" />
|
||||
</ListItem>
|
||||
<ListItem
|
||||
to="/ems-esp/settings"
|
||||
selected={path.startsWith('/ems-esp/settings')}
|
||||
button
|
||||
component={Link}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<TuneIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItem>
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
const ProjectMenu: FC = () => {
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
|
||||
export default withRouter(withAuthenticatedContext(ProjectMenu));
|
||||
return (
|
||||
<List>
|
||||
<LayoutMenuItem icon={DashboardIcon} label="Dashboard" to={`/${PROJECT_PATH}/dashboard`} />
|
||||
<LayoutMenuItem
|
||||
icon={TuneIcon}
|
||||
label="Settings"
|
||||
to={`/${PROJECT_PATH}/settings`}
|
||||
disabled={!authenticatedContext.me.admin}
|
||||
/>
|
||||
<LayoutMenuItem icon={InfoIcon} label="Help" to={`/${PROJECT_PATH}/help`} />
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectMenu;
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
import { Component } from 'react';
|
||||
import { Redirect, Switch } from 'react-router';
|
||||
import { FC } from 'react';
|
||||
import { Navigate, Routes, Route } from 'react-router-dom';
|
||||
|
||||
import { AuthenticatedRoute } from '../authentication';
|
||||
import { RequireAdmin } from '../components';
|
||||
|
||||
import EMSESPDashboard from './EMSESPDashboard';
|
||||
import EMSESPSettings from './EMSESPSettings';
|
||||
import Dashboard from './Dashboard';
|
||||
import Settings from './Settings';
|
||||
import Help from './Help';
|
||||
|
||||
class ProjectRouting extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/status/*"
|
||||
component={EMSESPDashboard}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/settings"
|
||||
component={EMSESPSettings}
|
||||
/>
|
||||
<AuthenticatedRoute
|
||||
exact
|
||||
path="/ems-esp/*"
|
||||
component={EMSESPDashboard}
|
||||
/>
|
||||
{/*
|
||||
* The redirect below caters for the default project route and redirecting invalid paths.
|
||||
* The "to" property must match one of the routes above for this to work correctly.
|
||||
*/}
|
||||
<Redirect to={`/ems-esp/status`} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
const ProjectRouting: FC = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="dashboard/*" element={<Dashboard />} />
|
||||
<Route
|
||||
path="settings/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Settings />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="help/*" element={<Help />} />
|
||||
<Route path="/*" element={<Navigate to="dashboard/data" />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectRouting;
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { ValidatorForm, TextValidator } from 'react-material-ui-form-validator';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
import { Sensor } from './EMSESPtypes';
|
||||
|
||||
interface SensorFormProps {
|
||||
sensor: Sensor;
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
handleSensorChange: (
|
||||
data: keyof Sensor
|
||||
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
class SensorForm extends React.Component<SensorFormProps> {
|
||||
formRef: RefObject<any> = React.createRef();
|
||||
|
||||
submit = () => {
|
||||
this.formRef.current.submit();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
sensor,
|
||||
handleSensorChange,
|
||||
onDoneEditing,
|
||||
onCancelEditing
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||
<Dialog
|
||||
maxWidth="xs"
|
||||
onClose={onCancelEditing}
|
||||
aria-labelledby="user-form-dialog-title"
|
||||
open
|
||||
>
|
||||
<DialogTitle id="user-form-dialog-title">
|
||||
Editing Sensor #{sensor.n}
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextValidator
|
||||
validators={['matchRegexp:^([a-zA-Z0-9_.-]{0,19})$']}
|
||||
errorMessages={['Not a valid name']}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={sensor.i}
|
||||
onChange={handleSensorChange('i')}
|
||||
margin="normal"
|
||||
label="Name"
|
||||
name="id"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['isFloat', 'minFloat:-5', 'maxFloat:5']}
|
||||
errorMessages={[
|
||||
'Must be a number',
|
||||
'Must be greater than -5',
|
||||
'Max value is +5'
|
||||
]}
|
||||
label="Custom Offset (°C)"
|
||||
name="offset"
|
||||
type="number"
|
||||
value={sensor.o}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
InputProps={{ inputProps: { min: '-5', max: '5', step: '0.1' } }}
|
||||
margin="normal"
|
||||
onChange={handleSensorChange('o')}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onCancelEditing}
|
||||
>
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={this.submit}
|
||||
>
|
||||
Done
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SensorForm;
|
||||
30
interface/src/project/Settings.tsx
Normal file
30
interface/src/project/Settings.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { FC } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
|
||||
|
||||
import SettingsApplication from './SettingsApplication';
|
||||
import SettingsCustomization from './SettingsCustomization';
|
||||
|
||||
const Settings: FC = () => {
|
||||
useLayoutTitle('Settings');
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="application" label="Application Settings" />
|
||||
<Tab value="customization" label="Customization" />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="application" element={<SettingsApplication />} />
|
||||
<Route path="customization" element={<SettingsCustomization />} />
|
||||
<Route path="/*" element={<Navigate replace to="application" />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
541
interface/src/project/SettingsApplication.tsx
Normal file
541
interface/src/project/SettingsApplication.tsx
Normal file
@@ -0,0 +1,541 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider } from '@mui/material';
|
||||
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
|
||||
import { validate } from '../validators';
|
||||
import { createSettingsValidator } from './validators';
|
||||
|
||||
import {
|
||||
SectionContent,
|
||||
FormLoader,
|
||||
BlockFormControlLabel,
|
||||
ValidatedTextField,
|
||||
ButtonRow,
|
||||
MessageBox
|
||||
} from '../components';
|
||||
import { numberValue, extractErrorMessage, updateValue, useRest } from '../utils';
|
||||
|
||||
import * as EMSESP from './api';
|
||||
import { Settings, BOARD_PROFILES } from './types';
|
||||
|
||||
export function boardProfileSelectItems() {
|
||||
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||
<MenuItem key={code} value={code}>
|
||||
{BOARD_PROFILES[code]}
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
|
||||
const SettingsApplication: FC = () => {
|
||||
const { loadData, saveData, saving, setData, data, errorMessage, restartNeeded } = useRest<Settings>({
|
||||
read: EMSESP.readSettings,
|
||||
update: EMSESP.writeSettings
|
||||
});
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const updateFormValue = updateValue(setData);
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||
const [processingBoard, setProcessingBoard] = useState<boolean>(false);
|
||||
|
||||
const updateBoardProfile = async (board_profile: string) => {
|
||||
setProcessingBoard(true);
|
||||
try {
|
||||
const response = await EMSESP.getBoardProfile({ board_profile: board_profile });
|
||||
if (data) {
|
||||
setData({
|
||||
...data,
|
||||
board_profile: board_profile,
|
||||
led_gpio: response.data.led_gpio,
|
||||
dallas_gpio: response.data.dallas_gpio,
|
||||
rx_gpio: response.data.rx_gpio,
|
||||
tx_gpio: response.data.tx_gpio,
|
||||
pbutton_gpio: response.data.pbutton_gpio,
|
||||
phy_type: response.data.phy_type,
|
||||
eth_power: response.data.eth_power,
|
||||
eth_phy_addr: response.data.eth_phy_addr,
|
||||
eth_clock_mode: response.data.eth_clock_mode
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching board profile'), { variant: 'error' });
|
||||
} finally {
|
||||
setProcessingBoard(false);
|
||||
}
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
if (!data) {
|
||||
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
|
||||
}
|
||||
|
||||
const validateAndSubmit = async () => {
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(createSettingsValidator(data), data);
|
||||
saveData();
|
||||
} catch (errors: any) {
|
||||
setFieldErrors(errors);
|
||||
}
|
||||
};
|
||||
|
||||
const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const board_profile = event.target.value;
|
||||
if (board_profile === 'CUSTOM') {
|
||||
setData({
|
||||
...data,
|
||||
board_profile: board_profile
|
||||
});
|
||||
} else {
|
||||
updateBoardProfile(board_profile);
|
||||
}
|
||||
};
|
||||
|
||||
const restart = async () => {
|
||||
validateAndSubmit();
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Interface Board Profile
|
||||
</Typography>
|
||||
<Box color="warning.main">
|
||||
<Typography variant="body2">
|
||||
Select a pre-configured interface board profile from the list below or choose "Custom" to configure your own
|
||||
hardware settings.
|
||||
</Typography>
|
||||
</Box>
|
||||
<ValidatedTextField
|
||||
name="board_profile"
|
||||
label="Board Profile"
|
||||
value={data.board_profile}
|
||||
disabled={processingBoard}
|
||||
variant="outlined"
|
||||
onChange={changeBoardProfile}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
{boardProfileSelectItems()}
|
||||
<Divider />
|
||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||
Custom…
|
||||
</MenuItem>
|
||||
</ValidatedTextField>
|
||||
{data.board_profile === 'CUSTOM' && (
|
||||
<>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="rx_gpio"
|
||||
label="Rx GPIO"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.rx_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="tx_gpio"
|
||||
label="Tx GPIO"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.tx_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="pbutton_gpio"
|
||||
label="Button GPIO"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.pbutton_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="dallas_gpio"
|
||||
label="Temperature GPIO (0=disabled)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.dallas_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="led_gpio"
|
||||
label="LED GPIO (0=disabled)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.led_gpio)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<ValidatedTextField
|
||||
name="phy_type"
|
||||
label="Eth PHY Type"
|
||||
disabled={saving}
|
||||
value={data.phy_type}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>No Ethernet Module</MenuItem>
|
||||
<MenuItem value={1}>LAN8720</MenuItem>
|
||||
<MenuItem value={2}>TLK110</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
{data.phy_type !== 0 && (
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="eth_power"
|
||||
label="Eth Power GPIO (-1=disabled)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.eth_power)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="eth_phy_addr"
|
||||
label="Eth I²C-address"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={numberValue(data.eth_phy_addr)}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<ValidatedTextField
|
||||
name="eth_clock_mode"
|
||||
label="Eth Clock Mode"
|
||||
disabled={saving}
|
||||
value={data.eth_clock_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>GPIO0_IN</MenuItem>
|
||||
<MenuItem value={1}>GPIO0_OUT</MenuItem>
|
||||
<MenuItem value={2}>GPIO16_OUT</MenuItem>
|
||||
<MenuItem value={3}>GPIO17_OUT</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Typography variant="h6" color="primary">
|
||||
EMS Bus Settings
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="tx_mode"
|
||||
label="Tx Mode"
|
||||
disabled={saving}
|
||||
value={data.tx_mode}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0}>Off</MenuItem>
|
||||
<MenuItem value={1}>EMS</MenuItem>
|
||||
<MenuItem value={2}>EMS+</MenuItem>
|
||||
<MenuItem value={3}>HT3</MenuItem>
|
||||
<MenuItem value={4}>Hardware</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="ems_bus_id"
|
||||
label="Bus ID"
|
||||
disabled={saving}
|
||||
value={data.ems_bus_id}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
|
||||
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
|
||||
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
|
||||
<MenuItem value={0x12}>Alarm Module (0x12)</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
General Options
|
||||
</Typography>
|
||||
{data.led_gpio !== 0 && (
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
|
||||
label="Hide LED"
|
||||
disabled={saving}
|
||||
/>
|
||||
)}
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.telnet_enabled} onChange={updateFormValue} name="telnet_enabled" />}
|
||||
label="Enable Telnet Console"
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.analog_enabled} onChange={updateFormValue} name="analog_enabled" />}
|
||||
label="Enable Analog Sensors"
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />}
|
||||
label="Convert temperature values to Fahrenheit"
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.low_clock} onChange={updateFormValue} name="low_clock" />}
|
||||
label="Underclock CPU speed"
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.notoken_api} onChange={updateFormValue} name="notoken_api" />}
|
||||
label="Bypass Access Token authorization on API calls"
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
|
||||
label="Enable Read only mode (blocks all outgoing EMS Tx write commands)"
|
||||
disabled={saving}
|
||||
/>
|
||||
<Grid container spacing={0} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.shower_timer} onChange={updateFormValue} name="shower_timer" />}
|
||||
label="Enable Shower Timer"
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.shower_alert} onChange={updateFormValue} name="shower_alert" />}
|
||||
label="Enable Shower Alert"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Formatting Options
|
||||
</Typography>
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="bool_format"
|
||||
label="Boolean Format"
|
||||
value={data.bool_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
||||
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
|
||||
<MenuItem value={3}>"true"/"false"</MenuItem>
|
||||
<MenuItem value={4}>true/false</MenuItem>
|
||||
<MenuItem value={5}>"1"/"0"</MenuItem>
|
||||
<MenuItem value={6}>1/0</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
name="enum_format"
|
||||
label="Enum Format"
|
||||
value={data.enum_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
>
|
||||
<MenuItem value={1}>Value</MenuItem>
|
||||
<MenuItem value={2}>Index</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{data.dallas_gpio !== 0 && (
|
||||
<>
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Temperature Sensors
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.dallas_parasite} onChange={updateFormValue} name="dallas_parasite" />}
|
||||
label="Enable parasite power"
|
||||
disabled={saving}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||
Logging
|
||||
</Typography>
|
||||
<BlockFormControlLabel
|
||||
control={<Checkbox checked={data.trace_raw} onChange={updateFormValue} name="trace_raw" />}
|
||||
label="Log EMS telegrams in hexadecimal"
|
||||
disabled={saving}
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.syslog_enabled}
|
||||
onChange={updateFormValue}
|
||||
name="syslog_enabled"
|
||||
disabled={saving}
|
||||
/>
|
||||
}
|
||||
label="Enable Syslog"
|
||||
/>
|
||||
{data.syslog_enabled && (
|
||||
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
|
||||
<Grid item xs={5}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_host"
|
||||
label="Host"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_host}
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_port"
|
||||
label="Port"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_port}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={5}>
|
||||
<ValidatedTextField
|
||||
name="syslog_level"
|
||||
label="Log Level"
|
||||
value={data.syslog_level}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
select
|
||||
disabled={saving}
|
||||
>
|
||||
<MenuItem value={-1}>OFF</MenuItem>
|
||||
<MenuItem value={3}>ERR</MenuItem>
|
||||
<MenuItem value={5}>NOTICE</MenuItem>
|
||||
<MenuItem value={6}>INFO</MenuItem>
|
||||
<MenuItem value={7}>DEBUG</MenuItem>
|
||||
<MenuItem value={9}>ALL</MenuItem>
|
||||
</ValidatedTextField>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="syslog_mark_interval"
|
||||
label="Mark Interval (seconds, 0=off)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_mark_interval}
|
||||
type="number"
|
||||
onChange={updateFormValue}
|
||||
margin="normal"
|
||||
disabled={saving}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{restartNeeded && (
|
||||
<MessageBox my={2} level="warning" message="EMS-ESP needs to be restarted to apply changed system settings">
|
||||
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
|
||||
Restart
|
||||
</Button>
|
||||
</MessageBox>
|
||||
)}
|
||||
{!restartNeeded && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<SaveIcon />}
|
||||
disabled={saving}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={validateAndSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionContent title="Application Settings" titleGutter>
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsApplication;
|
||||
281
interface/src/project/SettingsCustomization.tsx
Normal file
281
interface/src/project/SettingsCustomization.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import { FC, useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
Box,
|
||||
MenuItem,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle
|
||||
} 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 CloseIcon from '@mui/icons-material/Close';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
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: 11
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 11
|
||||
}
|
||||
}));
|
||||
|
||||
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);
|
||||
|
||||
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">
|
||||
Customize which entities to exclude from all all services (MQTT, API). This will have immediate effect.
|
||||
</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 exclude_entities = deviceEntities.filter((de) => de.x).map((new_de) => new_de.i);
|
||||
try {
|
||||
const response = await EMSESP.writeExcludeEntities({
|
||||
id: selectedDevice,
|
||||
entity_ids: exclude_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 toggleDeviceEntity = (id: number) => {
|
||||
setDeviceEntities(
|
||||
deviceEntities.map((o) => {
|
||||
if (o.i === id) {
|
||||
return { ...o, x: !o.x };
|
||||
}
|
||||
return o;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<StyledTableCell>
|
||||
({deviceEntities.reduce((a, v) => (v.x ? a + 1 : a), 0)}/{deviceEntities.length})
|
||||
</StyledTableCell>
|
||||
<StyledTableCell align="left">ENTITY NAME</StyledTableCell>
|
||||
<StyledTableCell>CODE</StyledTableCell>
|
||||
<StyledTableCell align="right">VALUE</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{deviceEntities.map((de) => (
|
||||
<TableRow
|
||||
key={de.i}
|
||||
onClick={() => toggleDeviceEntity(de.i)}
|
||||
sx={de.x ? { backgroundColor: '#f8696b' } : { backgroundColor: 'black' }}
|
||||
>
|
||||
<StyledTableCell padding="checkbox">{de.x && <CloseIcon fontSize="small" />}</StyledTableCell>
|
||||
<StyledTableCell component="th" scope="row">
|
||||
{de.n}
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>{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;
|
||||
@@ -1,115 +0,0 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
Typography,
|
||||
FormHelperText,
|
||||
OutlinedInput,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
MenuItem
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { FormButton } from '../components';
|
||||
import { DeviceValue, DeviceValueUOM_s } from './EMSESPtypes';
|
||||
|
||||
interface ValueFormProps {
|
||||
devicevalue: DeviceValue;
|
||||
onDoneEditing: () => void;
|
||||
onCancelEditing: () => void;
|
||||
handleValueChange: (
|
||||
data: keyof DeviceValue
|
||||
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
class ValueForm extends React.Component<ValueFormProps> {
|
||||
formRef: RefObject<any> = React.createRef();
|
||||
|
||||
submit = () => {
|
||||
this.formRef.current.submit();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
devicevalue,
|
||||
handleValueChange,
|
||||
onDoneEditing,
|
||||
onCancelEditing
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||
<Dialog
|
||||
maxWidth="xs"
|
||||
onClose={onCancelEditing}
|
||||
aria-labelledby="user-form-dialog-title"
|
||||
open
|
||||
>
|
||||
<DialogTitle id="user-form-dialog-title">Change Value</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
{devicevalue.l && (
|
||||
<TextField
|
||||
id="outlined-select-value"
|
||||
select
|
||||
value={devicevalue.v}
|
||||
autoFocus
|
||||
fullWidth
|
||||
onChange={handleValueChange('v')}
|
||||
variant="outlined"
|
||||
>
|
||||
{devicevalue.l.map((val) => (
|
||||
<MenuItem value={val}>{val}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
)}
|
||||
{!devicevalue.l && (
|
||||
<OutlinedInput
|
||||
id="value"
|
||||
value={devicevalue.v}
|
||||
autoFocus
|
||||
fullWidth
|
||||
onChange={handleValueChange('v')}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
{DeviceValueUOM_s[devicevalue.u]}
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<FormHelperText>{devicevalue.n}</FormHelperText>
|
||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={4} mb={0}>
|
||||
<Typography variant="body2">
|
||||
<i>
|
||||
Note: it may take a few seconds before the change is
|
||||
registered with the EMS device.
|
||||
</i>
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onCancelEditing}
|
||||
>
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
type="submit"
|
||||
onClick={this.submit}
|
||||
>
|
||||
Done
|
||||
</FormButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ValueForm;
|
||||
89
interface/src/project/api.ts
Normal file
89
interface/src/project/api.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { AXIOS, AXIOS_API, AXIOS_BIN } from '../api/endpoints';
|
||||
|
||||
import {
|
||||
BoardProfile,
|
||||
BoardProfileName,
|
||||
APIcall,
|
||||
Settings,
|
||||
Status,
|
||||
CoreData,
|
||||
Devices,
|
||||
DeviceData,
|
||||
DeviceEntity,
|
||||
UniqueID,
|
||||
ExcludeEntities,
|
||||
WriteValue,
|
||||
WriteSensor,
|
||||
WriteAnalog,
|
||||
SensorData
|
||||
} from './types';
|
||||
|
||||
export function restart(): AxiosPromise<void> {
|
||||
return AXIOS.post('/restart');
|
||||
}
|
||||
|
||||
export function readSettings(): AxiosPromise<Settings> {
|
||||
return AXIOS.get('/settings');
|
||||
}
|
||||
|
||||
export function writeSettings(settings: Settings): AxiosPromise<Settings> {
|
||||
return AXIOS.post('/settings', settings);
|
||||
}
|
||||
|
||||
export function getBoardProfile(boardProfile: BoardProfileName): AxiosPromise<BoardProfile> {
|
||||
return AXIOS.post('/boardProfile', boardProfile);
|
||||
}
|
||||
|
||||
export function readStatus(): AxiosPromise<Status> {
|
||||
return AXIOS.get('/status');
|
||||
}
|
||||
|
||||
export function readCoreData(): AxiosPromise<CoreData> {
|
||||
return AXIOS.get('/coreData');
|
||||
}
|
||||
|
||||
export function readDevices(): AxiosPromise<Devices> {
|
||||
return AXIOS.get('/devices');
|
||||
}
|
||||
|
||||
export function scanDevices(): AxiosPromise<void> {
|
||||
return AXIOS.post('/scanDevices');
|
||||
}
|
||||
|
||||
export function readDeviceData(unique_id: UniqueID): AxiosPromise<DeviceData> {
|
||||
return AXIOS_BIN.post('/deviceData', unique_id);
|
||||
}
|
||||
|
||||
export function readSensorData(): AxiosPromise<SensorData> {
|
||||
return AXIOS.get('/sensorData');
|
||||
}
|
||||
|
||||
export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEntity[]> {
|
||||
return AXIOS_BIN.post('/deviceEntities', unique_id);
|
||||
}
|
||||
|
||||
export function writeExcludeEntities(excludeEntities: ExcludeEntities): AxiosPromise<void> {
|
||||
return AXIOS.post('/excludeEntities', excludeEntities);
|
||||
}
|
||||
|
||||
export function writeValue(writevalue: WriteValue): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeValue', writevalue);
|
||||
}
|
||||
|
||||
export function writeSensor(writesensor: WriteSensor): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeSensor', writesensor);
|
||||
}
|
||||
|
||||
export function writeAnalog(writeanalog: WriteAnalog): AxiosPromise<void> {
|
||||
return AXIOS.post('/writeAnalog', writeanalog);
|
||||
}
|
||||
|
||||
export function resetCustomizations(): AxiosPromise<void> {
|
||||
return AXIOS.post('/resetCustomizations');
|
||||
}
|
||||
|
||||
// EMS-ESP API calls
|
||||
export function API(apiCall: APIcall): AxiosPromise<void> {
|
||||
return AXIOS_API.post('/', apiCall);
|
||||
}
|
||||
254
interface/src/project/types.ts
Normal file
254
interface/src/project/types.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
export interface Settings {
|
||||
tx_mode: number;
|
||||
ems_bus_id: number;
|
||||
syslog_enabled: boolean;
|
||||
syslog_level: number;
|
||||
syslog_mark_interval: number;
|
||||
syslog_host: string;
|
||||
syslog_port: number;
|
||||
master_thermostat: number;
|
||||
shower_timer: boolean;
|
||||
shower_alert: boolean;
|
||||
rx_gpio: number;
|
||||
tx_gpio: number;
|
||||
telnet_enabled: boolean;
|
||||
dallas_gpio: number;
|
||||
dallas_parasite: boolean;
|
||||
led_gpio: number;
|
||||
hide_led: boolean;
|
||||
low_clock: boolean;
|
||||
notoken_api: boolean;
|
||||
readonly_mode: boolean;
|
||||
analog_enabled: boolean;
|
||||
pbutton_gpio: number;
|
||||
trace_raw: boolean;
|
||||
board_profile: string;
|
||||
bool_format: number;
|
||||
enum_format: number;
|
||||
fahrenheit: boolean;
|
||||
phy_type: number;
|
||||
eth_power: number;
|
||||
eth_phy_addr: number;
|
||||
eth_clock_mode: number;
|
||||
}
|
||||
|
||||
export enum busConnectionStatus {
|
||||
BUS_STATUS_CONNECTED = 0,
|
||||
BUS_STATUS_TX_ERRORS = 1,
|
||||
BUS_STATUS_OFFLINE = 2
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
status: busConnectionStatus;
|
||||
tx_mode: number;
|
||||
rx_received: number;
|
||||
tx_reads: number;
|
||||
tx_writes: number;
|
||||
rx_quality: number;
|
||||
tx_read_quality: number;
|
||||
tx_write_quality: number;
|
||||
tx_read_fails: number;
|
||||
tx_write_fails: number;
|
||||
rx_fails: number;
|
||||
sensor_fails: number;
|
||||
sensor_reads: number;
|
||||
sensor_quality: number;
|
||||
analog_fails: number;
|
||||
analog_reads: number;
|
||||
analog_quality: number;
|
||||
mqtt_count: number;
|
||||
mqtt_fails: number;
|
||||
mqtt_quality: number;
|
||||
api_calls: number;
|
||||
api_fails: number;
|
||||
api_quality: number;
|
||||
num_devices: number;
|
||||
num_sensors: number;
|
||||
num_analogs: number;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
i: number; // id
|
||||
t: string; // type
|
||||
b: string; // brand
|
||||
n: string; // name
|
||||
d: number; // deviceid
|
||||
p: number; // productid
|
||||
v: string; // version
|
||||
e: number; // number of entries
|
||||
}
|
||||
|
||||
export interface Sensor {
|
||||
is: string; // id string
|
||||
n: string; // name/alias
|
||||
t?: number; // temp, optional
|
||||
o: number; // offset
|
||||
u: number; // uom
|
||||
}
|
||||
|
||||
export interface Analog {
|
||||
i: number;
|
||||
n: string;
|
||||
v?: number;
|
||||
u: number;
|
||||
o: number;
|
||||
f: number;
|
||||
t: number;
|
||||
}
|
||||
|
||||
export interface WriteSensor {
|
||||
id_str: string;
|
||||
name: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface SensorData {
|
||||
sensors: Sensor[];
|
||||
analogs: Analog[];
|
||||
}
|
||||
|
||||
export interface CoreData {
|
||||
devices: Device[];
|
||||
active_sensors: number;
|
||||
analog_enabled: boolean;
|
||||
}
|
||||
|
||||
export interface DeviceShort {
|
||||
i: number; // id
|
||||
d: number; // deviceid
|
||||
p: number; // productid
|
||||
s: string; // shortname
|
||||
}
|
||||
|
||||
export interface Devices {
|
||||
devices: DeviceShort[];
|
||||
}
|
||||
|
||||
export interface DeviceValue {
|
||||
v?: any; // value, in any format
|
||||
u: number; // uom
|
||||
n: string; // name
|
||||
c: string; // command
|
||||
l: string[]; // list
|
||||
}
|
||||
|
||||
export interface DeviceData {
|
||||
label: string;
|
||||
data: DeviceValue[];
|
||||
}
|
||||
|
||||
export interface DeviceEntity {
|
||||
v?: any; // value, in any format
|
||||
n: string; // name
|
||||
s: string; // shortname
|
||||
x: boolean; // excluded flag
|
||||
i: number; // unique id
|
||||
}
|
||||
|
||||
export interface ExcludeEntities {
|
||||
id: number;
|
||||
entity_ids: number[];
|
||||
}
|
||||
|
||||
export interface UniqueID {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export enum DeviceValueUOM {
|
||||
NONE = 0,
|
||||
DEGREES,
|
||||
DEGREES_R,
|
||||
PERCENT,
|
||||
LMIN,
|
||||
KWH,
|
||||
WH,
|
||||
HOURS,
|
||||
MINUTES,
|
||||
UA,
|
||||
BAR,
|
||||
KW,
|
||||
W,
|
||||
KB,
|
||||
SECONDS,
|
||||
DBM,
|
||||
FAHRENHEIT,
|
||||
MV,
|
||||
SQM
|
||||
}
|
||||
|
||||
export const DeviceValueUOM_s = [
|
||||
'',
|
||||
'°C',
|
||||
'°C',
|
||||
'%',
|
||||
'l/min',
|
||||
'kWh',
|
||||
'Wh',
|
||||
'hours',
|
||||
'minutes',
|
||||
'uA',
|
||||
'bar',
|
||||
'kW',
|
||||
'W',
|
||||
'KB',
|
||||
'second',
|
||||
'dBm',
|
||||
'°F',
|
||||
'mV',
|
||||
'sqm',
|
||||
"o'clock"
|
||||
];
|
||||
|
||||
export const AnalogTypes = ['(disabled)', 'Digital in', 'Counter', 'ADC'];
|
||||
|
||||
type BoardProfiles = {
|
||||
[name: string]: string;
|
||||
};
|
||||
|
||||
export const BOARD_PROFILES: BoardProfiles = {
|
||||
S32: 'BBQKees Gateway S32',
|
||||
E32: 'BBQKees Gateway E32',
|
||||
NODEMCU: 'NodeMCU 32S',
|
||||
'MH-ET': 'MH-ET Live D1 Mini',
|
||||
LOLIN: 'Lolin D32',
|
||||
OLIMEX: 'Olimex ESP32-EVB',
|
||||
OLIMEXPOE: 'Olimex ESP32-POE'
|
||||
};
|
||||
|
||||
export interface BoardProfileName {
|
||||
board_profile: string;
|
||||
}
|
||||
|
||||
export interface BoardProfile {
|
||||
board_profile: string;
|
||||
led_gpio: number;
|
||||
dallas_gpio: number;
|
||||
rx_gpio: number;
|
||||
tx_gpio: number;
|
||||
pbutton_gpio: number;
|
||||
phy_type: number;
|
||||
eth_power: number;
|
||||
eth_phy_addr: number;
|
||||
eth_clock_mode: number;
|
||||
}
|
||||
|
||||
export interface APIcall {
|
||||
device: string;
|
||||
entity: string;
|
||||
id: any;
|
||||
}
|
||||
|
||||
export interface WriteValue {
|
||||
id: number;
|
||||
devicevalue: DeviceValue;
|
||||
}
|
||||
|
||||
export interface WriteAnalog {
|
||||
id: number;
|
||||
name: string;
|
||||
factor: number;
|
||||
offset: number;
|
||||
uom: number;
|
||||
type: number;
|
||||
}
|
||||
44
interface/src/project/validators.ts
Normal file
44
interface/src/project/validators.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import Schema, { InternalRuleItem } from 'async-validator';
|
||||
import { IP_OR_HOSTNAME_VALIDATOR } from '../validators/shared';
|
||||
import { Settings } from './types';
|
||||
|
||||
export const GPIO_VALIDATOR = {
|
||||
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) {
|
||||
if (
|
||||
value &&
|
||||
(value === 1 ||
|
||||
(value >= 6 && value <= 12) ||
|
||||
(value >= 14 && value <= 15) ||
|
||||
value === 20 ||
|
||||
value === 24 ||
|
||||
(value >= 28 && value <= 31) ||
|
||||
value > 40)
|
||||
) {
|
||||
callback('Must be an valid GPIO port');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createSettingsValidator = (settings: Settings) =>
|
||||
new Schema({
|
||||
...(settings.board_profile === 'CUSTOM' && {
|
||||
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATOR],
|
||||
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATOR],
|
||||
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATOR],
|
||||
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATOR],
|
||||
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
|
||||
}),
|
||||
...(settings.syslog_enabled && {
|
||||
syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR],
|
||||
syslog_port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Port must be between 0 and 65535' }
|
||||
],
|
||||
syslog_mark_interval: [
|
||||
{ required: true, message: 'Mark interval is required' },
|
||||
{ type: 'number', min: 0, max: 10, message: 'Port must be between 0 and 10' }
|
||||
]
|
||||
})
|
||||
});
|
||||
Reference in New Issue
Block a user