mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 16:29:51 +03:00
initial commit
This commit is contained in:
38
interface/src/project/EMSESP.tsx
Normal file
38
interface/src/project/EMSESP.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { 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';
|
||||
import EMSESPStatusController from './EMSESPStatusController';
|
||||
|
||||
class EMSESP extends Component<RouteComponentProps> {
|
||||
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="EMS-ESP">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value={`/${PROJECT_PATH}/status`} label="EMS-ESP Status" />
|
||||
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/settings`} component={EMSESPSettingsController} />
|
||||
<Redirect to={`/${PROJECT_PATH}/status`} />
|
||||
</Switch>
|
||||
</MenuAppBar>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EMSESP;
|
||||
180
interface/src/project/EMSESPSettingsController.tsx
Normal file
180
interface/src/project/EMSESPSettingsController.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { ValidatorForm, TextValidator, SelectValidator } from 'react-material-ui-form-validator';
|
||||
|
||||
import { Checkbox, Typography, Box } from '@material-ui/core';
|
||||
import SaveIcon from '@material-ui/icons/Save';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, BlockFormControlLabel, SectionContent } from '../components';
|
||||
|
||||
import { isIP, isHostname, or } from '../validators';
|
||||
|
||||
import { EMSESPSettings } from './types';
|
||||
|
||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
||||
|
||||
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
||||
|
||||
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
||||
|
||||
componentDidMount() {
|
||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SectionContent title='EMS-ESP Settings' titleGutter>
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={props => (
|
||||
<EMSESPSettingsControllerForm {...props} />
|
||||
)}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
|
||||
|
||||
type EMSESPSettingsControllerFormProps = RestFormProps<EMSESPSettings>;
|
||||
|
||||
function EMSESPSettingsControllerForm(props: EMSESPSettingsControllerFormProps) {
|
||||
const { data, saveData, loadData, handleValueChange } = props;
|
||||
return (
|
||||
<ValidatorForm onSubmit={saveData}>
|
||||
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
|
||||
<Typography variant="body1">
|
||||
Customize EMS-ESP by editing the default settings here. Refer to the <a href="https://emsesp.github.io/docs/#/">Wiki</a> for assistance.
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:255']}
|
||||
errorMessages={['TX mode is required', "Must be a number", "Must be greater than 0", "Max value is 255"]}
|
||||
name="tx_mode"
|
||||
label="Tx mode"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.tx_mode}
|
||||
type="number"
|
||||
onChange={handleValueChange('tx_mode')}
|
||||
margin="normal"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.system_heartbeat}
|
||||
onChange={handleValueChange('system_heartbeat')}
|
||||
value="system_heartbeat"
|
||||
/>
|
||||
}
|
||||
label="MQTT heartbeat"
|
||||
/>
|
||||
<SelectValidator name="mqtt_format"
|
||||
label="MQTT format"
|
||||
value={data.mqtt_format}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('mqtt_format')}
|
||||
margin="normal">
|
||||
<MenuItem value={1}>Single</MenuItem>
|
||||
<MenuItem value={2}>Nested</MenuItem>
|
||||
<MenuItem value={3}>Home Assistant</MenuItem>
|
||||
<MenuItem value={4}>Custom</MenuItem>
|
||||
</SelectValidator>
|
||||
<SelectValidator name="mqtt_qos"
|
||||
label="MQTT QoS"
|
||||
value={data.mqtt_qos}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
onChange={handleValueChange('mqtt_qos')}
|
||||
margin="normal">
|
||||
<MenuItem value={0}>0</MenuItem>
|
||||
<MenuItem value={1}>1</MenuItem>
|
||||
<MenuItem value={2}>2</MenuItem>
|
||||
</SelectValidator>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
|
||||
errorMessages={['Keep alive is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
|
||||
name="publish_time"
|
||||
label="MQTT Publish time (seconds)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.publish_time}
|
||||
type="number"
|
||||
onChange={handleValueChange('publish_time')}
|
||||
margin="normal"
|
||||
/>
|
||||
<SelectValidator name="syslog_level"
|
||||
label="Syslog 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={6}>INFO</MenuItem>
|
||||
<MenuItem value={7}>DEBUG</MenuItem>
|
||||
</SelectValidator>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_timer}
|
||||
onChange={handleValueChange('shower_timer')}
|
||||
value="shower_timer"
|
||||
/>
|
||||
}
|
||||
label="Shower Timer"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.shower_alert}
|
||||
onChange={handleValueChange('shower_alert')}
|
||||
value="shower_alert"
|
||||
/>
|
||||
}
|
||||
label="Shower Alert"
|
||||
/>
|
||||
{data.syslog_level !== -1 &&
|
||||
<Fragment>
|
||||
<TextValidator
|
||||
validators={['isIPOrHostname']}
|
||||
errorMessages={["Not a valid IP address or hostname"]}
|
||||
name="syslog_host"
|
||||
label="Syslog IP/Host"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.syslog_host}
|
||||
onChange={handleValueChange('syslog_host')}
|
||||
margin="normal"
|
||||
/>
|
||||
<TextValidator
|
||||
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
|
||||
errorMessages={['Keep alive is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
|
||||
name="syslog_mark_interval"
|
||||
label="Syslog mark interval (seconds)"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={data.publish_time}
|
||||
type="number"
|
||||
onChange={handleValueChange('syslog_mark_interval')}
|
||||
margin="normal"
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||
Save
|
||||
</FormButton>
|
||||
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||
Reset
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
);
|
||||
}
|
||||
41
interface/src/project/EMSESPStatus.ts
Normal file
41
interface/src/project/EMSESPStatus.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Theme } from '@material-ui/core';
|
||||
import { EMSESPStatus, busConnectionStatus } from './types';
|
||||
|
||||
export const isConnected = ({ status }: EMSESPStatus) => status === busConnectionStatus.BUS_STATUS_CONNECTED;
|
||||
|
||||
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
||||
switch (status) {
|
||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||
return theme.palette.info.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 not stable";
|
||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||
return "Disconnected";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export const mqttStatusHighlight = ({ mqtt_fails }: EMSESPStatus, theme: Theme) => {
|
||||
if (mqtt_fails === 0)
|
||||
return theme.palette.success.main;
|
||||
|
||||
if (mqtt_fails < 10)
|
||||
return theme.palette.warning.main;
|
||||
|
||||
return theme.palette.success.main;
|
||||
|
||||
}
|
||||
30
interface/src/project/EMSESPStatusController.tsx
Normal file
30
interface/src/project/EMSESPStatusController.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||
import { ENDPOINT_ROOT } from '../api';
|
||||
import EMSESPStatusForm from './EMSESPStatusForm';
|
||||
import { EMSESPStatus } from './types';
|
||||
|
||||
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-ESP Status">
|
||||
<RestFormLoader
|
||||
{...this.props}
|
||||
render={formProps => <EMSESPStatusForm {...formProps} />}
|
||||
/>
|
||||
</SectionContent>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default restController(EMSESP_STATUS_ENDPOINT, EMSESPStatusController);
|
||||
149
interface/src/project/EMSESPStatusForm.tsx
Normal file
149
interface/src/project/EMSESPStatusForm.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
||||
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||
import { Table, TableBody, TableCell, TableHead, TableRow, Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||
|
||||
import BuildIcon from '@material-ui/icons/Build';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||
import SpeakerNotesOffIcon from '@material-ui/icons/SpeakerNotesOff';
|
||||
import TimerIcon from '@material-ui/icons/Timer';
|
||||
import BatteryUnknownIcon from '@material-ui/icons/BatteryUnknown';
|
||||
|
||||
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
||||
import { busStatus, busStatusHighlight, isConnected, mqttStatusHighlight } from './EMSESPStatus';
|
||||
|
||||
import { EMSESPStatus } from './types';
|
||||
|
||||
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> & WithTheme;
|
||||
|
||||
class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||
|
||||
createListItems() {
|
||||
const { data, theme } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<BuildIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Firmware Version" secondary={data.version} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<TimerIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="System Uptime" secondary={data.uptime} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<BatteryUnknownIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Free System Memory" secondary={data.free_mem + '%'} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<HighlightAvatar color={busStatusHighlight(data, theme)}>
|
||||
<DeviceHubIcon />
|
||||
</HighlightAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="EMS Bus Status" secondary={busStatus(data)} />
|
||||
</ListItem>
|
||||
<Divider variant="inset" component="li" />
|
||||
{
|
||||
isConnected(data) &&
|
||||
<Fragment>
|
||||
|
||||
<Table size="small" padding="default">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Statistic</TableCell>
|
||||
<TableCell align="center"># Telegrams</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row">
|
||||
Rx Received
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{data.rx_received}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row">
|
||||
Tx Sent
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{data.tx_sent}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row">
|
||||
CRC Errors
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{data.crc_errors}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell component="th" scope="row">
|
||||
Tx Errors
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{data.tx_errors}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<HighlightAvatar color={mqttStatusHighlight(data, theme)}>
|
||||
<SpeakerNotesOffIcon />
|
||||
</HighlightAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="MQTT Publish fails" secondary={data.mqtt_fails} />
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="inset" component="li" />
|
||||
<ListItem></ListItem>
|
||||
</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(EMSESPStatusForm);
|
||||
27
interface/src/project/ProjectMenu.tsx
Normal file
27
interface/src/project/ProjectMenu.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
|
||||
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
|
||||
|
||||
import { PROJECT_PATH } from '../api';
|
||||
|
||||
class ProjectMenu extends Component<RouteComponentProps> {
|
||||
|
||||
render() {
|
||||
const path = this.props.match.url;
|
||||
return (
|
||||
<List>
|
||||
<ListItem to={`/${PROJECT_PATH}/`} selected={path.startsWith(`/${PROJECT_PATH}/`)} button component={Link}>
|
||||
<ListItemIcon>
|
||||
<SettingsRemoteIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="EMS-ESP" />
|
||||
</ListItem>
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(ProjectMenu);
|
||||
33
interface/src/project/ProjectRouting.tsx
Normal file
33
interface/src/project/ProjectRouting.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch } from 'react-router';
|
||||
|
||||
import { PROJECT_PATH } from '../api';
|
||||
import { AuthenticatedRoute } from '../authentication';
|
||||
|
||||
import EMSESP from './EMSESP';
|
||||
|
||||
class ProjectRouting extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
{
|
||||
/*
|
||||
* Add your project page routing below.
|
||||
*/
|
||||
}
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={EMSESP} />
|
||||
{
|
||||
/*
|
||||
* 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={`/${PROJECT_PATH}/`} />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ProjectRouting;
|
||||
32
interface/src/project/types.ts
Normal file
32
interface/src/project/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface EMSESPSettings {
|
||||
tx_mode: number;
|
||||
ems_bus_id: number;
|
||||
system_heartbeat: boolean;
|
||||
syslog_level: number;
|
||||
syslog_mark_interval: number;
|
||||
syslog_host: string;
|
||||
master_thermostat: number;
|
||||
shower_timer: boolean;
|
||||
shower_alert: boolean;
|
||||
publish_time: number;
|
||||
mqtt_format: number;
|
||||
mqtt_qos: number;
|
||||
}
|
||||
|
||||
export enum busConnectionStatus {
|
||||
BUS_STATUS_CONNECTED = 0,
|
||||
BUS_STATUS_TX_ERRORS = 1,
|
||||
BUS_STATUS_OFFLINE = 2
|
||||
}
|
||||
|
||||
export interface EMSESPStatus {
|
||||
version: string;
|
||||
status: busConnectionStatus;
|
||||
rx_received: number;
|
||||
tx_sent: number;
|
||||
crc_errors: number;
|
||||
tx_errors: number;
|
||||
mqtt_fails: number;
|
||||
uptime: string;
|
||||
free_mem: number;
|
||||
}
|
||||
Reference in New Issue
Block a user