Rework backend add MQTT and WebSocket support

* Update back end to add MQTT and WebSocket support
* Update demo project to demonstrate MQTT and WebSockets
* Update documentation to describe newly added and modified functionallity
* Introduce separate MQTT pub/sub, HTTP get/post and WebSocket rx/tx classes
* Significant reanaming - more accurate class names
* Use PROGMEM_WWW as default
* Update README documenting PROGMEM_WWW as default
* Update README with API changes
This commit is contained in:
rjwats
2020-05-14 23:23:45 +01:00
committed by GitHub
parent c47ea49a5d
commit a1f4e57a21
77 changed files with 2907 additions and 1192 deletions

View File

@@ -0,0 +1,37 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Tabs, Tab } from '@material-ui/core';
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
import { MenuAppBar } from '../components';
import MqttStatusController from './MqttStatusController';
import MqttSettingsController from './MqttSettingsController';
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
class Mqtt extends Component<MqttProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
this.props.history.push(path);
};
render() {
const { authenticatedContext } = this.props;
return (
<MenuAppBar sectionTitle="MQTT">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tab value="/mqtt/status" label="MQTT Status" />
<Tab value="/mqtt/settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
<Redirect to="/mqtt/status" />
</Switch>
</MenuAppBar>
)
}
}
export default withAuthenticatedContext(Mqtt);

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import { MQTT_SETTINGS_ENDPOINT } from '../api';
import MqttSettingsForm from './MqttSettingsForm';
import { MqttSettings } from './types';
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
<SectionContent title="MQTT Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <MqttSettingsForm {...formProps} />}
/>
</SectionContent>
)
}
}
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);

View File

@@ -0,0 +1,131 @@
import React from 'react';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { Checkbox, TextField } from '@material-ui/core';
import SaveIcon from '@material-ui/icons/Save';
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel, PasswordValidator } from '../components';
import { isIP, isHostname, or } from '../validators';
import { MqttSettings } from './types';
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
}
render() {
const { data, handleValueChange, saveData, loadData } = this.props;
return (
<ValidatorForm onSubmit={saveData}>
<BlockFormControlLabel
control={
<Checkbox
checked={data.enabled}
onChange={handleValueChange('enabled')}
value="enabled"
/>
}
label="Enable MQTT?"
/>
<TextValidator
validators={['required', 'isIPOrHostname']}
errorMessages={['Host is required', "Not a valid IP address or hostname"]}
name="host"
label="Host"
fullWidth
variant="outlined"
value={data.host}
onChange={handleValueChange('host')}
margin="normal"
/>
<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="port"
label="Port"
fullWidth
variant="outlined"
value={data.port}
type="number"
onChange={handleValueChange('port')}
margin="normal"
/>
<TextField
name="username"
label="Username"
fullWidth
variant="outlined"
value={data.username}
onChange={handleValueChange('username')}
margin="normal"
/>
<PasswordValidator
name="password"
label="Password"
fullWidth
variant="outlined"
value={data.password}
onChange={handleValueChange('password')}
margin="normal"
/>
<TextField
name="client_id"
label="Client ID (optional)"
fullWidth
variant="outlined"
value={data.client_id}
onChange={handleValueChange('client_id')}
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="keep_alive"
label="Keep Alive (seconds)"
fullWidth
variant="outlined"
value={data.keep_alive}
type="number"
onChange={handleValueChange('keep_alive')}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.clean_session}
onChange={handleValueChange('clean_session')}
value="clean_session"
/>
}
label="Clean Session?"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:1', 'maxNumber:65535']}
errorMessages={['Max topic length is required', "Must be a number", "Must be greater than 0", "Max value is 65535"]}
name="max_topic_length"
label="Max Topic Length"
fullWidth
variant="outlined"
value={data.max_topic_length}
type="number"
onChange={handleValueChange('max_topic_length')}
margin="normal"
/>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
Save
</FormButton>
<FormButton variant="contained" color="secondary" onClick={loadData}>
Reset
</FormButton>
</FormActions>
</ValidatorForm>
);
}
}
export default MqttSettingsForm;

View File

@@ -0,0 +1,45 @@
import { Theme } from "@material-ui/core";
import { MqttStatus, MqttDisconnectReason } from "./types";
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
if (!enabled) {
return theme.palette.info.main;
}
if (connected) {
return theme.palette.success.main;
}
return theme.palette.error.main;
}
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
if (!enabled) {
return "Not enabled";
}
if (connected) {
return "Connected";
}
return "Disconnected";
}
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
switch (disconnect_reason) {
case MqttDisconnectReason.TCP_DISCONNECTED:
return "TCP disconnected";
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
return "Unacceptable protocol version";
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
return "Client ID rejected";
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
return "Server unavailable";
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
return "Malformed credentials";
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
return "Not authorized";
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
return "Device out of memory";
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
return "Server fingerprint invalid";
default:
return "Unknown"
}
}

View File

@@ -0,0 +1,29 @@
import React, { Component } from 'react';
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import { MQTT_STATUS_ENDPOINT } from '../api';
import MqttStatusForm from './MqttStatusForm';
import { MqttStatus } from './types';
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
class MqttStatusController extends Component<MqttStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
render() {
return (
<SectionContent title="MQTT Status">
<RestFormLoader
{...this.props}
render={formProps => <MqttStatusForm {...formProps} />}
/>
</SectionContent>
)
}
}
export default restController(MQTT_STATUS_ENDPOINT, MqttStatusController);

View File

@@ -0,0 +1,83 @@
import React, { Component, Fragment } from 'react';
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import RefreshIcon from '@material-ui/icons/Refresh';
import ReportIcon from '@material-ui/icons/Report';
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
import { mqttStatusHighlight, mqttStatus, disconnectReason } from './MqttStatus';
import { MqttStatus } from './types';
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
class MqttStatusForm extends Component<MqttStatusFormProps> {
renderConnectionStatus() {
const { data } = this.props
if (data.connected) {
return (
<Fragment>
<ListItem>
<ListItemAvatar>
<Avatar>#</Avatar>
</ListItemAvatar>
<ListItemText primary="Client ID" secondary={data.client_id} />
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
);
}
return (
<Fragment>
<ListItem>
<ListItemAvatar>
<Avatar>
<ReportIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
);
}
createListItems() {
const { data, theme } = this.props
return (
<Fragment>
<ListItem>
<ListItemAvatar>
<HighlightAvatar color={mqttStatusHighlight(data, theme)}>
<DeviceHubIcon />
</HighlightAvatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={mqttStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{data.enabled && this.renderConnectionStatus()}
</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(MqttStatusForm);

View File

@@ -0,0 +1,29 @@
export enum MqttDisconnectReason {
TCP_DISCONNECTED = 0,
MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1,
MQTT_IDENTIFIER_REJECTED = 2,
MQTT_SERVER_UNAVAILABLE = 3,
MQTT_MALFORMED_CREDENTIALS = 4,
MQTT_NOT_AUTHORIZED = 5,
ESP8266_NOT_ENOUGH_SPACE = 6,
TLS_BAD_FINGERPRINT = 7
}
export interface MqttStatus {
enabled: boolean;
connected: boolean;
client_id: string;
disconnect_reason: MqttDisconnectReason;
}
export interface MqttSettings {
enabled: boolean;
host: string;
port: number;
username: string;
password: string;
client_id: string;
keep_alive: number;
clean_session: boolean;
max_topic_length: number;
}