Merge remote-tracking branch 'origin/v3.4' into dev

This commit is contained in:
proddy
2022-01-23 17:56:52 +01:00
parent 02e2b51814
commit 77e1898512
538 changed files with 32282 additions and 38655 deletions

View 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 &amp; 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;

View 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:&nbsp;{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;

View 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 &amp; 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 &amp; Activity Status" titleGutter>
{content()}
</SectionContent>
);
};
export default DashboardStatus;

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

View File

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

View File

@@ -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 &amp; 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;

View File

@@ -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 &amp; Sensors">
<RestFormLoader
{...this.props}
render={(formProps) => <EMSESPDataForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(EMSESP_DATA_ENDPOINT, EMSESPDataController);

View File

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

View File

@@ -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'}&nbsp;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;

View File

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

View File

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

View File

@@ -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&nbsp;
<Link
target="_blank"
href="https://emsesp.github.io/docs/#/Configure-firmware?id=ems-esp-settings"
color="primary"
>
{'online documentation'}
</Link>
&nbsp;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));

View File

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

View File

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

View File

@@ -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)}&nbsp;(quality{' '}
{data.rx_quality}%)
</TableCell>
</TableRow>
<TableRow>
<TableCell>Telegrams Sent</TableCell>
<TableCell align="right">
{formatNumber(data.tx_sent)}&nbsp;(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));

View File

@@ -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"
];

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

View 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 &amp; Support" titleGutter>
<List>
<ListItem>
<ListItemAvatar>
<TuneIcon />
</ListItemAvatar>
<ListItemText>
For a help on each of the Application Settings see&nbsp;
<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&nbsp;
<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&nbsp;
<Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
{'Discord'}
</Link>
&nbsp;server
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<GitHubIcon />
</ListItemAvatar>
<ListItemText>
To report an issue or request a feature, please do via&nbsp;
<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&nbsp;
<StarIcon style={{ color: '#fdff3a' }} /> on&nbsp;
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
{'GitHub'}
</Link>
&nbsp;!
</Typography>
</Box>
</SectionContent>
);
};
export default HelpInformation;

View File

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

View File

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

View File

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

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

View 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&hellip;
</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;

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

View File

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

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

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

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