Browse Source
Rework backend add MQTT and WebSocket support
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 changesmaster
rjwats
4 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 2877 additions and 1162 deletions
-
364README.md
-
3data/config/demoSettings.json
-
11data/config/mqttSettings.json
-
3interface/.env.development
-
1interface/.env.production
-
10interface/package-lock.json
-
3interface/package.json
-
2interface/src/AppRouting.tsx
-
2interface/src/api/Endpoints.ts
-
23interface/src/api/Env.ts
-
10interface/src/authentication/Authentication.ts
-
7interface/src/components/MenuAppBar.tsx
-
29interface/src/components/RestController.tsx
-
3interface/src/components/RestFormLoader.tsx
-
133interface/src/components/WebSocketController.tsx
-
40interface/src/components/WebSocketFormLoader.tsx
-
4interface/src/components/index.ts
-
37interface/src/mqtt/Mqtt.tsx
-
30interface/src/mqtt/MqttSettingsController.tsx
-
131interface/src/mqtt/MqttSettingsForm.tsx
-
45interface/src/mqtt/MqttStatus.ts
-
29interface/src/mqtt/MqttStatusController.tsx
-
83interface/src/mqtt/MqttStatusForm.tsx
-
29interface/src/mqtt/types.ts
-
3interface/src/ntp/NTPSettingsForm.tsx
-
2interface/src/ntp/TZ.tsx
-
75interface/src/project/DemoController.tsx
-
20interface/src/project/DemoInformation.tsx
-
14interface/src/project/DemoProject.tsx
-
93interface/src/project/LightMqttSettingsController.tsx
-
70interface/src/project/LightStateRestController.tsx
-
62interface/src/project/LightStateWebSocketController.tsx
-
9interface/src/project/types.ts
-
8interface/src/security/ManageUsersForm.tsx
-
8interface/src/security/SecuritySettingsForm.tsx
-
44lib/framework/APSettingsService.cpp
-
35lib/framework/APSettingsService.h
-
50lib/framework/AdminSettingsService.h
-
33lib/framework/AsyncJsonCallbackResponse.h
-
131lib/framework/AsyncJsonWebHandler.h
-
18lib/framework/AuthenticationService.cpp
-
6lib/framework/AuthenticationService.h
-
5lib/framework/ESP8266React.cpp
-
22lib/framework/ESP8266React.h
-
103lib/framework/FSPersistence.h
-
167lib/framework/HttpEndpoint.h
-
9lib/framework/JsonDeserializer.h
-
9lib/framework/JsonSerializer.h
-
17lib/framework/JsonUtils.h
-
161lib/framework/MqttPubSub.h
-
155lib/framework/MqttSettingsService.cpp
-
125lib/framework/MqttSettingsService.h
-
24lib/framework/MqttStatus.cpp
-
31lib/framework/MqttStatus.h
-
57lib/framework/NTPSettingsService.cpp
-
35lib/framework/NTPSettingsService.h
-
40lib/framework/OTASettingsService.cpp
-
26lib/framework/OTASettingsService.h
-
16lib/framework/SecurityManager.h
-
101lib/framework/SecuritySettingsService.cpp
-
52lib/framework/SecuritySettingsService.h
-
96lib/framework/SettingsPersistence.h
-
166lib/framework/SettingsService.h
-
87lib/framework/SimpleService.h
-
137lib/framework/StatefulService.h
-
242lib/framework/WebSocketTxRx.h
-
84lib/framework/WiFiSettingsService.cpp
-
61lib/framework/WiFiSettingsService.h
-
BINmedia/framework.png
-
10platformio.ini
-
27src/DemoProject.cpp
-
34src/DemoProject.h
-
16src/LightMqttSettingsService.cpp
-
47src/LightMqttSettingsService.h
-
73src/LightStateService.cpp
-
71src/LightStateService.h
-
20src/main.cpp
@ -1,3 +0,0 @@ |
|||
{ |
|||
"blink_speed": 100 |
|||
} |
@ -0,0 +1,11 @@ |
|||
{ |
|||
"enabled": false, |
|||
"host": "test.mosquitto.org", |
|||
"port": 1883, |
|||
"authenticated": false, |
|||
"username": "mqttuser", |
|||
"password": "mqttpassword", |
|||
"keepAlive": 16, |
|||
"cleanSession": true, |
|||
"maxTopicLength": 128 |
|||
} |
@ -1,3 +1,4 @@ |
|||
# Change the IP address to that of your ESP device to enable local development of the UI. |
|||
# Remember to also enable CORS in platformio.ini before uploading the code to the device. |
|||
REACT_APP_ENDPOINT_ROOT=http://192.168.0.21/rest/ |
|||
REACT_APP_HTTP_ROOT=http://192.168.0.99 |
|||
REACT_APP_WEB_SOCKET_ROOT=ws://192.168.0.99 |
@ -1,2 +1 @@ |
|||
REACT_APP_ENDPOINT_ROOT=/rest/ |
|||
GENERATE_SOURCEMAP=false |
@ -1,3 +1,24 @@ |
|||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!; |
|||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!; |
|||
export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT!; |
|||
|
|||
export const ENDPOINT_ROOT = calculateEndpointRoot("/rest/"); |
|||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot("/ws/"); |
|||
|
|||
function calculateEndpointRoot(endpointPath: string) { |
|||
const httpRoot = process.env.REACT_APP_HTTP_ROOT; |
|||
if (httpRoot) { |
|||
return httpRoot + endpointPath; |
|||
} |
|||
const location = window.location; |
|||
return location.protocol + "//" + location.host + endpointPath; |
|||
} |
|||
|
|||
function calculateWebSocketRoot(webSocketPath: string) { |
|||
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT; |
|||
if (webSocketRoot) { |
|||
return webSocketRoot + webSocketPath; |
|||
} |
|||
const location = window.location; |
|||
const webProtocol = location.protocol === "https:" ? "wss:" : "ws:"; |
|||
return webProtocol + "//" + location.host + webSocketPath; |
|||
} |
@ -0,0 +1,133 @@ |
|||
import React from 'react'; |
|||
import Sockette from 'sockette'; |
|||
import throttle from 'lodash/throttle'; |
|||
import { withSnackbar, WithSnackbarProps } from 'notistack'; |
|||
|
|||
import { addAccessTokenParameter } from '../authentication'; |
|||
import { extractEventValue } from '.'; |
|||
|
|||
export interface WebSocketControllerProps<D> extends WithSnackbarProps { |
|||
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void; |
|||
|
|||
setData: (data: D, callback?: () => void) => void; |
|||
saveData: () => void; |
|||
saveDataAndClear(): () => void; |
|||
|
|||
connected: boolean; |
|||
data?: D; |
|||
} |
|||
|
|||
interface WebSocketControllerState<D> { |
|||
ws: Sockette; |
|||
connected: boolean; |
|||
clientId?: string; |
|||
data?: D; |
|||
} |
|||
|
|||
enum WebSocketMessageType { |
|||
ID = "id", |
|||
PAYLOAD = "payload" |
|||
} |
|||
|
|||
interface WebSocketIdMessage { |
|||
type: typeof WebSocketMessageType.ID; |
|||
id: string; |
|||
} |
|||
|
|||
interface WebSocketPayloadMessage<D> { |
|||
type: typeof WebSocketMessageType.PAYLOAD; |
|||
origin_id: string; |
|||
payload: D; |
|||
} |
|||
|
|||
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>; |
|||
|
|||
export function webSocketController<D, P extends WebSocketControllerProps<D>>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>) { |
|||
return withSnackbar( |
|||
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> { |
|||
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) { |
|||
super(props); |
|||
this.state = { |
|||
ws: new Sockette(addAccessTokenParameter(wsUrl), { |
|||
onmessage: this.onMessage, |
|||
onopen: this.onOpen, |
|||
onclose: this.onClose, |
|||
}), |
|||
connected: false |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
this.state.ws.close(); |
|||
} |
|||
|
|||
onMessage = (event: MessageEvent) => { |
|||
const rawData = event.data; |
|||
if (typeof rawData === 'string' || rawData instanceof String) { |
|||
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>); |
|||
} |
|||
} |
|||
|
|||
handleMessage = (message: WebSocketMessage<D>) => { |
|||
switch (message.type) { |
|||
case WebSocketMessageType.ID: |
|||
this.setState({ clientId: message.id }); |
|||
break; |
|||
case WebSocketMessageType.PAYLOAD: |
|||
const { clientId, data } = this.state; |
|||
if (clientId && (!data || clientId !== message.origin_id)) { |
|||
this.setState( |
|||
{ data: message.payload } |
|||
); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
onOpen = () => { |
|||
this.setState({ connected: true }); |
|||
} |
|||
|
|||
onClose = () => { |
|||
this.setState({ connected: false, clientId: undefined, data: undefined }); |
|||
} |
|||
|
|||
setData = (data: D, callback?: () => void) => { |
|||
this.setState({ data }, callback); |
|||
} |
|||
|
|||
saveData = throttle(() => { |
|||
const { ws, connected, data } = this.state; |
|||
if (connected) { |
|||
ws.json(data); |
|||
} |
|||
}, wsThrottle); |
|||
|
|||
saveDataAndClear = throttle(() => { |
|||
const { ws, connected, data } = this.state; |
|||
if (connected) { |
|||
this.setState({ |
|||
data: undefined |
|||
}, () => ws.json(data)); |
|||
} |
|||
}, wsThrottle); |
|||
|
|||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => { |
|||
const data = { ...this.state.data!, [name]: extractEventValue(event) }; |
|||
this.setState({ data }); |
|||
} |
|||
|
|||
render() { |
|||
return <WebSocketController |
|||
handleValueChange={this.handleValueChange} |
|||
setData={this.setData} |
|||
saveData={this.saveData} |
|||
saveDataAndClear={this.saveDataAndClear} |
|||
connected={this.state.connected} |
|||
data={this.state.data} |
|||
{...this.props as P} |
|||
/>; |
|||
} |
|||
|
|||
}); |
|||
} |
@ -0,0 +1,40 @@ |
|||
import React from 'react'; |
|||
|
|||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; |
|||
import { LinearProgress, Typography } from '@material-ui/core'; |
|||
|
|||
import { WebSocketControllerProps } from '.'; |
|||
|
|||
const useStyles = makeStyles((theme: Theme) => |
|||
createStyles({ |
|||
loadingSettings: { |
|||
margin: theme.spacing(0.5), |
|||
}, |
|||
loadingSettingsDetails: { |
|||
margin: theme.spacing(4), |
|||
textAlign: "center" |
|||
} |
|||
}) |
|||
); |
|||
|
|||
export type WebSocketFormProps<D> = Omit<WebSocketControllerProps<D>, "connected"> & { data: D }; |
|||
|
|||
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> { |
|||
render: (props: WebSocketFormProps<D>) => JSX.Element; |
|||
} |
|||
|
|||
export default function WebSocketFormLoader<D>(props: WebSocketFormLoaderProps<D>) { |
|||
const { connected, render, data, ...rest } = props; |
|||
const classes = useStyles(); |
|||
if (!connected || !data) { |
|||
return ( |
|||
<div className={classes.loadingSettings}> |
|||
<LinearProgress className={classes.loadingSettingsDetails} /> |
|||
<Typography variant="h6" className={classes.loadingSettingsDetails}> |
|||
Connecting to WebSocket... |
|||
</Typography> |
|||
</div> |
|||
); |
|||
} |
|||
return render({ ...rest, data }); |
|||
} |
@ -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); |
@ -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); |
@ -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; |
@ -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" |
|||
} |
|||
} |
@ -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); |
@ -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); |
@ -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; |
|||
} |
@ -1,75 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
import { ValidatorForm } from 'react-material-ui-form-validator'; |
|||
|
|||
import { Typography, Slider, Box } from '@material-ui/core'; |
|||
import SaveIcon from '@material-ui/icons/Save'; |
|||
|
|||
import { ENDPOINT_ROOT } from '../api'; |
|||
import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components'; |
|||
|
|||
export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings"; |
|||
|
|||
interface DemoSettings { |
|||
blink_speed: number; |
|||
} |
|||
|
|||
type DemoControllerProps = RestControllerProps<DemoSettings>; |
|||
|
|||
class DemoController extends Component<DemoControllerProps> { |
|||
|
|||
componentDidMount() { |
|||
this.props.loadData(); |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<SectionContent title='Demo Controller' titleGutter> |
|||
<RestFormLoader |
|||
{...this.props} |
|||
render={props => ( |
|||
<DemoControllerForm {...props} /> |
|||
)} |
|||
/> |
|||
</SectionContent> |
|||
) |
|||
} |
|||
|
|||
} |
|||
|
|||
export default restController(DEMO_SETTINGS_ENDPOINT, DemoController); |
|||
|
|||
const valueToPercentage = (value: number) => `${Math.round(value / 255 * 100)}%`; |
|||
|
|||
type DemoControllerFormProps = RestFormProps<DemoSettings>; |
|||
|
|||
function DemoControllerForm(props: DemoControllerFormProps) { |
|||
const { data, saveData, loadData, handleSliderChange } = props; |
|||
return ( |
|||
<ValidatorForm onSubmit={saveData}> |
|||
<Typography id="blink-speed-slider"> |
|||
Blink Speed |
|||
</Typography> |
|||
<Box pt={5}> |
|||
<Slider |
|||
value={data.blink_speed} |
|||
valueLabelFormat={valueToPercentage} |
|||
aria-labelledby="blink-speed-slider" |
|||
valueLabelDisplay="on" |
|||
min={0} |
|||
max={255} |
|||
onChange={handleSliderChange('blink_speed')} |
|||
/> |
|||
</Box> |
|||
<FormActions> |
|||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit"> |
|||
Save |
|||
</FormButton> |
|||
<FormButton variant="contained" color="secondary" onClick={loadData}> |
|||
Reset |
|||
</FormButton> |
|||
</FormActions> |
|||
</ValidatorForm> |
|||
); |
|||
} |
|||
|
|||
|
@ -0,0 +1,93 @@ |
|||
import React, { Component } from 'react'; |
|||
import { ValidatorForm, TextValidator } from 'react-material-ui-form-validator'; |
|||
|
|||
import { Typography, Box } from '@material-ui/core'; |
|||
import SaveIcon from '@material-ui/icons/Save'; |
|||
|
|||
import { ENDPOINT_ROOT } from '../api'; |
|||
import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components'; |
|||
|
|||
import { LightMqttSettings } from './types'; |
|||
|
|||
export const LIGHT_BROKER_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "brokerSettings"; |
|||
|
|||
type LightMqttSettingsControllerProps = RestControllerProps<LightMqttSettings>; |
|||
|
|||
class LightMqttSettingsController extends Component<LightMqttSettingsControllerProps> { |
|||
|
|||
componentDidMount() { |
|||
this.props.loadData(); |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<SectionContent title='MQTT Controller' titleGutter> |
|||
<RestFormLoader |
|||
{...this.props} |
|||
render={props => ( |
|||
<LightMqttSettingsControllerForm {...props} /> |
|||
)} |
|||
/> |
|||
</SectionContent> |
|||
) |
|||
} |
|||
|
|||
} |
|||
|
|||
export default restController(LIGHT_BROKER_SETTINGS_ENDPOINT, LightMqttSettingsController); |
|||
|
|||
type LightMqttSettingsControllerFormProps = RestFormProps<LightMqttSettings>; |
|||
|
|||
function LightMqttSettingsControllerForm(props: LightMqttSettingsControllerFormProps) { |
|||
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"> |
|||
The LED is controllable via MQTT with the demo project designed to work with Home Assistant's auto discovery feature. |
|||
</Typography> |
|||
</Box> |
|||
<TextValidator |
|||
validators={['required']} |
|||
errorMessages={['Unique ID is required']} |
|||
name="unique_id" |
|||
label="Unique ID" |
|||
fullWidth |
|||
variant="outlined" |
|||
value={data.unique_id} |
|||
onChange={handleValueChange('unique_id')} |
|||
margin="normal" |
|||
/> |
|||
<TextValidator |
|||
validators={['required']} |
|||
errorMessages={['Name is required']} |
|||
name="name" |
|||
label="Name" |
|||
fullWidth |
|||
variant="outlined" |
|||
value={data.name} |
|||
onChange={handleValueChange('name')} |
|||
margin="normal" |
|||
/> |
|||
<TextValidator |
|||
validators={['required']} |
|||
errorMessages={['MQTT Path is required']} |
|||
name="mqtt_path" |
|||
label="MQTT Path" |
|||
fullWidth |
|||
variant="outlined" |
|||
value={data.mqtt_path} |
|||
onChange={handleValueChange('mqtt_path')} |
|||
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> |
|||
); |
|||
} |
@ -0,0 +1,70 @@ |
|||
import React, { Component } from 'react'; |
|||
import { ValidatorForm } from 'react-material-ui-form-validator'; |
|||
|
|||
import { Typography, Box, Checkbox } from '@material-ui/core'; |
|||
import SaveIcon from '@material-ui/icons/Save'; |
|||
|
|||
import { ENDPOINT_ROOT } from '../api'; |
|||
import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent, BlockFormControlLabel } from '../components'; |
|||
|
|||
import { LightState } from './types'; |
|||
|
|||
export const LIGHT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "lightState"; |
|||
|
|||
type LightStateRestControllerProps = RestControllerProps<LightState>; |
|||
|
|||
class LightStateRestController extends Component<LightStateRestControllerProps> { |
|||
|
|||
componentDidMount() { |
|||
this.props.loadData(); |
|||
} |
|||
|
|||
render() { |
|||
return ( |
|||
<SectionContent title='REST Controller' titleGutter> |
|||
<RestFormLoader |
|||
{...this.props} |
|||
render={props => ( |
|||
<LightStateRestControllerForm {...props} /> |
|||
)} |
|||
/> |
|||
</SectionContent> |
|||
) |
|||
} |
|||
|
|||
} |
|||
|
|||
export default restController(LIGHT_SETTINGS_ENDPOINT, LightStateRestController); |
|||
|
|||
type LightStateRestControllerFormProps = RestFormProps<LightState>; |
|||
|
|||
function LightStateRestControllerForm(props: LightStateRestControllerFormProps) { |
|||
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"> |
|||
The form below controls the LED via the RESTful service exposed by the ESP device. |
|||
</Typography> |
|||
</Box> |
|||
<BlockFormControlLabel |
|||
control={ |
|||
<Checkbox |
|||
checked={data.led_on} |
|||
onChange={handleValueChange('led_on')} |
|||
color="primary" |
|||
/> |
|||
} |
|||
label="LED State?" |
|||
/> |
|||
<FormActions> |
|||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit"> |
|||
Save |
|||
</FormButton> |
|||
<FormButton variant="contained" color="secondary" onClick={loadData}> |
|||
Reset |
|||
</FormButton> |
|||
</FormActions> |
|||
</ValidatorForm> |
|||
); |
|||
} |
@ -0,0 +1,62 @@ |
|||
import React, { Component } from 'react'; |
|||
import { ValidatorForm } from 'react-material-ui-form-validator'; |
|||
|
|||
import { Typography, Box, Switch } from '@material-ui/core'; |
|||
import { WEB_SOCKET_ROOT } from '../api'; |
|||
import { WebSocketControllerProps, WebSocketFormLoader, WebSocketFormProps, webSocketController } from '../components'; |
|||
import { SectionContent, BlockFormControlLabel } from '../components'; |
|||
|
|||
import { LightState } from './types'; |
|||
|
|||
export const LIGHT_SETTINGS_WEBSOCKET_URL = WEB_SOCKET_ROOT + "lightState"; |
|||
|
|||
type LightStateWebSocketControllerProps = WebSocketControllerProps<LightState>; |
|||
|
|||
class LightStateWebSocketController extends Component<LightStateWebSocketControllerProps> { |
|||
|
|||
render() { |
|||
return ( |
|||
<SectionContent title='WebSocket Controller' titleGutter> |
|||
<WebSocketFormLoader |
|||
{...this.props} |
|||
render={props => ( |
|||
<LightStateWebSocketControllerForm {...props} /> |
|||
)} |
|||
/> |
|||
</SectionContent> |
|||
) |
|||
} |
|||
|
|||
} |
|||
|
|||
export default webSocketController(LIGHT_SETTINGS_WEBSOCKET_URL, 100, LightStateWebSocketController); |
|||
|
|||
type LightStateWebSocketControllerFormProps = WebSocketFormProps<LightState>; |
|||
|
|||
function LightStateWebSocketControllerForm(props: LightStateWebSocketControllerFormProps) { |
|||
const { data, saveData, setData } = props; |
|||
|
|||
const changeLedOn = (event: React.ChangeEvent<HTMLInputElement>) => { |
|||
setData({ led_on: event.target.checked }, saveData); |
|||
} |
|||
|
|||
return ( |
|||
<ValidatorForm onSubmit={saveData}> |
|||
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}> |
|||
<Typography variant="body1"> |
|||
The switch below controls the LED via the WebSocket. It will automatically update whenever the LED state changes. |
|||
</Typography> |
|||
</Box> |
|||
<BlockFormControlLabel |
|||
control={ |
|||
<Switch |
|||
checked={data.led_on} |
|||
onChange={changeLedOn} |
|||
color="primary" |
|||
/> |
|||
} |
|||
label="LED State?" |
|||
/> |
|||
</ValidatorForm> |
|||
); |
|||
} |
@ -0,0 +1,9 @@ |
|||
export interface LightState { |
|||
led_on: boolean; |
|||
} |
|||
|
|||
export interface LightMqttSettings { |
|||
unique_id : string; |
|||
name: string; |
|||
mqtt_path : string; |
|||
} |
@ -1,50 +0,0 @@ |
|||
#ifndef AdminSettingsService_h |
|||
#define AdminSettingsService_h |
|||
|
|||
#include <SettingsService.h> |
|||
|
|||
template <class T> |
|||
class AdminSettingsService : public SettingsService<T> { |
|||
public: |
|||
AdminSettingsService(AsyncWebServer* server, |
|||
FS* fs, |
|||
SecurityManager* securityManager, |
|||
char const* servicePath, |
|||
char const* filePath) : |
|||
SettingsService<T>(server, fs, servicePath, filePath), |
|||
_securityManager(securityManager) { |
|||
} |
|||
|
|||
protected: |
|||
// will validate the requests with the security manager |
|||
SecurityManager* _securityManager; |
|||
|
|||
void fetchConfig(AsyncWebServerRequest* request) { |
|||
// verify the request against the predicate |
|||
Authentication authentication = _securityManager->authenticateRequest(request); |
|||
if (!getAuthenticationPredicate()(authentication)) { |
|||
request->send(401); |
|||
return; |
|||
} |
|||
// delegate to underlying implemetation |
|||
SettingsService<T>::fetchConfig(request); |
|||
} |
|||
|
|||
void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) { |
|||
// verify the request against the predicate |
|||
Authentication authentication = _securityManager->authenticateRequest(request); |
|||
if (!getAuthenticationPredicate()(authentication)) { |
|||
request->send(401); |
|||
return; |
|||
} |
|||
// delegate to underlying implemetation |
|||
SettingsService<T>::updateConfig(request, jsonDocument); |
|||
} |
|||
|
|||
// override this to replace the default authentication predicate, IS_ADMIN |
|||
AuthenticationPredicate getAuthenticationPredicate() { |
|||
return AuthenticationPredicates::IS_ADMIN; |
|||
} |
|||
}; |
|||
|
|||
#endif // end AdminSettingsService |
@ -1,33 +0,0 @@ |
|||
#ifndef _AsyncJsonCallbackResponse_H_ |
|||
#define _AsyncJsonCallbackResponse_H_ |
|||
|
|||
#include <AsyncJson.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
|
|||
/* |
|||
* Listens for a response being destroyed and calls a callback during said distruction. |
|||
* used so we can take action after the response has been rendered to the client. |
|||
* |
|||
* Avoids having to fork ESPAsyncWebServer with a callback feature, but not nice! |
|||
*/ |
|||
|
|||
typedef std::function<void()> AsyncJsonCallback; |
|||
|
|||
class AsyncJsonCallbackResponse : public AsyncJsonResponse { |
|||
private: |
|||
AsyncJsonCallback _callback; |
|||
|
|||
public: |
|||
AsyncJsonCallbackResponse(AsyncJsonCallback callback, |
|||
bool isArray = false, |
|||
size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : |
|||
AsyncJsonResponse(isArray, maxJsonBufferSize), |
|||
_callback{callback} { |
|||
} |
|||
|
|||
~AsyncJsonCallbackResponse() { |
|||
_callback(); |
|||
} |
|||
}; |
|||
|
|||
#endif // end _AsyncJsonCallbackResponse_H_ |
@ -1,131 +0,0 @@ |
|||
#ifndef Async_Json_Request_Web_Handler_H_ |
|||
#define Async_Json_Request_Web_Handler_H_ |
|||
|
|||
#include <ArduinoJson.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
|
|||
#define ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE 1024 |
|||
#define ASYNC_JSON_REQUEST_MIMETYPE "application/json" |
|||
|
|||
/* |
|||
* Handy little utility for dealing with small JSON request body payloads. |
|||
* |
|||
* Need to be careful using this as we are somewhat limited by RAM. |
|||
* |
|||
* Really only of use where there is a determinate payload size. |
|||
*/ |
|||
|
|||
typedef std::function<void(AsyncWebServerRequest* request, JsonDocument& jsonDocument)> JsonRequestCallback; |
|||
|
|||
class AsyncJsonWebHandler : public AsyncWebHandler { |
|||
private: |
|||
WebRequestMethodComposite _method; |
|||
JsonRequestCallback _onRequest; |
|||
size_t _maxContentLength; |
|||
|
|||
protected: |
|||
String _uri; |
|||
|
|||
public: |
|||
AsyncJsonWebHandler() : |
|||
_method(HTTP_POST | HTTP_PUT | HTTP_PATCH), |
|||
_onRequest(nullptr), |
|||
_maxContentLength(ASYNC_JSON_REQUEST_DEFAULT_MAX_SIZE), |
|||
_uri() { |
|||
} |
|||
|
|||
~AsyncJsonWebHandler() { |
|||
} |
|||
|
|||
void setUri(const String& uri) { |
|||
_uri = uri; |
|||
} |
|||
void setMethod(WebRequestMethodComposite method) { |
|||
_method = method; |
|||
} |
|||
void setMaxContentLength(size_t maxContentLength) { |
|||
_maxContentLength = maxContentLength; |
|||
} |
|||
void onRequest(JsonRequestCallback fn) { |
|||
_onRequest = fn; |
|||
} |
|||
|
|||
virtual bool canHandle(AsyncWebServerRequest* request) override final { |
|||
if (!_onRequest) |
|||
return false; |
|||
|
|||
if (!(_method & request->method())) |
|||
return false; |
|||
|
|||
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) |
|||
return false; |
|||
|
|||
if (!request->contentType().equalsIgnoreCase(ASYNC_JSON_REQUEST_MIMETYPE)) |
|||
return false; |
|||
|
|||
request->addInterestingHeader("ANY"); |
|||
return true; |
|||
} |
|||
|
|||
virtual void handleRequest(AsyncWebServerRequest* request) override final { |
|||
// no request configured |
|||
if (!_onRequest) { |
|||
Serial.print("No request callback was configured for endpoint: "); |
|||
Serial.println(_uri); |
|||
request->send(500); |
|||
return; |
|||
} |
|||
|
|||
// we have been handed too much data, return a 413 (payload too large) |
|||
if (request->contentLength() > _maxContentLength) { |
|||
request->send(413); |
|||
return; |
|||
} |
|||
|
|||
// parse JSON and if possible handle the request |
|||
if (request->_tempObject) { |
|||
DynamicJsonDocument jsonDocument(_maxContentLength); |
|||
DeserializationError error = deserializeJson(jsonDocument, (uint8_t*)request->_tempObject); |
|||
if (error == DeserializationError::Ok) { |
|||
_onRequest(request, jsonDocument); |
|||
} else { |
|||
request->send(400); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
// fallthrough, we have a null pointer, return 500. |
|||
// this can be due to running out of memory or never receiving body data. |
|||
request->send(500); |
|||
} |
|||
|
|||
virtual void handleBody(AsyncWebServerRequest* request, |
|||
uint8_t* data, |
|||
size_t len, |
|||
size_t index, |
|||
size_t total) override final { |
|||
if (_onRequest) { |
|||
// don't allocate if data is too large |
|||
if (total > _maxContentLength) { |
|||
return; |
|||
} |
|||
|
|||
// try to allocate memory on first call |
|||
// NB: the memory allocated here is freed by ~AsyncWebServerRequest |
|||
if (index == 0 && !request->_tempObject) { |
|||
request->_tempObject = malloc(total); |
|||
} |
|||
|
|||
// copy the data into the buffer, if we have a buffer! |
|||
if (request->_tempObject) { |
|||
memcpy((uint8_t*)request->_tempObject + index, data, len); |
|||
} |
|||
} |
|||
} |
|||
|
|||
virtual bool isRequestHandlerTrivial() override final { |
|||
return _onRequest ? false : true; |
|||
} |
|||
}; |
|||
|
|||
#endif // end Async_Json_Request_Web_Handler_H_ |
@ -0,0 +1,103 @@ |
|||
#ifndef FSPersistence_h |
|||
#define FSPersistence_h |
|||
|
|||
#include <StatefulService.h> |
|||
#include <JsonSerializer.h> |
|||
#include <JsonDeserializer.h> |
|||
#include <FS.h> |
|||
|
|||
#define MAX_FILE_SIZE 1024 |
|||
|
|||
template <class T> |
|||
class FSPersistence { |
|||
public: |
|||
FSPersistence(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
FS* fs, |
|||
char const* filePath) : |
|||
_jsonSerializer(jsonSerializer), |
|||
_jsonDeserializer(jsonDeserializer), |
|||
_statefulService(statefulService), |
|||
_fs(fs), |
|||
_filePath(filePath) { |
|||
enableUpdateHandler(); |
|||
} |
|||
|
|||
void readFromFS() { |
|||
File settingsFile = _fs->open(_filePath, "r"); |
|||
|
|||
if (settingsFile) { |
|||
if (settingsFile.size() <= MAX_FILE_SIZE) { |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); |
|||
DeserializationError error = deserializeJson(jsonDocument, settingsFile); |
|||
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) { |
|||
updateSettings(jsonDocument.as<JsonObject>()); |
|||
settingsFile.close(); |
|||
return; |
|||
} |
|||
} |
|||
settingsFile.close(); |
|||
} |
|||
|
|||
// If we reach here we have not been successful in loading the config, |
|||
// hard-coded emergency defaults are now applied. |
|||
applyDefaults(); |
|||
} |
|||
|
|||
bool writeToFS() { |
|||
// create and populate a new json object |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); |
|||
JsonObject jsonObject = jsonDocument.to<JsonObject>(); |
|||
_statefulService->read(jsonObject, _jsonSerializer); |
|||
|
|||
// serialize it to filesystem |
|||
File settingsFile = _fs->open(_filePath, "w"); |
|||
|
|||
// failed to open file, return false |
|||
if (!settingsFile) { |
|||
return false; |
|||
} |
|||
|
|||
// serialize the data to the file |
|||
serializeJson(jsonDocument, settingsFile); |
|||
settingsFile.close(); |
|||
return true; |
|||
} |
|||
|
|||
void disableUpdateHandler() { |
|||
if (_updateHandlerId) { |
|||
_statefulService->removeUpdateHandler(_updateHandlerId); |
|||
_updateHandlerId = 0; |
|||
} |
|||
} |
|||
|
|||
void enableUpdateHandler() { |
|||
if (!_updateHandlerId) { |
|||
_updateHandlerId = _statefulService->addUpdateHandler([&](String originId) { writeToFS(); }); |
|||
} |
|||
} |
|||
|
|||
private: |
|||
JsonSerializer<T> _jsonSerializer; |
|||
JsonDeserializer<T> _jsonDeserializer; |
|||
StatefulService<T>* _statefulService; |
|||
FS* _fs; |
|||
char const* _filePath; |
|||
update_handler_id_t _updateHandlerId = 0; |
|||
|
|||
// update the settings, but do not call propogate |
|||
void updateSettings(JsonObject root) { |
|||
_statefulService->updateWithoutPropagation(root, _jsonDeserializer); |
|||
} |
|||
|
|||
protected: |
|||
// We assume the deserializer supplies sensible defaults if an empty object |
|||
// is supplied, this virtual function allows that to be changed. |
|||
virtual void applyDefaults() { |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_FILE_SIZE); |
|||
updateSettings(jsonDocument.to<JsonObject>()); |
|||
} |
|||
}; |
|||
|
|||
#endif // end FSPersistence |
@ -0,0 +1,167 @@ |
|||
#ifndef HttpEndpoint_h |
|||
#define HttpEndpoint_h |
|||
|
|||
#include <functional> |
|||
|
|||
#include <AsyncJson.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
|
|||
#include <SecurityManager.h> |
|||
#include <StatefulService.h> |
|||
#include <JsonSerializer.h> |
|||
#include <JsonDeserializer.h> |
|||
|
|||
#define MAX_CONTENT_LENGTH 1024 |
|||
#define HTTP_ENDPOINT_ORIGIN_ID "http" |
|||
|
|||
template <class T> |
|||
class HttpGetEndpoint { |
|||
public: |
|||
HttpGetEndpoint(JsonSerializer<T> jsonSerializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
const String& servicePath, |
|||
SecurityManager* securityManager, |
|||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : |
|||
_jsonSerializer(jsonSerializer), _statefulService(statefulService) { |
|||
server->on(servicePath.c_str(), |
|||
HTTP_GET, |
|||
securityManager->wrapRequest(std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1), |
|||
authenticationPredicate)); |
|||
} |
|||
|
|||
HttpGetEndpoint(JsonSerializer<T> jsonSerializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
const String& servicePath) : |
|||
_jsonSerializer(jsonSerializer), _statefulService(statefulService) { |
|||
server->on(servicePath.c_str(), HTTP_GET, std::bind(&HttpGetEndpoint::fetchSettings, this, std::placeholders::_1)); |
|||
} |
|||
|
|||
protected: |
|||
JsonSerializer<T> _jsonSerializer; |
|||
StatefulService<T>* _statefulService; |
|||
|
|||
void fetchSettings(AsyncWebServerRequest* request) { |
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); |
|||
JsonObject jsonObject = response->getRoot().to<JsonObject>(); |
|||
_statefulService->read(jsonObject, _jsonSerializer); |
|||
|
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
|||
}; |
|||
|
|||
template <class T> |
|||
class HttpPostEndpoint { |
|||
public: |
|||
HttpPostEndpoint(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
const String& servicePath, |
|||
SecurityManager* securityManager, |
|||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : |
|||
_jsonSerializer(jsonSerializer), |
|||
_jsonDeserializer(jsonDeserializer), |
|||
_statefulService(statefulService), |
|||
_updateHandler( |
|||
servicePath, |
|||
securityManager->wrapCallback( |
|||
std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2), |
|||
authenticationPredicate)) { |
|||
_updateHandler.setMethod(HTTP_POST); |
|||
_updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); |
|||
server->addHandler(&_updateHandler); |
|||
} |
|||
|
|||
HttpPostEndpoint(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
const String& servicePath) : |
|||
_jsonSerializer(jsonSerializer), |
|||
_jsonDeserializer(jsonDeserializer), |
|||
_statefulService(statefulService), |
|||
_updateHandler(servicePath, |
|||
std::bind(&HttpPostEndpoint::updateSettings, this, std::placeholders::_1, std::placeholders::_2)) { |
|||
_updateHandler.setMethod(HTTP_POST); |
|||
_updateHandler.setMaxContentLength(MAX_CONTENT_LENGTH); |
|||
server->addHandler(&_updateHandler); |
|||
} |
|||
|
|||
protected: |
|||
JsonSerializer<T> _jsonSerializer; |
|||
JsonDeserializer<T> _jsonDeserializer; |
|||
StatefulService<T>* _statefulService; |
|||
AsyncCallbackJsonWebHandler _updateHandler; |
|||
|
|||
void fetchSettings(AsyncWebServerRequest* request) { |
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); |
|||
JsonObject jsonObject = response->getRoot().to<JsonObject>(); |
|||
_statefulService->read(jsonObject, _jsonSerializer); |
|||
|
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
|||
|
|||
void updateSettings(AsyncWebServerRequest* request, JsonVariant& json) { |
|||
if (json.is<JsonObject>()) { |
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_CONTENT_LENGTH); |
|||
|
|||
// use callback to update the settings once the response is complete |
|||
request->onDisconnect([this]() { _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); }); |
|||
|
|||
// update the settings, deferring the call to the update handlers to when the response is complete |
|||
_statefulService->updateWithoutPropagation([&](T& settings) { |
|||
JsonObject jsonObject = json.as<JsonObject>(); |
|||
_jsonDeserializer(jsonObject, settings); |
|||
jsonObject = response->getRoot().to<JsonObject>(); |
|||
_jsonSerializer(settings, jsonObject); |
|||
}); |
|||
|
|||
// write the response to the client |
|||
response->setLength(); |
|||
request->send(response); |
|||
} else { |
|||
request->send(400); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
template <class T> |
|||
class HttpEndpoint : public HttpGetEndpoint<T>, public HttpPostEndpoint<T> { |
|||
public: |
|||
HttpEndpoint(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
const String& servicePath, |
|||
SecurityManager* securityManager, |
|||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : |
|||
HttpGetEndpoint<T>(jsonSerializer, |
|||
statefulService, |
|||
server, |
|||
servicePath, |
|||
securityManager, |
|||
authenticationPredicate), |
|||
HttpPostEndpoint<T>(jsonSerializer, |
|||
jsonDeserializer, |
|||
statefulService, |
|||
server, |
|||
servicePath, |
|||
securityManager, |
|||
authenticationPredicate) { |
|||
} |
|||
|
|||
HttpEndpoint(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
const String& servicePath) : |
|||
HttpGetEndpoint<T>(jsonSerializer, statefulService, server, servicePath), |
|||
HttpPostEndpoint<T>(jsonSerializer, jsonDeserializer, statefulService, server, servicePath) { |
|||
} |
|||
}; |
|||
|
|||
#endif // end HttpEndpoint |
@ -0,0 +1,9 @@ |
|||
#ifndef JsonDeserializer_h |
|||
#define JsonDeserializer_h |
|||
|
|||
#include <ArduinoJson.h> |
|||
|
|||
template <class T> |
|||
using JsonDeserializer = void (*)(JsonObject& root, T& settings); |
|||
|
|||
#endif // end JsonDeserializer |
@ -0,0 +1,9 @@ |
|||
#ifndef JsonSerializer_h |
|||
#define JsonSerializer_h |
|||
|
|||
#include <ArduinoJson.h> |
|||
|
|||
template <class T> |
|||
using JsonSerializer = void (*)(T& settings, JsonObject& root); |
|||
|
|||
#endif // end JsonSerializer |
@ -0,0 +1,17 @@ |
|||
#include <Arduino.h> |
|||
#include <IPAddress.h> |
|||
#include <ArduinoJson.h> |
|||
|
|||
class JsonUtils { |
|||
public: |
|||
static void readIP(JsonObject& root, String key, IPAddress& _ip) { |
|||
if (!root[key].is<String>() || !_ip.fromString(root[key].as<String>())) { |
|||
_ip = INADDR_NONE; |
|||
} |
|||
} |
|||
static void writeIP(JsonObject& root, String key, IPAddress& _ip) { |
|||
if (_ip != INADDR_NONE) { |
|||
root[key] = _ip.toString(); |
|||
} |
|||
} |
|||
}; |
@ -0,0 +1,161 @@ |
|||
#ifndef MqttPubSub_h |
|||
#define MqttPubSub_h |
|||
|
|||
#include <StatefulService.h> |
|||
#include <JsonSerializer.h> |
|||
#include <JsonDeserializer.h> |
|||
#include <AsyncMqttClient.h> |
|||
|
|||
#define MAX_MESSAGE_SIZE 1024 |
|||
#define MQTT_ORIGIN_ID "mqtt" |
|||
|
|||
template <class T> |
|||
class MqttConnector { |
|||
protected: |
|||
StatefulService<T>* _statefulService; |
|||
AsyncMqttClient* _mqttClient; |
|||
|
|||
MqttConnector(StatefulService<T>* statefulService, AsyncMqttClient* mqttClient) : |
|||
_statefulService(statefulService), _mqttClient(mqttClient) { |
|||
_mqttClient->onConnect(std::bind(&MqttConnector::onConnect, this)); |
|||
} |
|||
|
|||
virtual void onConnect() = 0; |
|||
}; |
|||
|
|||
template <class T> |
|||
class MqttPub : virtual public MqttConnector<T> { |
|||
public: |
|||
MqttPub(JsonSerializer<T> jsonSerializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncMqttClient* mqttClient, |
|||
String pubTopic = "") : |
|||
MqttConnector<T>(statefulService, mqttClient), _jsonSerializer(jsonSerializer), _pubTopic(pubTopic) { |
|||
MqttConnector<T>::_statefulService->addUpdateHandler([&](String originId) { publish(); }, false); |
|||
} |
|||
|
|||
void setPubTopic(String pubTopic) { |
|||
_pubTopic = pubTopic; |
|||
publish(); |
|||
} |
|||
|
|||
protected: |
|||
virtual void onConnect() { |
|||
publish(); |
|||
} |
|||
|
|||
private: |
|||
JsonSerializer<T> _jsonSerializer; |
|||
String _pubTopic; |
|||
|
|||
void publish() { |
|||
if (_pubTopic.length() > 0 && MqttConnector<T>::_mqttClient->connected()) { |
|||
// serialize to json doc |
|||
DynamicJsonDocument json(MAX_MESSAGE_SIZE); |
|||
JsonObject jsonObject = json.to<JsonObject>(); |
|||
MqttConnector<T>::_statefulService->read(jsonObject, _jsonSerializer); |
|||
|
|||
// serialize to string |
|||
String payload; |
|||
serializeJson(json, payload); |
|||
|
|||
// publish the payload |
|||
MqttConnector<T>::_mqttClient->publish(_pubTopic.c_str(), 0, false, payload.c_str()); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
template <class T> |
|||
class MqttSub : virtual public MqttConnector<T> { |
|||
public: |
|||
MqttSub(JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncMqttClient* mqttClient, |
|||
String subTopic = "") : |
|||
MqttConnector<T>(statefulService, mqttClient), _jsonDeserializer(jsonDeserializer), _subTopic(subTopic) { |
|||
MqttConnector<T>::_mqttClient->onMessage(std::bind(&MqttSub::onMqttMessage, |
|||
this, |
|||
std::placeholders::_1, |
|||
std::placeholders::_2, |
|||
std::placeholders::_3, |
|||
std::placeholders::_4, |
|||
std::placeholders::_5, |
|||
std::placeholders::_6)); |
|||
} |
|||
|
|||
void setSubTopic(String subTopic) { |
|||
if (!_subTopic.equals(subTopic)) { |
|||
// unsubscribe from the existing topic if one was set |
|||
if (_subTopic.length() > 0) { |
|||
MqttConnector<T>::_mqttClient->unsubscribe(_subTopic.c_str()); |
|||
} |
|||
// set the new topic and re-configure the subscription |
|||
_subTopic = subTopic; |
|||
subscribe(); |
|||
} |
|||
} |
|||
|
|||
protected: |
|||
virtual void onConnect() { |
|||
subscribe(); |
|||
} |
|||
|
|||
private: |
|||
JsonDeserializer<T> _jsonDeserializer; |
|||
String _subTopic; |
|||
|
|||
void subscribe() { |
|||
if (_subTopic.length() > 0) { |
|||
MqttConnector<T>::_mqttClient->subscribe(_subTopic.c_str(), 2); |
|||
} |
|||
} |
|||
|
|||
void onMqttMessage(char* topic, |
|||
char* payload, |
|||
AsyncMqttClientMessageProperties properties, |
|||
size_t len, |
|||
size_t index, |
|||
size_t total) { |
|||
// we only care about the topic we are watching in this class |
|||
if (strcmp(_subTopic.c_str(), topic)) { |
|||
return; |
|||
} |
|||
|
|||
// deserialize from string |
|||
DynamicJsonDocument json(MAX_MESSAGE_SIZE); |
|||
DeserializationError error = deserializeJson(json, payload, len); |
|||
if (!error && json.is<JsonObject>()) { |
|||
JsonObject jsonObject = json.as<JsonObject>(); |
|||
MqttConnector<T>::_statefulService->update(jsonObject, _jsonDeserializer, MQTT_ORIGIN_ID); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
template <class T> |
|||
class MqttPubSub : public MqttPub<T>, public MqttSub<T> { |
|||
public: |
|||
MqttPubSub(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncMqttClient* mqttClient, |
|||
String pubTopic = "", |
|||
String subTopic = "") : |
|||
MqttConnector<T>(statefulService, mqttClient), |
|||
MqttPub<T>(jsonSerializer, statefulService, mqttClient, pubTopic = ""), |
|||
MqttSub<T>(jsonDeserializer, statefulService, mqttClient, subTopic = "") { |
|||
} |
|||
|
|||
public: |
|||
void configureTopics(String pubTopic, String subTopic) { |
|||
MqttSub<T>::setSubTopic(subTopic); |
|||
MqttPub<T>::setPubTopic(pubTopic); |
|||
} |
|||
|
|||
protected: |
|||
void onConnect() { |
|||
MqttSub<T>::onConnect(); |
|||
MqttPub<T>::onConnect(); |
|||
} |
|||
}; |
|||
|
|||
#endif // end MqttPubSub |
@ -0,0 +1,155 @@ |
|||
#include <MqttSettingsService.h>
|
|||
|
|||
/**
|
|||
* Retains a copy of the cstr provided in the pointer provided using dynamic allocation. |
|||
* |
|||
* Frees the pointer before allocation and leaves it as nullptr if cstr == nullptr. |
|||
*/ |
|||
static char* retainCstr(const char* cstr, char** ptr) { |
|||
// free up previously retained value if exists
|
|||
free(*ptr); |
|||
*ptr = nullptr; |
|||
|
|||
// dynamically allocate and copy cstr (if non null)
|
|||
if (cstr != nullptr) { |
|||
*ptr = (char*)malloc(strlen(cstr) + 1); |
|||
strcpy(*ptr, cstr); |
|||
} |
|||
|
|||
// return reference to pointer for convenience
|
|||
return *ptr; |
|||
} |
|||
|
|||
MqttSettingsService::MqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : |
|||
_httpEndpoint(MqttSettings::serialize, |
|||
MqttSettings::deserialize, |
|||
this, |
|||
server, |
|||
MQTT_SETTINGS_SERVICE_PATH, |
|||
securityManager), |
|||
_fsPersistence(MqttSettings::serialize, MqttSettings::deserialize, this, fs, MQTT_SETTINGS_FILE) { |
|||
#ifdef ESP32
|
|||
WiFi.onEvent( |
|||
std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), |
|||
WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); |
|||
WiFi.onEvent(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1, std::placeholders::_2), |
|||
WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP); |
|||
#elif defined(ESP8266)
|
|||
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected( |
|||
std::bind(&MqttSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); |
|||
_onStationModeGotIPHandler = |
|||
WiFi.onStationModeGotIP(std::bind(&MqttSettingsService::onStationModeGotIP, this, std::placeholders::_1)); |
|||
#endif
|
|||
_mqttClient.onConnect(std::bind(&MqttSettingsService::onMqttConnect, this, std::placeholders::_1)); |
|||
_mqttClient.onDisconnect(std::bind(&MqttSettingsService::onMqttDisconnect, this, std::placeholders::_1)); |
|||
addUpdateHandler([&](String originId) { onConfigUpdated(); }, false); |
|||
} |
|||
|
|||
MqttSettingsService::~MqttSettingsService() { |
|||
} |
|||
|
|||
void MqttSettingsService::begin() { |
|||
_fsPersistence.readFromFS(); |
|||
} |
|||
|
|||
void MqttSettingsService::loop() { |
|||
if (_reconfigureMqtt || (_disconnectedAt && (unsigned long)(millis() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) { |
|||
// reconfigure MQTT client
|
|||
configureMqtt(); |
|||
|
|||
// clear the reconnection flags
|
|||
_reconfigureMqtt = false; |
|||
_disconnectedAt = 0; |
|||
} |
|||
} |
|||
|
|||
bool MqttSettingsService::isEnabled() { |
|||
return _state.enabled; |
|||
} |
|||
|
|||
bool MqttSettingsService::isConnected() { |
|||
return _mqttClient.connected(); |
|||
} |
|||
|
|||
const char* MqttSettingsService::getClientId() { |
|||
return _mqttClient.getClientId(); |
|||
} |
|||
|
|||
AsyncMqttClientDisconnectReason MqttSettingsService::getDisconnectReason() { |
|||
return _disconnectReason; |
|||
} |
|||
|
|||
AsyncMqttClient* MqttSettingsService::getMqttClient() { |
|||
return &_mqttClient; |
|||
} |
|||
|
|||
void MqttSettingsService::onMqttConnect(bool sessionPresent) { |
|||
Serial.print("Connected to MQTT, "); |
|||
Serial.print(sessionPresent ? "with" : "without"); |
|||
Serial.println(" persistent session"); |
|||
} |
|||
|
|||
void MqttSettingsService::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) { |
|||
Serial.print("Disconnected from MQTT reason: "); |
|||
Serial.println((uint8_t)reason); |
|||
_disconnectReason = reason; |
|||
_disconnectedAt = millis(); |
|||
} |
|||
|
|||
void MqttSettingsService::onConfigUpdated() { |
|||
_reconfigureMqtt = true; |
|||
_disconnectedAt = 0; |
|||
} |
|||
|
|||
#ifdef ESP32
|
|||
void MqttSettingsService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) { |
|||
if (_state.enabled) { |
|||
Serial.println("WiFi connection dropped, starting MQTT client."); |
|||
onConfigUpdated(); |
|||
} |
|||
} |
|||
|
|||
void MqttSettingsService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) { |
|||
if (_state.enabled) { |
|||
Serial.println("WiFi connection dropped, stopping MQTT client."); |
|||
onConfigUpdated(); |
|||
} |
|||
} |
|||
#elif defined(ESP8266)
|
|||
void MqttSettingsService::onStationModeGotIP(const WiFiEventStationModeGotIP& event) { |
|||
if (_state.enabled) { |
|||
Serial.println("WiFi connection dropped, starting MQTT client."); |
|||
onConfigUpdated(); |
|||
} |
|||
} |
|||
|
|||
void MqttSettingsService::onStationModeDisconnected(const WiFiEventStationModeDisconnected& event) { |
|||
if (_state.enabled) { |
|||
Serial.println("WiFi connection dropped, stopping MQTT client."); |
|||
onConfigUpdated(); |
|||
} |
|||
} |
|||
#endif
|
|||
|
|||
void MqttSettingsService::configureMqtt() { |
|||
// disconnect if currently connected
|
|||
_mqttClient.disconnect(); |
|||
|
|||
// only connect if WiFi is connected and MQTT is enabled
|
|||
if (_state.enabled && WiFi.isConnected()) { |
|||
Serial.println("Connecting to MQTT..."); |
|||
_mqttClient.setServer(retainCstr(_state.host.c_str(), &_retainedHost), _state.port); |
|||
if (_state.username.length() > 0) { |
|||
_mqttClient.setCredentials( |
|||
retainCstr(_state.username.c_str(), &_retainedUsername), |
|||
retainCstr(_state.password.length() > 0 ? _state.password.c_str() : nullptr, &_retainedPassword)); |
|||
} else { |
|||
_mqttClient.setCredentials(retainCstr(nullptr, &_retainedUsername), retainCstr(nullptr, &_retainedPassword)); |
|||
} |
|||
_mqttClient.setClientId(retainCstr(_state.clientId.c_str(), &_retainedClientId)); |
|||
_mqttClient.setKeepAlive(_state.keepAlive); |
|||
_mqttClient.setCleanSession(_state.cleanSession); |
|||
_mqttClient.setMaxTopicLength(_state.maxTopicLength); |
|||
_mqttClient.connect(); |
|||
} |
|||
} |
@ -0,0 +1,125 @@ |
|||
#ifndef MqttSettingsService_h |
|||
#define MqttSettingsService_h |
|||
|
|||
#include <StatefulService.h> |
|||
#include <HttpEndpoint.h> |
|||
#include <FSPersistence.h> |
|||
#include <AsyncMqttClient.h> |
|||
|
|||
#define MQTT_RECONNECTION_DELAY 5000 |
|||
|
|||
#define MQTT_SETTINGS_FILE "/config/mqttSettings.json" |
|||
#define MQTT_SETTINGS_SERVICE_PATH "/rest/mqttSettings" |
|||
|
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED false |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_HOST "test.mosquitto.org" |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_PORT 1883 |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_USERNAME "" |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_PASSWORD "" |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_CLIENT_ID generateClientId() |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_KEEP_ALIVE 16 |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_CLEAN_SESSION true |
|||
#define MQTT_SETTINGS_SERVICE_DEFAULT_MAX_TOPIC_LENGTH 128 |
|||
|
|||
static String generateClientId() { |
|||
#ifdef ESP32 |
|||
return "esp32-" + String((unsigned long)ESP.getEfuseMac(), HEX); |
|||
#elif defined(ESP8266) |
|||
return "esp8266-" + String(ESP.getChipId(), HEX); |
|||
#endif |
|||
} |
|||
|
|||
class MqttSettings { |
|||
public: |
|||
// host and port - if enabled |
|||
bool enabled; |
|||
String host; |
|||
uint16_t port; |
|||
|
|||
// username and password |
|||
String username; |
|||
String password; |
|||
|
|||
// client id settings |
|||
String clientId; |
|||
|
|||
// connection settings |
|||
uint16_t keepAlive; |
|||
bool cleanSession; |
|||
uint16_t maxTopicLength; |
|||
|
|||
static void serialize(MqttSettings& settings, JsonObject& root) { |
|||
root["enabled"] = settings.enabled; |
|||
root["host"] = settings.host; |
|||
root["port"] = settings.port; |
|||
root["username"] = settings.username; |
|||
root["password"] = settings.password; |
|||
root["client_id"] = settings.clientId; |
|||
root["keep_alive"] = settings.keepAlive; |
|||
root["clean_session"] = settings.cleanSession; |
|||
root["max_topic_length"] = settings.maxTopicLength; |
|||
} |
|||
|
|||
static void deserialize(JsonObject& root, MqttSettings& settings) { |
|||
settings.enabled = root["enabled"] | MQTT_SETTINGS_SERVICE_DEFAULT_ENABLED; |
|||
settings.host = root["host"] | MQTT_SETTINGS_SERVICE_DEFAULT_HOST; |
|||
settings.port = root["port"] | MQTT_SETTINGS_SERVICE_DEFAULT_PORT; |
|||
settings.username = root["username"] | MQTT_SETTINGS_SERVICE_DEFAULT_USERNAME; |
|||
settings.password = root["password"] | MQTT_SETTINGS_SERVICE_DEFAULT_PASSWORD; |
|||
settings.clientId = root["client_id"] | MQTT_SETTINGS_SERVICE_DEFAULT_CLIENT_ID; |
|||
settings.keepAlive = root["keep_alive"] | MQTT_SETTINGS_SERVICE_DEFAULT_KEEP_ALIVE; |
|||
settings.cleanSession = root["clean_session"] | MQTT_SETTINGS_SERVICE_DEFAULT_CLEAN_SESSION; |
|||
settings.maxTopicLength = root["max_topic_length"] | MQTT_SETTINGS_SERVICE_DEFAULT_MAX_TOPIC_LENGTH; |
|||
} |
|||
}; |
|||
|
|||
class MqttSettingsService : public StatefulService<MqttSettings> { |
|||
public: |
|||
MqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); |
|||
~MqttSettingsService(); |
|||
|
|||
void begin(); |
|||
void loop(); |
|||
bool isEnabled(); |
|||
bool isConnected(); |
|||
const char* getClientId(); |
|||
AsyncMqttClientDisconnectReason getDisconnectReason(); |
|||
AsyncMqttClient* getMqttClient(); |
|||
|
|||
protected: |
|||
void onConfigUpdated(); |
|||
|
|||
private: |
|||
HttpEndpoint<MqttSettings> _httpEndpoint; |
|||
FSPersistence<MqttSettings> _fsPersistence; |
|||
|
|||
// Pointers to hold retained copies of the mqtt client connection strings. |
|||
// Required as AsyncMqttClient holds refrences to the supplied connection strings. |
|||
char* _retainedHost = nullptr; |
|||
char* _retainedClientId = nullptr; |
|||
char* _retainedUsername = nullptr; |
|||
char* _retainedPassword = nullptr; |
|||
|
|||
AsyncMqttClient _mqttClient; |
|||
bool _reconfigureMqtt; |
|||
unsigned long _disconnectedAt; |
|||
|
|||
// connection status |
|||
AsyncMqttClientDisconnectReason _disconnectReason; |
|||
|
|||
#ifdef ESP32 |
|||
void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info); |
|||
void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); |
|||
#elif defined(ESP8266) |
|||
WiFiEventHandler _onStationModeDisconnectedHandler; |
|||
WiFiEventHandler _onStationModeGotIPHandler; |
|||
void onStationModeGotIP(const WiFiEventStationModeGotIP& event); |
|||
void onStationModeDisconnected(const WiFiEventStationModeDisconnected& event); |
|||
#endif |
|||
|
|||
void onMqttConnect(bool sessionPresent); |
|||
void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); |
|||
void configureMqtt(); |
|||
}; |
|||
|
|||
#endif // end MqttSettingsService_h |
@ -0,0 +1,24 @@ |
|||
#include <MqttStatus.h>
|
|||
|
|||
MqttStatus::MqttStatus(AsyncWebServer* server, |
|||
MqttSettingsService* mqttSettingsService, |
|||
SecurityManager* securityManager) : |
|||
_mqttSettingsService(mqttSettingsService) { |
|||
server->on(MQTT_STATUS_SERVICE_PATH, |
|||
HTTP_GET, |
|||
securityManager->wrapRequest(std::bind(&MqttStatus::mqttStatus, this, std::placeholders::_1), |
|||
AuthenticationPredicates::IS_AUTHENTICATED)); |
|||
} |
|||
|
|||
void MqttStatus::mqttStatus(AsyncWebServerRequest* request) { |
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_MQTT_STATUS_SIZE); |
|||
JsonObject root = response->getRoot(); |
|||
|
|||
root["enabled"] = _mqttSettingsService->isEnabled(); |
|||
root["connected"] = _mqttSettingsService->isConnected(); |
|||
root["client_id"] = _mqttSettingsService->getClientId(); |
|||
root["disconnect_reason"] = (uint8_t)_mqttSettingsService->getDisconnectReason(); |
|||
|
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
@ -0,0 +1,31 @@ |
|||
#ifndef MqttStatus_h |
|||
#define MqttStatus_h |
|||
|
|||
#ifdef ESP32 |
|||
#include <WiFi.h> |
|||
#include <AsyncTCP.h> |
|||
#elif defined(ESP8266) |
|||
#include <ESP8266WiFi.h> |
|||
#include <ESPAsyncTCP.h> |
|||
#endif |
|||
|
|||
#include <MqttSettingsService.h> |
|||
#include <ArduinoJson.h> |
|||
#include <AsyncJson.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
#include <SecurityManager.h> |
|||
|
|||
#define MAX_MQTT_STATUS_SIZE 1024 |
|||
#define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus" |
|||
|
|||
class MqttStatus { |
|||
public: |
|||
MqttStatus(AsyncWebServer* server, MqttSettingsService* mqttSettingsService, SecurityManager* securityManager); |
|||
|
|||
private: |
|||
MqttSettingsService* _mqttSettingsService; |
|||
|
|||
void mqttStatus(AsyncWebServerRequest* request); |
|||
}; |
|||
|
|||
#endif // end MqttStatus_h |
@ -1,96 +0,0 @@ |
|||
#ifndef SettingsPersistence_h |
|||
#define SettingsPersistence_h |
|||
|
|||
#include <ArduinoJson.h> |
|||
#include <AsyncJson.h> |
|||
#include <AsyncJsonWebHandler.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
#include <FS.h> |
|||
|
|||
/** |
|||
* At the moment, not expecting settings service to have to deal with large JSON |
|||
* files this could be made configurable fairly simply, it's exposed on |
|||
* AsyncJsonWebHandler with a setter. |
|||
*/ |
|||
#define MAX_SETTINGS_SIZE 1024 |
|||
|
|||
/* |
|||
* Mixin for classes which need to save settings to/from a file on the the file system as JSON. |
|||
*/ |
|||
class SettingsPersistence { |
|||
protected: |
|||
// will store and retrieve config from the file system |
|||
FS* _fs; |
|||
|
|||
// the file path our settings will be saved to |
|||
char const* _filePath; |
|||
|
|||
bool writeToFS() { |
|||
// create and populate a new json object |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); |
|||
JsonObject root = jsonDocument.to<JsonObject>(); |
|||
writeToJsonObject(root); |
|||
|
|||
// serialize it to filesystem |
|||
File configFile = _fs->open(_filePath, "w"); |
|||
|
|||
// failed to open file, return false |
|||
if (!configFile) { |
|||
return false; |
|||
} |
|||
|
|||
serializeJson(jsonDocument, configFile); |
|||
configFile.close(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void readFromFS() { |
|||
File configFile = _fs->open(_filePath, "r"); |
|||
|
|||
// use defaults if no config found |
|||
if (configFile) { |
|||
// Protect against bad data uploaded to file system |
|||
// We never expect the config file to get very large, so cap it. |
|||
size_t size = configFile.size(); |
|||
if (size <= MAX_SETTINGS_SIZE) { |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); |
|||
DeserializationError error = deserializeJson(jsonDocument, configFile); |
|||
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) { |
|||
JsonObject root = jsonDocument.as<JsonObject>(); |
|||
readFromJsonObject(root); |
|||
configFile.close(); |
|||
return; |
|||
} |
|||
} |
|||
configFile.close(); |
|||
} |
|||
|
|||
// If we reach here we have not been successful in loading the config, |
|||
// hard-coded emergency defaults are now applied. |
|||
applyDefaultConfig(); |
|||
} |
|||
|
|||
// serialization routene, from local config to JsonObject |
|||
virtual void readFromJsonObject(JsonObject& root) { |
|||
} |
|||
virtual void writeToJsonObject(JsonObject& root) { |
|||
} |
|||
|
|||
// We assume the readFromJsonObject supplies sensible defaults if an empty object |
|||
// is supplied, this virtual function allows that to be changed. |
|||
virtual void applyDefaultConfig() { |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(MAX_SETTINGS_SIZE); |
|||
JsonObject root = jsonDocument.to<JsonObject>(); |
|||
readFromJsonObject(root); |
|||
} |
|||
|
|||
public: |
|||
SettingsPersistence(FS* fs, char const* filePath) : _fs(fs), _filePath(filePath) { |
|||
} |
|||
|
|||
virtual ~SettingsPersistence() { |
|||
} |
|||
}; |
|||
|
|||
#endif // end SettingsPersistence |
@ -1,166 +0,0 @@ |
|||
#ifndef SettingsService_h |
|||
#define SettingsService_h |
|||
|
|||
#include <functional> |
|||
|
|||
#ifdef ESP32 |
|||
#include <WiFi.h> |
|||
#include <AsyncTCP.h> |
|||
#elif defined(ESP8266) |
|||
#include <ESP8266WiFi.h> |
|||
#include <ESPAsyncTCP.h> |
|||
#endif |
|||
|
|||
#include <ArduinoJson.h> |
|||
#include <AsyncJson.h> |
|||
#include <AsyncJsonCallbackResponse.h> |
|||
#include <AsyncJsonWebHandler.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
#include <SecurityManager.h> |
|||
#include <SettingsPersistence.h> |
|||
|
|||
typedef size_t update_handler_id_t; |
|||
typedef std::function<void(void)> SettingsUpdateCallback; |
|||
static update_handler_id_t currentUpdateHandlerId; |
|||
|
|||
typedef struct SettingsUpdateHandlerInfo { |
|||
update_handler_id_t _id; |
|||
SettingsUpdateCallback _cb; |
|||
bool _allowRemove; |
|||
SettingsUpdateHandlerInfo(SettingsUpdateCallback cb, bool allowRemove) : |
|||
_id(++currentUpdateHandlerId), |
|||
_cb(cb), |
|||
_allowRemove(allowRemove){}; |
|||
} SettingsUpdateHandlerInfo_t; |
|||
|
|||
/* |
|||
* Abstraction of a service which stores it's settings as JSON in a file system. |
|||
*/ |
|||
template <class T> |
|||
class SettingsService : public SettingsPersistence { |
|||
public: |
|||
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath) : |
|||
SettingsPersistence(fs, filePath), |
|||
_servicePath(servicePath) { |
|||
server->on(_servicePath, HTTP_GET, std::bind(&SettingsService::fetchConfig, this, std::placeholders::_1)); |
|||
_updateHandler.setUri(servicePath); |
|||
_updateHandler.setMethod(HTTP_POST); |
|||
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); |
|||
_updateHandler.onRequest( |
|||
std::bind(&SettingsService::updateConfig, this, std::placeholders::_1, std::placeholders::_2)); |
|||
server->addHandler(&_updateHandler); |
|||
} |
|||
|
|||
virtual ~SettingsService() { |
|||
} |
|||
|
|||
update_handler_id_t addUpdateHandler(SettingsUpdateCallback cb, bool allowRemove = true) { |
|||
if (!cb) { |
|||
return 0; |
|||
} |
|||
SettingsUpdateHandlerInfo_t updateHandler(cb, allowRemove); |
|||
_settingsUpdateHandlers.push_back(updateHandler); |
|||
return updateHandler._id; |
|||
} |
|||
|
|||
void removeUpdateHandler(update_handler_id_t id) { |
|||
for (auto i = _settingsUpdateHandlers.begin(); i != _settingsUpdateHandlers.end();) { |
|||
if ((*i)._id == id) { |
|||
i = _settingsUpdateHandlers.erase(i); |
|||
} else { |
|||
++i; |
|||
} |
|||
} |
|||
} |
|||
|
|||
T fetch() { |
|||
return _settings; |
|||
} |
|||
|
|||
void update(T& settings) { |
|||
_settings = settings; |
|||
writeToFS(); |
|||
callUpdateHandlers(); |
|||
} |
|||
|
|||
void fetchAsString(String& config) { |
|||
DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE); |
|||
fetchAsDocument(jsonDocument); |
|||
serializeJson(jsonDocument, config); |
|||
} |
|||
|
|||
void updateFromString(String& config) { |
|||
DynamicJsonDocument jsonDocument(MAX_SETTINGS_SIZE); |
|||
deserializeJson(jsonDocument, config); |
|||
updateFromDocument(jsonDocument); |
|||
} |
|||
|
|||
void fetchAsDocument(JsonDocument& jsonDocument) { |
|||
JsonObject jsonObject = jsonDocument.to<JsonObject>(); |
|||
writeToJsonObject(jsonObject); |
|||
} |
|||
|
|||
void updateFromDocument(JsonDocument& jsonDocument) { |
|||
if (jsonDocument.is<JsonObject>()) { |
|||
JsonObject newConfig = jsonDocument.as<JsonObject>(); |
|||
readFromJsonObject(newConfig); |
|||
writeToFS(); |
|||
callUpdateHandlers(); |
|||
} |
|||
} |
|||
|
|||
void begin() { |
|||
// read the initial data from the file system |
|||
readFromFS(); |
|||
} |
|||
|
|||
protected: |
|||
T _settings; |
|||
char const* _servicePath; |
|||
AsyncJsonWebHandler _updateHandler; |
|||
std::list<SettingsUpdateHandlerInfo_t> _settingsUpdateHandlers; |
|||
|
|||
virtual void fetchConfig(AsyncWebServerRequest* request) { |
|||
// handle the request |
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); |
|||
JsonObject jsonObject = response->getRoot(); |
|||
writeToJsonObject(jsonObject); |
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
|||
|
|||
virtual void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) { |
|||
// handle the request |
|||
if (jsonDocument.is<JsonObject>()) { |
|||
JsonObject newConfig = jsonDocument.as<JsonObject>(); |
|||
readFromJsonObject(newConfig); |
|||
writeToFS(); |
|||
|
|||
// write settings back with a callback to reconfigure the wifi |
|||
AsyncJsonCallbackResponse* response = |
|||
new AsyncJsonCallbackResponse([this]() { callUpdateHandlers(); }, false, MAX_SETTINGS_SIZE); |
|||
JsonObject jsonObject = response->getRoot(); |
|||
writeToJsonObject(jsonObject); |
|||
response->setLength(); |
|||
request->send(response); |
|||
} else { |
|||
request->send(400); |
|||
} |
|||
} |
|||
|
|||
void callUpdateHandlers() { |
|||
// call the classes own config update function |
|||
onConfigUpdated(); |
|||
|
|||
// call all setting update handlers |
|||
for (const SettingsUpdateHandlerInfo_t& handler : _settingsUpdateHandlers) { |
|||
handler._cb(); |
|||
} |
|||
} |
|||
|
|||
// implement to perform action when config has been updated |
|||
virtual void onConfigUpdated() { |
|||
} |
|||
}; |
|||
|
|||
#endif // end SettingsService |
@ -1,87 +0,0 @@ |
|||
#ifndef Service_h |
|||
#define Service_h |
|||
|
|||
#ifdef ESP32 |
|||
#include <WiFi.h> |
|||
#include <AsyncTCP.h> |
|||
#elif defined(ESP8266) |
|||
#include <ESP8266WiFi.h> |
|||
#include <ESPAsyncTCP.h> |
|||
#endif |
|||
|
|||
#include <ArduinoJson.h> |
|||
#include <AsyncJson.h> |
|||
#include <AsyncJsonCallbackResponse.h> |
|||
#include <AsyncJsonWebHandler.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
|
|||
/** |
|||
* At the moment, not expecting services to have to deal with large JSON |
|||
* files this could be made configurable fairly simply, it's exposed on |
|||
* AsyncJsonWebHandler with a setter. |
|||
*/ |
|||
#define MAX_SETTINGS_SIZE 1024 |
|||
|
|||
/* |
|||
* Abstraction of a service which reads and writes data from an endpoint. |
|||
* |
|||
* Not currently used, but indended for use by features which do not |
|||
* require setting persistance. |
|||
*/ |
|||
class SimpleService { |
|||
private: |
|||
AsyncJsonWebHandler _updateHandler; |
|||
|
|||
void fetchConfig(AsyncWebServerRequest* request) { |
|||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_SETTINGS_SIZE); |
|||
JsonObject jsonObject = response->getRoot(); |
|||
writeToJsonObject(jsonObject); |
|||
response->setLength(); |
|||
request->send(response); |
|||
} |
|||
|
|||
void updateConfig(AsyncWebServerRequest* request, JsonDocument& jsonDocument) { |
|||
if (jsonDocument.is<JsonObject>()) { |
|||
JsonObject newConfig = jsonDocument.as<JsonObject>(); |
|||
readFromJsonObject(newConfig); |
|||
|
|||
// write settings back with a callback to reconfigure the wifi |
|||
AsyncJsonCallbackResponse* response = |
|||
new AsyncJsonCallbackResponse([this]() { onConfigUpdated(); }, false, MAX_SETTINGS_SIZE); |
|||
JsonObject jsonObject = response->getRoot(); |
|||
writeToJsonObject(jsonObject); |
|||
response->setLength(); |
|||
request->send(response); |
|||
} else { |
|||
request->send(400); |
|||
} |
|||
} |
|||
|
|||
protected: |
|||
// reads the local config from the |
|||
virtual void readFromJsonObject(JsonObject& root) { |
|||
} |
|||
virtual void writeToJsonObject(JsonObject& root) { |
|||
} |
|||
|
|||
// implement to perform action when config has been updated |
|||
virtual void onConfigUpdated() { |
|||
} |
|||
|
|||
public: |
|||
SimpleService(AsyncWebServer* server, char const* servicePath) { |
|||
server->on(servicePath, HTTP_GET, std::bind(&SimpleService::fetchConfig, this, std::placeholders::_1)); |
|||
|
|||
_updateHandler.setUri(servicePath); |
|||
_updateHandler.setMethod(HTTP_POST); |
|||
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); |
|||
_updateHandler.onRequest( |
|||
std::bind(&SimpleService::updateConfig, this, std::placeholders::_1, std::placeholders::_2)); |
|||
server->addHandler(&_updateHandler); |
|||
} |
|||
|
|||
virtual ~SimpleService() { |
|||
} |
|||
}; |
|||
|
|||
#endif // end SimpleService |
@ -0,0 +1,137 @@ |
|||
#ifndef StatefulService_h |
|||
#define StatefulService_h |
|||
|
|||
#include <Arduino.h> |
|||
#include <JsonDeserializer.h> |
|||
#include <JsonSerializer.h> |
|||
|
|||
#include <list> |
|||
#include <functional> |
|||
#ifdef ESP32 |
|||
#include <freertos/FreeRTOS.h> |
|||
#include <freertos/semphr.h> |
|||
#endif |
|||
|
|||
typedef size_t update_handler_id_t; |
|||
typedef std::function<void(String originId)> StateUpdateCallback; |
|||
static update_handler_id_t currentUpdatedHandlerId; |
|||
|
|||
typedef struct StateUpdateHandlerInfo { |
|||
update_handler_id_t _id; |
|||
StateUpdateCallback _cb; |
|||
bool _allowRemove; |
|||
StateUpdateHandlerInfo(StateUpdateCallback cb, bool allowRemove) : |
|||
_id(++currentUpdatedHandlerId), _cb(cb), _allowRemove(allowRemove){}; |
|||
} StateUpdateHandlerInfo_t; |
|||
|
|||
template <class T> |
|||
class StatefulService { |
|||
public: |
|||
template <typename... Args> |
|||
#ifdef ESP32 |
|||
StatefulService(Args&&... args) : |
|||
_state(std::forward<Args>(args)...), _accessMutex(xSemaphoreCreateRecursiveMutex()) { |
|||
} |
|||
#else |
|||
StatefulService(Args&&... args) : _state(std::forward<Args>(args)...) { |
|||
} |
|||
#endif |
|||
|
|||
update_handler_id_t addUpdateHandler(StateUpdateCallback cb, bool allowRemove = true) { |
|||
if (!cb) { |
|||
return 0; |
|||
} |
|||
StateUpdateHandlerInfo_t updateHandler(cb, allowRemove); |
|||
_updateHandlers.push_back(updateHandler); |
|||
return updateHandler._id; |
|||
} |
|||
|
|||
void removeUpdateHandler(update_handler_id_t id) { |
|||
for (auto i = _updateHandlers.begin(); i != _updateHandlers.end();) { |
|||
if ((*i)._allowRemove && (*i)._id == id) { |
|||
i = _updateHandlers.erase(i); |
|||
} else { |
|||
++i; |
|||
} |
|||
} |
|||
} |
|||
|
|||
void updateWithoutPropagation(std::function<void(T&)> callback) { |
|||
#ifdef ESP32 |
|||
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); |
|||
#endif |
|||
callback(_state); |
|||
#ifdef ESP32 |
|||
xSemaphoreGiveRecursive(_accessMutex); |
|||
#endif |
|||
} |
|||
|
|||
void updateWithoutPropagation(JsonObject& jsonObject, JsonDeserializer<T> deserializer) { |
|||
#ifdef ESP32 |
|||
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); |
|||
#endif |
|||
deserializer(jsonObject, _state); |
|||
#ifdef ESP32 |
|||
xSemaphoreGiveRecursive(_accessMutex); |
|||
#endif |
|||
} |
|||
|
|||
void update(std::function<void(T&)> callback, String originId) { |
|||
#ifdef ESP32 |
|||
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); |
|||
#endif |
|||
callback(_state); |
|||
callUpdateHandlers(originId); |
|||
#ifdef ESP32 |
|||
xSemaphoreGiveRecursive(_accessMutex); |
|||
#endif |
|||
} |
|||
|
|||
void update(JsonObject& jsonObject, JsonDeserializer<T> deserializer, String originId) { |
|||
#ifdef ESP32 |
|||
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); |
|||
#endif |
|||
deserializer(jsonObject, _state); |
|||
callUpdateHandlers(originId); |
|||
#ifdef ESP32 |
|||
xSemaphoreGiveRecursive(_accessMutex); |
|||
#endif |
|||
} |
|||
|
|||
void read(std::function<void(T&)> callback) { |
|||
#ifdef ESP32 |
|||
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); |
|||
#endif |
|||
callback(_state); |
|||
#ifdef ESP32 |
|||
xSemaphoreGiveRecursive(_accessMutex); |
|||
#endif |
|||
} |
|||
|
|||
void read(JsonObject& jsonObject, JsonSerializer<T> serializer) { |
|||
#ifdef ESP32 |
|||
xSemaphoreTakeRecursive(_accessMutex, portMAX_DELAY); |
|||
#endif |
|||
serializer(_state, jsonObject); |
|||
#ifdef ESP32 |
|||
xSemaphoreGiveRecursive(_accessMutex); |
|||
#endif |
|||
} |
|||
|
|||
void callUpdateHandlers(String originId) { |
|||
for (const StateUpdateHandlerInfo_t& updateHandler : _updateHandlers) { |
|||
updateHandler._cb(originId); |
|||
} |
|||
} |
|||
|
|||
protected: |
|||
T _state; |
|||
|
|||
private: |
|||
#ifdef ESP32 |
|||
SemaphoreHandle_t _accessMutex; |
|||
#endif |
|||
std::list<StateUpdateHandlerInfo_t> _updateHandlers; |
|||
}; |
|||
|
|||
#endif // end StatefulService_h |
@ -0,0 +1,242 @@ |
|||
#ifndef WebSocketTxRx_h |
|||
#define WebSocketTxRx_h |
|||
|
|||
#include <StatefulService.h> |
|||
#include <JsonSerializer.h> |
|||
#include <JsonDeserializer.h> |
|||
#include <ESPAsyncWebServer.h> |
|||
|
|||
#define WEB_SOCKET_MSG_SIZE 1024 |
|||
#define WEB_SOCKET_CLIENT_ID_MSG_SIZE 128 |
|||
|
|||
#define WEB_SOCKET_ORIGIN "websocket" |
|||
#define WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX "websocket:" |
|||
|
|||
template <class T> |
|||
class WebSocketConnector { |
|||
protected: |
|||
StatefulService<T>* _statefulService; |
|||
AsyncWebServer* _server; |
|||
AsyncWebSocket _webSocket; |
|||
|
|||
WebSocketConnector(StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
char const* webSocketPath, |
|||
SecurityManager* securityManager, |
|||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : |
|||
_statefulService(statefulService), _server(server), _webSocket(webSocketPath) { |
|||
_webSocket.setFilter(securityManager->filterRequest(authenticationPredicate)); |
|||
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, |
|||
this, |
|||
std::placeholders::_1, |
|||
std::placeholders::_2, |
|||
std::placeholders::_3, |
|||
std::placeholders::_4, |
|||
std::placeholders::_5, |
|||
std::placeholders::_6)); |
|||
_server->addHandler(&_webSocket); |
|||
_server->on(webSocketPath, HTTP_GET, std::bind(&WebSocketConnector::forbidden, this, std::placeholders::_1)); |
|||
} |
|||
|
|||
WebSocketConnector(StatefulService<T>* statefulService, AsyncWebServer* server, char const* webSocketPath) : |
|||
_statefulService(statefulService), _server(server), _webSocket(webSocketPath) { |
|||
_webSocket.onEvent(std::bind(&WebSocketConnector::onWSEvent, |
|||
this, |
|||
std::placeholders::_1, |
|||
std::placeholders::_2, |
|||
std::placeholders::_3, |
|||
std::placeholders::_4, |
|||
std::placeholders::_5, |
|||
std::placeholders::_6)); |
|||
_server->addHandler(&_webSocket); |
|||
} |
|||
|
|||
virtual void onWSEvent(AsyncWebSocket* server, |
|||
AsyncWebSocketClient* client, |
|||
AwsEventType type, |
|||
void* arg, |
|||
uint8_t* data, |
|||
size_t len) = 0; |
|||
|
|||
String clientId(AsyncWebSocketClient* client) { |
|||
return WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX + String(client->id()); |
|||
} |
|||
|
|||
private: |
|||
void forbidden(AsyncWebServerRequest* request) { |
|||
request->send(403); |
|||
} |
|||
}; |
|||
|
|||
template <class T> |
|||
class WebSocketTx : virtual public WebSocketConnector<T> { |
|||
public: |
|||
WebSocketTx(JsonSerializer<T> jsonSerializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
char const* webSocketPath, |
|||
SecurityManager* securityManager, |
|||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : |
|||
WebSocketConnector<T>(statefulService, server, webSocketPath, securityManager, authenticationPredicate), |
|||
_jsonSerializer(jsonSerializer) { |
|||
WebSocketConnector<T>::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, |
|||
false); |
|||
} |
|||
|
|||
WebSocketTx(JsonSerializer<T> jsonSerializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
char const* webSocketPath) : |
|||
WebSocketConnector<T>(statefulService, server, webSocketPath), _jsonSerializer(jsonSerializer) { |
|||
WebSocketConnector<T>::_statefulService->addUpdateHandler([&](String originId) { transmitData(nullptr, originId); }, |
|||
false); |
|||
} |
|||
|
|||
protected: |
|||
virtual void onWSEvent(AsyncWebSocket* server, |
|||
AsyncWebSocketClient* client, |
|||
AwsEventType type, |
|||
void* arg, |
|||
uint8_t* data, |
|||
size_t len) { |
|||
if (type == WS_EVT_CONNECT) { |
|||
// when a client connects, we transmit it's id and the current payload |
|||
transmitId(client); |
|||
transmitData(client, WEB_SOCKET_ORIGIN); |
|||
} |
|||
} |
|||
|
|||
private: |
|||
JsonSerializer<T> _jsonSerializer; |
|||
|
|||
void transmitId(AsyncWebSocketClient* client) { |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_CLIENT_ID_MSG_SIZE); |
|||
JsonObject root = jsonDocument.to<JsonObject>(); |
|||
root["type"] = "id"; |
|||
root["id"] = WebSocketConnector<T>::clientId(client); |
|||
size_t len = measureJson(jsonDocument); |
|||
AsyncWebSocketMessageBuffer* buffer = WebSocketConnector<T>::_webSocket.makeBuffer(len); |
|||
if (buffer) { |
|||
serializeJson(jsonDocument, (char*)buffer->get(), len + 1); |
|||
client->text(buffer); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if |
|||
* specified. |
|||
* |
|||
* Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach |
|||
* simplifies the client and the server implementation but may not be sufficent for all use-cases. |
|||
*/ |
|||
void transmitData(AsyncWebSocketClient* client, String originId) { |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_MSG_SIZE); |
|||
JsonObject root = jsonDocument.to<JsonObject>(); |
|||
root["type"] = "payload"; |
|||
root["origin_id"] = originId; |
|||
JsonObject payload = root.createNestedObject("payload"); |
|||
WebSocketConnector<T>::_statefulService->read(payload, _jsonSerializer); |
|||
|
|||
size_t len = measureJson(jsonDocument); |
|||
AsyncWebSocketMessageBuffer* buffer = WebSocketConnector<T>::_webSocket.makeBuffer(len); |
|||
if (buffer) { |
|||
serializeJson(jsonDocument, (char*)buffer->get(), len + 1); |
|||
if (client) { |
|||
client->text(buffer); |
|||
} else { |
|||
WebSocketConnector<T>::_webSocket.textAll(buffer); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
template <class T> |
|||
class WebSocketRx : virtual public WebSocketConnector<T> { |
|||
public: |
|||
WebSocketRx(JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
char const* webSocketPath, |
|||
SecurityManager* securityManager, |
|||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : |
|||
WebSocketConnector<T>(statefulService, server, webSocketPath, securityManager, authenticationPredicate), |
|||
_jsonDeserializer(jsonDeserializer) { |
|||
} |
|||
|
|||
WebSocketRx(JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
char const* webSocketPath) : |
|||
WebSocketConnector<T>(statefulService, server, webSocketPath), _jsonDeserializer(jsonDeserializer) { |
|||
} |
|||
|
|||
protected: |
|||
virtual void onWSEvent(AsyncWebSocket* server, |
|||
AsyncWebSocketClient* client, |
|||
AwsEventType type, |
|||
void* arg, |
|||
uint8_t* data, |
|||
size_t len) { |
|||
if (type == WS_EVT_DATA) { |
|||
AwsFrameInfo* info = (AwsFrameInfo*)arg; |
|||
if (info->final && info->index == 0 && info->len == len) { |
|||
if (info->opcode == WS_TEXT) { |
|||
DynamicJsonDocument jsonDocument = DynamicJsonDocument(WEB_SOCKET_MSG_SIZE); |
|||
DeserializationError error = deserializeJson(jsonDocument, (char*)data); |
|||
if (!error && jsonDocument.is<JsonObject>()) { |
|||
JsonObject jsonObject = jsonDocument.as<JsonObject>(); |
|||
WebSocketConnector<T>::_statefulService->update( |
|||
jsonObject, _jsonDeserializer, WebSocketConnector<T>::clientId(client)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private: |
|||
JsonDeserializer<T> _jsonDeserializer; |
|||
}; |
|||
|
|||
template <class T> |
|||
class WebSocketTxRx : public WebSocketTx<T>, public WebSocketRx<T> { |
|||
public: |
|||
WebSocketTxRx(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
char const* webSocketPath, |
|||
SecurityManager* securityManager, |
|||
AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : |
|||
WebSocketConnector<T>(statefulService, server, webSocketPath, securityManager, authenticationPredicate), |
|||
WebSocketTx<T>(jsonSerializer, statefulService, server, webSocketPath, securityManager, authenticationPredicate), |
|||
WebSocketRx<T>(jsonDeserializer, |
|||
statefulService, |
|||
server, |
|||
webSocketPath, |
|||
securityManager, |
|||
authenticationPredicate) { |
|||
} |
|||
|
|||
WebSocketTxRx(JsonSerializer<T> jsonSerializer, |
|||
JsonDeserializer<T> jsonDeserializer, |
|||
StatefulService<T>* statefulService, |
|||
AsyncWebServer* server, |
|||
char const* webSocketPath) : |
|||
WebSocketConnector<T>(statefulService, server, webSocketPath), |
|||
WebSocketTx<T>(jsonSerializer, statefulService, server, webSocketPath), |
|||
WebSocketRx<T>(jsonDeserializer, statefulService, server, webSocketPath) { |
|||
} |
|||
|
|||
protected: |
|||
void onWSEvent(AsyncWebSocket* server, |
|||
AsyncWebSocketClient* client, |
|||
AwsEventType type, |
|||
void* arg, |
|||
uint8_t* data, |
|||
size_t len) { |
|||
WebSocketRx<T>::onWSEvent(server, client, type, arg, data, len); |
|||
WebSocketTx<T>::onWSEvent(server, client, type, arg, data, len); |
|||
} |
|||
}; |
|||
|
|||
#endif |
After Width: 856 | Height: 587 | Size: 57 KiB |
@ -1,27 +0,0 @@ |
|||
#include <DemoProject.h>
|
|||
|
|||
DemoProject::DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : |
|||
AdminSettingsService(server, fs, securityManager, DEMO_SETTINGS_PATH, DEMO_SETTINGS_FILE) { |
|||
pinMode(BLINK_LED, OUTPUT); |
|||
} |
|||
|
|||
DemoProject::~DemoProject() { |
|||
} |
|||
|
|||
void DemoProject::loop() { |
|||
unsigned delay = MAX_DELAY / 255 * (255 - _settings.blinkSpeed); |
|||
unsigned long currentMillis = millis(); |
|||
if (!_lastBlink || (unsigned long)(currentMillis - _lastBlink) >= delay) { |
|||
_lastBlink = currentMillis; |
|||
digitalWrite(BLINK_LED, !digitalRead(BLINK_LED)); |
|||
} |
|||
} |
|||
|
|||
void DemoProject::readFromJsonObject(JsonObject& root) { |
|||
_settings.blinkSpeed = root["blink_speed"] | DEFAULT_BLINK_SPEED; |
|||
} |
|||
|
|||
void DemoProject::writeToJsonObject(JsonObject& root) { |
|||
// connection settings
|
|||
root["blink_speed"] = _settings.blinkSpeed; |
|||
} |
@ -1,34 +0,0 @@ |
|||
#ifndef DemoProject_h |
|||
#define DemoProject_h |
|||
|
|||
#include <AdminSettingsService.h> |
|||
#include <ESP8266React.h> |
|||
|
|||
#define BLINK_LED 2 |
|||
#define MAX_DELAY 1000 |
|||
|
|||
#define DEFAULT_BLINK_SPEED 100 |
|||
#define DEMO_SETTINGS_FILE "/config/demoSettings.json" |
|||
#define DEMO_SETTINGS_PATH "/rest/demoSettings" |
|||
|
|||
class DemoSettings { |
|||
public: |
|||
uint8_t blinkSpeed; |
|||
}; |
|||
|
|||
class DemoProject : public AdminSettingsService<DemoSettings> { |
|||
public: |
|||
DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); |
|||
~DemoProject(); |
|||
|
|||
void loop(); |
|||
|
|||
private: |
|||
unsigned long _lastBlink = 0; |
|||
|
|||
protected: |
|||
void readFromJsonObject(JsonObject& root); |
|||
void writeToJsonObject(JsonObject& root); |
|||
}; |
|||
|
|||
#endif |
@ -0,0 +1,16 @@ |
|||
#include <LightMqttSettingsService.h>
|
|||
|
|||
LightMqttSettingsService::LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : |
|||
_httpEndpoint(LightMqttSettings::serialize, |
|||
LightMqttSettings::deserialize, |
|||
this, |
|||
server, |
|||
LIGHT_BROKER_SETTINGS_PATH, |
|||
securityManager, |
|||
AuthenticationPredicates::IS_AUTHENTICATED), |
|||
_fsPersistence(LightMqttSettings::serialize, LightMqttSettings::deserialize, this, fs, LIGHT_BROKER_SETTINGS_FILE) { |
|||
} |
|||
|
|||
void LightMqttSettingsService::begin() { |
|||
_fsPersistence.readFromFS(); |
|||
} |
@ -0,0 +1,47 @@ |
|||
#ifndef LightMqttSettingsService_h |
|||
#define LightMqttSettingsService_h |
|||
|
|||
#include <HttpEndpoint.h> |
|||
#include <FSPersistence.h> |
|||
|
|||
#define LIGHT_BROKER_SETTINGS_FILE "/config/brokerSettings.json" |
|||
#define LIGHT_BROKER_SETTINGS_PATH "/rest/brokerSettings" |
|||
|
|||
static String defaultDeviceValue(String prefix = "") { |
|||
#ifdef ESP32 |
|||
return prefix + String((unsigned long)ESP.getEfuseMac(), HEX); |
|||
#elif defined(ESP8266) |
|||
return prefix + String(ESP.getChipId(), HEX); |
|||
#endif |
|||
} |
|||
|
|||
class LightMqttSettings { |
|||
public: |
|||
String mqttPath; |
|||
String name; |
|||
String uniqueId; |
|||
|
|||
static void serialize(LightMqttSettings& settings, JsonObject& root) { |
|||
root["mqtt_path"] = settings.mqttPath; |
|||
root["name"] = settings.name; |
|||
root["unique_id"] = settings.uniqueId; |
|||
} |
|||
|
|||
static void deserialize(JsonObject& root, LightMqttSettings& settings) { |
|||
settings.mqttPath = root["mqtt_path"] | defaultDeviceValue("homeassistant/light/"); |
|||
settings.name = root["name"] | defaultDeviceValue("light-"); |
|||
settings.uniqueId = root["unique_id"] | defaultDeviceValue("light-"); |
|||
} |
|||
}; |
|||
|
|||
class LightMqttSettingsService : public StatefulService<LightMqttSettings> { |
|||
public: |
|||
LightMqttSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); |
|||
void begin(); |
|||
|
|||
private: |
|||
HttpEndpoint<LightMqttSettings> _httpEndpoint; |
|||
FSPersistence<LightMqttSettings> _fsPersistence; |
|||
}; |
|||
|
|||
#endif // end LightMqttSettingsService_h |
@ -0,0 +1,73 @@ |
|||
#include <LightStateService.h>
|
|||
|
|||
LightStateService::LightStateService(AsyncWebServer* server, |
|||
SecurityManager* securityManager, |
|||
AsyncMqttClient* mqttClient, |
|||
LightMqttSettingsService* lightMqttSettingsService) : |
|||
_httpEndpoint(LightState::serialize, |
|||
LightState::deserialize, |
|||
this, |
|||
server, |
|||
LIGHT_SETTINGS_ENDPOINT_PATH, |
|||
securityManager, |
|||
AuthenticationPredicates::IS_AUTHENTICATED), |
|||
_mqttPubSub(LightState::haSerialize, LightState::haDeserialize, this, mqttClient), |
|||
_webSocket(LightState::serialize, |
|||
LightState::deserialize, |
|||
this, |
|||
server, |
|||
LIGHT_SETTINGS_SOCKET_PATH, |
|||
securityManager, |
|||
AuthenticationPredicates::IS_AUTHENTICATED), |
|||
_mqttClient(mqttClient), |
|||
_lightMqttSettingsService(lightMqttSettingsService) { |
|||
// configure blink led to be output
|
|||
pinMode(BLINK_LED, OUTPUT); |
|||
|
|||
// configure MQTT callback
|
|||
_mqttClient->onConnect(std::bind(&LightStateService::registerConfig, this)); |
|||
|
|||
// configure update handler for when the light settings change
|
|||
_lightMqttSettingsService->addUpdateHandler([&](String originId) { registerConfig(); }, false); |
|||
|
|||
// configure settings service update handler to update LED state
|
|||
addUpdateHandler([&](String originId) { onConfigUpdated(); }, false); |
|||
} |
|||
|
|||
void LightStateService::begin() { |
|||
_state.ledOn = DEFAULT_LED_STATE; |
|||
onConfigUpdated(); |
|||
} |
|||
|
|||
void LightStateService::onConfigUpdated() { |
|||
digitalWrite(BLINK_LED, _state.ledOn ? LED_ON : LED_OFF); |
|||
} |
|||
|
|||
void LightStateService::registerConfig() { |
|||
if (!_mqttClient->connected()) { |
|||
return; |
|||
} |
|||
String configTopic; |
|||
String setTopic; |
|||
String stateTopic; |
|||
|
|||
DynamicJsonDocument doc(256); |
|||
_lightMqttSettingsService->read([&](LightMqttSettings& settings) { |
|||
configTopic = settings.mqttPath + "/config"; |
|||
setTopic = settings.mqttPath + "/set"; |
|||
stateTopic = settings.mqttPath + "/state"; |
|||
doc["~"] = settings.mqttPath; |
|||
doc["name"] = settings.name; |
|||
doc["unique_id"] = settings.uniqueId; |
|||
}); |
|||
doc["cmd_t"] = "~/set"; |
|||
doc["stat_t"] = "~/state"; |
|||
doc["schema"] = "json"; |
|||
doc["brightness"] = false; |
|||
|
|||
String payload; |
|||
serializeJson(doc, payload); |
|||
_mqttClient->publish(configTopic.c_str(), 0, false, payload.c_str()); |
|||
|
|||
_mqttPubSub.configureTopics(stateTopic, setTopic); |
|||
} |
@ -0,0 +1,71 @@ |
|||
#ifndef LightStateService_h |
|||
#define LightStateService_h |
|||
|
|||
#include <LightMqttSettingsService.h> |
|||
|
|||
#include <HttpEndpoint.h> |
|||
#include <MqttPubSub.h> |
|||
#include <WebSocketTxRx.h> |
|||
|
|||
#define BLINK_LED 2 |
|||
#define PRINT_DELAY 5000 |
|||
|
|||
#define DEFAULT_LED_STATE false |
|||
#define OFF_STATE "OFF" |
|||
#define ON_STATE "ON" |
|||
|
|||
// Note that the built-in LED is on when the pin is low on most NodeMCU boards. |
|||
// This is because the anode is tied to VCC and the cathode to the GPIO 4 (Arduino pin 2). |
|||
#ifdef ESP32 |
|||
#define LED_ON 0x1 |
|||
#define LED_OFF 0x0 |
|||
#elif defined(ESP8266) |
|||
#define LED_ON 0x0 |
|||
#define LED_OFF 0x1 |
|||
#endif |
|||
|
|||
#define LIGHT_SETTINGS_ENDPOINT_PATH "/rest/lightState" |
|||
#define LIGHT_SETTINGS_SOCKET_PATH "/ws/lightState" |
|||
|
|||
class LightState { |
|||
public: |
|||
bool ledOn; |
|||
|
|||
static void serialize(LightState& settings, JsonObject& root) { |
|||
root["led_on"] = settings.ledOn; |
|||
} |
|||
|
|||
static void deserialize(JsonObject& root, LightState& settings) { |
|||
settings.ledOn = root["led_on"] | DEFAULT_LED_STATE; |
|||
} |
|||
|
|||
static void haSerialize(LightState& settings, JsonObject& root) { |
|||
root["state"] = settings.ledOn ? ON_STATE : OFF_STATE; |
|||
} |
|||
|
|||
static void haDeserialize(JsonObject& root, LightState& settings) { |
|||
String state = root["state"]; |
|||
settings.ledOn = strcmp(ON_STATE, state.c_str()) ? false : true; |
|||
} |
|||
}; |
|||
|
|||
class LightStateService : public StatefulService<LightState> { |
|||
public: |
|||
LightStateService(AsyncWebServer* server, |
|||
SecurityManager* securityManager, |
|||
AsyncMqttClient* mqttClient, |
|||
LightMqttSettingsService* lightMqttSettingsService); |
|||
void begin(); |
|||
|
|||
private: |
|||
HttpEndpoint<LightState> _httpEndpoint; |
|||
MqttPubSub<LightState> _mqttPubSub; |
|||
WebSocketTxRx<LightState> _webSocket; |
|||
AsyncMqttClient* _mqttClient; |
|||
LightMqttSettingsService* _lightMqttSettingsService; |
|||
|
|||
void registerConfig(); |
|||
void onConfigUpdated(); |
|||
}; |
|||
|
|||
#endif |
Write
Preview
Loading…
Cancel
Save
Reference in new issue