Merge branch 'dev2' of https://github.com/emsesp/EMS-ESP32 into dev2

This commit is contained in:
MichaelDvP
2023-07-02 13:56:47 +02:00
94 changed files with 3916 additions and 3774 deletions

View File

@@ -22,6 +22,7 @@ There are breaking changes in 3.6.0. Please read carefully before applying the u
- AM200 code 10 [#1161](https://github.com/emsesp/EMS-ESP32/issues/1161)
- Ventilation device [#1172](https://github.com/emsesp/EMS-ESP32/issues/1172)
- Turn ETH off on wifi connect [#1167](https://github.com/emsesp/EMS-ESP32/issues/1167)
- Support for multiple EMS-ESPs with HA [#1196](https://github.com/emsesp/EMS-ESP32/issues/1196)
## Fixed

View File

@@ -0,0 +1,2 @@
VITE_ALOVA_TIPS=0
REACT_APP_ALOVA_TIPS=0

View File

@@ -1,7 +1,7 @@
{
"name": "EMS-ESP",
"version": "3.6.0",
"description": "build EMS-ESP TypeScript WebUI",
"description": "build EMS-ESP WebUI",
"homepage": "https://emsesp.github.io/docs",
"author": "proddy",
"license": "MIT",
@@ -19,6 +19,8 @@
"lint": "eslint . --cache --fix"
},
"dependencies": {
"@alova/adapter-xhr": "^1.0.1",
"@alova/scene-react": "^1.1.3",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.16",
@@ -29,11 +31,12 @@
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react-router-dom": "^5.3.3",
"alova": "^2.9.0",
"async-validator": "^4.2.5",
"axios": "^1.4.0",
"history": "^5.3.0",
"jwt-decode": "^3.1.2",
"lodash-es": "^4.17.21",
"mime-types": "^2.1.35",
"react": "latest",
"react-dom": "latest",
"react-dropzone": "^14.2.3",
@@ -45,6 +48,7 @@
"typescript": "^5.1.6"
},
"devDependencies": {
"@types/mime-types": "^2",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitejs/plugin-react-swc": "^3.3.2",

View File

@@ -1,15 +1,10 @@
import { useCallback, useEffect } from 'react';
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import { Navigate, Routes, Route } from 'react-router-dom';
import Dashboard from './project/Dashboard';
import Help from './project/Help';
import Settings from './project/Settings';
import type { AxiosError } from 'axios';
import type { FC } from 'react';
import * as AuthenticationApi from 'api/authentication';
import { AXIOS } from 'api/endpoints';
import { Layout, RequireAdmin } from 'components';
import AccessPoint from 'framework/ap/AccessPoint';
import Mqtt from 'framework/mqtt/Mqtt';
import NetworkConnection from 'framework/network/NetworkConnection';
@@ -17,27 +12,25 @@ import NetworkTime from 'framework/ntp/NetworkTime';
import Security from 'framework/security/Security';
import System from 'framework/system/System';
const AuthenticatedRouting: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const AuthenticatedRouting: FC = () => (
// TODO not sure if this is needed, to redirect on 401. If so add incerceptor to Alova
// const location = useLocation();
// const navigate = useNavigate();
// const handleApiResponseError = useCallback(
// (error: AxiosError) => {
// if (error.response && error.response.status === 401) {
// AuthenticationApi.storeLoginRedirect(location);
// navigate('/unauthorized');
// }
// return Promise.reject(error);
// },
// [location, navigate]
// );
// useEffect(() => {
// const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
// return () => AXIOS.interceptors.response.eject(axiosHandlerId);
// }, [handleApiResponseError]);
const handleApiResponseError = useCallback(
(error: AxiosError) => {
if (error.response && error.response.status === 401) {
AuthenticationApi.storeLoginRedirect(location);
navigate('/unauthorized');
}
return Promise.reject(error);
},
[location, navigate]
);
useEffect(() => {
const axiosHandlerId = AXIOS.interceptors.response.use((response) => response, handleApiResponseError);
return () => AXIOS.interceptors.response.eject(axiosHandlerId);
}, [handleApiResponseError]);
return (
<Layout>
<Routes>
<Route path="/dashboard/*" element={<Dashboard />} />
@@ -67,7 +60,6 @@ const AuthenticatedRouting: FC = () => {
<Route path="/*" element={<Navigate to="/" />} />
</Routes>
</Layout>
);
};
);
export default AuthenticatedRouting;

View File

@@ -1,5 +1,6 @@
import ForwardIcon from '@mui/icons-material/Forward';
import { Box, Fab, Paper, Typography, Button } from '@mui/material';
import { useRequest } from 'alova';
import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import type { ValidateFieldsError } from 'async-validator';
@@ -24,7 +25,7 @@ import { ReactComponent as SVflag } from 'i18n/SV.svg';
import { ReactComponent as TRflag } from 'i18n/TR.svg';
import { I18nContext } from 'i18n/i18n-react';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
import { extractErrorMessage, onEnterCallback, updateValue } from 'utils';
import { onEnterCallback, updateValue } from 'utils';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
const SignIn: FC = () => {
@@ -39,22 +40,27 @@ const SignIn: FC = () => {
const [processing, setProcessing] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), {
immediate: false
});
onSuccess((response) => {
if (response.data) {
authenticationContext.signIn(response.data.access_token);
}
});
const updateLoginRequestValue = updateValue(setSignInRequest);
const signIn = async () => {
try {
const { data: loginResponse } = await AuthenticationApi.signIn(signInRequest);
authenticationContext.signIn(loginResponse.access_token);
} catch (error) {
if (error.response) {
if (error.response?.status === 401) {
await callSignIn(signInRequest).catch((event) => {
if (event.message === 'Unauthorized') {
toast.warn(LL.INVALID_LOGIN());
}
} else {
toast.error(extractErrorMessage(error, LL.ERROR()));
toast.error(LL.ERROR() + ' ' + event.message);
}
setProcessing(false);
}
});
};
const validateAndSignIn = async () => {

View File

@@ -1,16 +1,7 @@
import { AXIOS } from './endpoints';
import type { AxiosPromise } from 'axios';
import { alovaInstance } from './endpoints';
import type { APSettings, APStatus } from 'types';
export function readAPStatus(): AxiosPromise<APStatus> {
return AXIOS.get('/apStatus');
}
export function readAPSettings(): AxiosPromise<APSettings> {
return AXIOS.get('/apSettings');
}
export function updateAPSettings(apSettings: APSettings): AxiosPromise<APSettings> {
return AXIOS.post('/apSettings', apSettings);
}
export const readAPStatus = () => alovaInstance.Get<APStatus>('/rest/apStatus');
export const readAPSettings = () => alovaInstance.Get<APSettings>('/rest/apSettings');
export const updateAPSettings = (data: APSettings) => alovaInstance.Post<APSettings>('/rest/apSettings', data);

View File

@@ -1,6 +1,5 @@
import jwtDecode from 'jwt-decode';
import { ACCESS_TOKEN, AXIOS } from './endpoints';
import type { AxiosPromise } from 'axios';
import { ACCESS_TOKEN, alovaInstance } from './endpoints';
import type * as H from 'history';
import type { Path } from 'react-router-dom';
@@ -9,13 +8,8 @@ import type { Me, SignInRequest, SignInResponse } from 'types';
export const SIGN_IN_PATHNAME = 'loginPathname';
export const SIGN_IN_SEARCH = 'loginSearch';
export function verifyAuthorization(): AxiosPromise<void> {
return AXIOS.get('/verifyAuthorization');
}
export function signIn(request: SignInRequest): AxiosPromise<SignInResponse> {
return AXIOS.post('/signIn', request);
}
export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization');
export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request);
export function getStorage() {
return localStorage || sessionStorage;

View File

@@ -1,95 +1,61 @@
import axios from 'axios';
import { unpack } from './unpack';
import { xhrRequestAdapter } from '@alova/adapter-xhr';
import { createAlova } from 'alova';
import ReactHook from 'alova/react';
import { unpack } from '../api/unpack';
import type { AxiosPromise, CancelToken, AxiosProgressEvent } from 'axios';
export const WS_BASE_URL = '/ws/';
export const API_BASE_URL = '/rest/';
export const ES_BASE_URL = '/es/';
export const EMSESP_API_BASE_URL = '/api/';
export const ACCESS_TOKEN = 'access_token';
const location = window.location;
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
export const WEB_SOCKET_ROOT = webProtocol + '//' + location.host + WS_BASE_URL;
export const EVENT_SOURCE_ROOT = location.protocol + '//' + location.host + ES_BASE_URL;
const host = window.location.host;
export const WEB_SOCKET_ROOT = 'ws://' + host + '/ws/';
export const EVENT_SOURCE_ROOT = 'http://' + host + '/es/';
export const AXIOS = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
},
transformRequest: [
(data, headers) => {
if (headers) {
if (localStorage.getItem(ACCESS_TOKEN)) {
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
export const alovaInstance = createAlova({
statesHook: ReactHook,
// timeout: 3000, // timeout not used because of uploading firmware
// localCache: null,
localCache: {
GET: {
mode: 'placeholder', // see https://alova.js.org/learning/response-cache/#cache-replaceholder-mode
expire: 2000
}
},
requestAdapter: xhrRequestAdapter(),
beforeRequest(method) {
if (localStorage.getItem(ACCESS_TOKEN)) {
method.config.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
}
},
responded: {
onSuccess: async (response) => {
// if (response.status === 202) {
// throw new Error('Wait'); // wifi scan in progress
// } else
if (response.status === 205) {
throw new Error('Reboot required');
} else if (response.status === 400) {
throw new Error('Request Failed');
} else if (response.status >= 400) {
throw new Error(response.statusText);
}
const data = await response.data;
if (response.data instanceof ArrayBuffer) {
return unpack(data);
}
if (headers['Content-Type'] !== 'application/json') {
return data;
}
// Interceptor for request failure. This interceptor will be entered when the request is wrong.
// TODO how best to handle http errors like 401 (unauthorized)
// but I think this is handled correctly in AppRouting? See AuthenticatedRouting()
// onError: (error, method) => {
// alert(error.message);
// }
}
return JSON.stringify(data);
}
]
});
export const AXIOS_API = axios.create({
baseURL: EMSESP_API_BASE_URL,
headers: {
'Content-Type': 'application/json'
},
transformRequest: [
(data, headers) => {
if (headers) {
if (localStorage.getItem(ACCESS_TOKEN)) {
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
}
if (headers['Content-Type'] !== 'application/json') {
return data;
}
}
return JSON.stringify(data);
}
]
export const alovaInstanceGH = createAlova({
baseURL: 'https://api.github.com/repos/emsesp/EMS-ESP32',
statesHook: ReactHook,
requestAdapter: xhrRequestAdapter()
});
export const AXIOS_BIN = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
},
responseType: 'arraybuffer',
transformRequest: [
(data, headers) => {
if (headers) {
if (localStorage.getItem(ACCESS_TOKEN)) {
headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
}
if (headers['Content-Type'] !== 'application/json') {
return data;
}
}
return JSON.stringify(data);
}
],
// transformResponse: [(data) => decode(data)]
transformResponse: [(data) => unpack(data)] // new using msgpackr
});
export interface FileUploadConfig {
cancelToken?: CancelToken;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
}
export const startUploadFile = (url: string, file: File, config?: FileUploadConfig): AxiosPromise<void> => {
const formData = new FormData();
formData.append('file', file);
return AXIOS.post(url, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
...(config || {})
});
};

View File

@@ -1,8 +1,5 @@
import { AXIOS } from './endpoints';
import type { AxiosPromise } from 'axios';
import { alovaInstance } from './endpoints';
import type { Features } from 'types';
export function readFeatures(): AxiosPromise<Features> {
return AXIOS.get('/features');
}
export const readFeatures = () => alovaInstance.Get<Features>('/rest/features');

View File

@@ -1,15 +1,6 @@
import { AXIOS } from './endpoints';
import type { AxiosPromise } from 'axios';
import { alovaInstance } from './endpoints';
import type { MqttSettings, MqttStatus } from 'types';
export function readMqttStatus(): AxiosPromise<MqttStatus> {
return AXIOS.get('/mqttStatus');
}
export function readMqttSettings(): AxiosPromise<MqttSettings> {
return AXIOS.get('/mqttSettings');
}
export function updateMqttSettings(mqttSettings: MqttSettings): AxiosPromise<MqttSettings> {
return AXIOS.post('/mqttSettings', mqttSettings);
}
export const readMqttStatus = () => alovaInstance.Get<MqttStatus>('/rest/mqttStatus');
export const readMqttSettings = () => alovaInstance.Get<MqttSettings>('/rest/mqttSettings');
export const updateMqttSettings = (data: MqttSettings) => alovaInstance.Post<MqttSettings>('/rest/mqttSettings', data);

View File

@@ -1,24 +1,14 @@
import { AXIOS } from './endpoints';
import type { AxiosPromise } from 'axios';
import { alovaInstance } from './endpoints';
import type { WiFiNetworkList, NetworkSettings, NetworkStatus } from 'types';
export function readNetworkStatus(): AxiosPromise<NetworkStatus> {
return AXIOS.get('/networkStatus');
}
export function scanNetworks(): AxiosPromise<void> {
return AXIOS.get('/scanNetworks');
}
export function listNetworks(): AxiosPromise<WiFiNetworkList> {
return AXIOS.get('/listNetworks');
}
export function readNetworkSettings(): AxiosPromise<NetworkSettings> {
return AXIOS.get('/networkSettings');
}
export function updateNetworkSettings(wifiSettings: NetworkSettings): AxiosPromise<NetworkSettings> {
return AXIOS.post('/networkSettings', wifiSettings);
}
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatus>('/rest/networkStatus');
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
export const listNetworks = () =>
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
name: 'listNetworks'
});
export const readNetworkSettings = () =>
alovaInstance.Get<NetworkSettings>('/rest/networkSettings', { name: 'networkSettings' });
export const updateNetworkSettings = (wifiSettings: NetworkSettings) =>
alovaInstance.Post<NetworkSettings>('/rest/networkSettings', wifiSettings);

View File

@@ -1,19 +1,11 @@
import { AXIOS } from './endpoints';
import type { AxiosPromise } from 'axios';
import { alovaInstance } from './endpoints';
import type { NTPSettings, NTPStatus, Time } from 'types';
export function readNTPStatus(): AxiosPromise<NTPStatus> {
return AXIOS.get('/ntpStatus');
}
export const readNTPStatus = () => alovaInstance.Get<NTPStatus>('/rest/ntpStatus');
export const readNTPSettings = () =>
alovaInstance.Get<NTPSettings>('/rest/ntpSettings', {
name: 'ntpSettings'
});
export const updateNTPSettings = (data: NTPSettings) => alovaInstance.Post<NTPSettings>('/rest/ntpSettings', data);
export function readNTPSettings(): AxiosPromise<NTPSettings> {
return AXIOS.get('/ntpSettings');
}
export function updateNTPSettings(ntpSettings: NTPSettings): AxiosPromise<NTPSettings> {
return AXIOS.post('/ntpSettings', ntpSettings);
}
export function updateTime(time: Time): AxiosPromise<Time> {
return AXIOS.post('/time', time);
}
export const updateTime = (data: Time) => alovaInstance.Post<Time>('/rest/time', data);

View File

@@ -1,16 +1,13 @@
import { AXIOS } from './endpoints';
import type { AxiosPromise } from 'axios';
import { alovaInstance } from './endpoints';
import type { SecuritySettings, Token } from 'types';
export function readSecuritySettings(): AxiosPromise<SecuritySettings> {
return AXIOS.get('/securitySettings');
}
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettings>('/rest/securitySettings');
export function updateSecuritySettings(securitySettings: SecuritySettings): AxiosPromise<SecuritySettings> {
return AXIOS.post('/securitySettings', securitySettings);
}
export const updateSecuritySettings = (securitySettings: SecuritySettings) =>
alovaInstance.Post('/rest/securitySettings', securitySettings);
export function generateToken(username?: string): AxiosPromise<Token> {
return AXIOS.get('/generateToken', { params: { username } });
}
export const generateToken = (username?: string) =>
alovaInstance.Get<Token>('/rest/generateToken', {
params: { username }
});

View File

@@ -1,44 +1,50 @@
import { AXIOS, AXIOS_BIN, startUploadFile } from './endpoints';
import type { FileUploadConfig } from './endpoints';
import type { AxiosPromise } from 'axios';
import { alovaInstance, alovaInstanceGH } from './endpoints';
import type { OTASettings, SystemStatus, LogSettings, Version } from 'types';
import type { OTASettings, SystemStatus, LogSettings, LogEntries } from 'types';
// SystemStatus - also used to ping in Restart monitor
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus');
export function readSystemStatus(timeout?: number): AxiosPromise<SystemStatus> {
return AXIOS.get('/systemStatus', { timeout });
}
// commands
export const restart = () => alovaInstance.Post('/rest/restart');
export const partition = () => alovaInstance.Post('/rest/partition');
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
export function restart(): AxiosPromise<void> {
return AXIOS.post('/restart');
}
// OTA
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
export const updateOTASettings = (data: any) => alovaInstance.Post('/rest/otaSettings', data);
export function partition(): AxiosPromise<void> {
return AXIOS.post('/partition');
}
// SystemLog
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`);
export const updateLogSettings = (data: any) => alovaInstance.Post('/rest/logSettings', data);
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
export function factoryReset(): AxiosPromise<void> {
return AXIOS.post('/factoryReset');
}
// Get versions from github
export const getStableVersion = () =>
alovaInstanceGH.Get<Version>('releases/latest', {
transformData(reponse: any) {
return {
version: reponse.data.name,
url: reponse.data.assets[1].browser_download_url,
changelog: reponse.data.assets[0].browser_download_url
};
}
});
export function readOTASettings(): AxiosPromise<OTASettings> {
return AXIOS.get('/otaSettings');
}
export const getDevVersion = () =>
alovaInstanceGH.Get<Version>('releases/tags/latest', {
transformData(reponse: any) {
return {
version: reponse.data.name.split(/\s+/).splice(-1),
url: reponse.data.assets[1].browser_download_url,
changelog: reponse.data.assets[0].browser_download_url
};
}
});
export function updateOTASettings(otaSettings: OTASettings): AxiosPromise<OTASettings> {
return AXIOS.post('/otaSettings', otaSettings);
}
export const uploadFile = (file: File, config?: FileUploadConfig): AxiosPromise<void> =>
startUploadFile('/uploadFile', file, config);
export function readLogSettings(): AxiosPromise<LogSettings> {
return AXIOS.get('/logSettings');
}
export function updateLogSettings(logSettings: LogSettings): AxiosPromise<LogSettings> {
return AXIOS.post('/logSettings', logSettings);
}
export function readLogEntries(): AxiosPromise<LogEntries> {
return AXIOS_BIN.get('/fetchLog');
}
export const uploadFile = (file: File) => {
const formData = new FormData();
formData.append('file', file);
return alovaInstance.Post('/rest/uploadFile', formData, {
enableUpload: true
});
};

View File

@@ -968,7 +968,6 @@ currentExtensions[0x69] = (data) => {
if (!referenceMap) referenceMap = new Map();
const token = src[position];
let target;
// TODO: handle Maps, Sets, and other types that can cycle; this is complicated, because you potentially need to read
// ahead past references to record structure definitions
if ((token >= 0x90 && token < 0xa0) || token == 0xdc || token == 0xdd) target = [];
else target = {};
@@ -1041,7 +1040,6 @@ currentExtensions[0xff] = (data) => {
((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000
);
else if (data.length == 12)
// TODO: Implement support for negative
return new Date(
((data[0] << 24) + (data[1] << 16) + (data[2] << 8) + data[3]) / 1000000 +
((data[4] & 0x80 ? -0x1000000000000 : 0) +
@@ -1070,7 +1068,6 @@ function saveState(callback) {
const savedReferenceMap = referenceMap;
const savedBundledStrings = bundledStrings;
// TODO: We may need to revisit this if we do more external calls to user code (since it could be slow)
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
const savedStructures = currentStructures;
const savedStructuresContents = currentStructures.slice(0, currentStructures.length);

View File

@@ -4,7 +4,7 @@ import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material
import { Fragment } from 'react';
import { useDropzone } from 'react-dropzone';
import type { Theme } from '@mui/material';
import type { AxiosProgressEvent } from 'axios';
import type { Progress } from 'alova';
import type { FC } from 'react';
import type { DropzoneState } from 'react-dropzone';
@@ -26,11 +26,13 @@ const getBorderColor = (theme: Theme, props: DropzoneState) => {
export interface SingleUploadProps {
onDrop: (acceptedFiles: File[]) => void;
onCancel: () => void;
uploading: boolean;
progress?: AxiosProgressEvent;
isUploading: boolean;
progress: Progress;
}
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, progress }) => {
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, progress }) => {
const uploading = isUploading && progress.total > 0;
const dropzoneState = useDropzone({
onDrop,
accept: {
@@ -38,20 +40,19 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
'application/json': ['.json'],
'text/plain': ['.md5']
},
disabled: uploading,
disabled: isUploading,
multiple: false
});
const { getRootProps, getInputProps } = dropzoneState;
const theme = useTheme();
const { LL } = useI18nContext();
const progressText = () => {
if (uploading) {
if (progress?.total) {
if (progress.total) {
return LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%';
}
return LL.UPLOADING() + `\u2026`;
}
return LL.UPLOAD_DROP_TEXT();
};
@@ -81,8 +82,8 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
<Fragment>
<Box width="100%" p={2}>
<LinearProgress
variant={!progress || progress.total ? 'determinate' : 'indeterminate'}
value={!progress ? 0 : progress.total ? Math.round((progress.loaded * 100) / progress.total) : 0}
variant="determinate"
value={progress.total === 0 ? 0 : Math.round((progress.loaded * 100) / progress.total)}
/>
</Box>
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>

View File

@@ -1,2 +1 @@
export { default as SingleUpload } from './SingleUpload';
export { default as useFileUpload } from './useFileUpload';

View File

@@ -1,71 +0,0 @@
import axios from 'axios';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import type { FileUploadConfig } from 'api/endpoints';
import type { AxiosPromise, CancelTokenSource, AxiosProgressEvent } from 'axios';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
interface MediaUploadOptions {
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
}
const useFileUpload = ({ upload }: MediaUploadOptions) => {
const { LL } = useI18nContext();
const [uploading, setUploading] = useState<boolean>(false);
const [md5, setMd5] = useState<string>('');
const [uploadProgress, setUploadProgress] = useState<AxiosProgressEvent>();
const [uploadCancelToken, setUploadCancelToken] = useState<CancelTokenSource>();
const resetUploadingStates = () => {
setUploading(false);
setUploadProgress(undefined);
setUploadCancelToken(undefined);
setMd5('');
};
const cancelUpload = useCallback(() => {
uploadCancelToken?.cancel();
resetUploadingStates();
}, [uploadCancelToken]);
useEffect(
() => () => {
uploadCancelToken?.cancel();
},
[uploadCancelToken]
);
const uploadFile = async (images: File[]) => {
try {
const cancelToken = axios.CancelToken.source();
setUploadCancelToken(cancelToken);
setUploading(true);
const response = await upload(images[0], {
onUploadProgress: setUploadProgress,
cancelToken: cancelToken.token
});
resetUploadingStates();
if (response.status === 200) {
toast.success(LL.UPLOAD() + ' ' + LL.SUCCESSFUL());
} else if (response.status === 201) {
setMd5(String(response.data));
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
}
} catch (error) {
if (axios.isCancel(error)) {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else {
resetUploadingStates();
toast.error(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED(0)));
}
}
};
return [uploadFile, cancelUpload, uploading, uploadProgress, md5] as const;
};
export default useFileUpload;

View File

@@ -1,3 +1,4 @@
import { useRequest } from 'alova';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -19,6 +20,10 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const [initialized, setInitialized] = useState<boolean>(false);
const [me, setMe] = useState<Me>();
const { send: verifyAuthorization } = useRequest(AuthenticationApi.verifyAuthorization(), {
immediate: false
});
const signIn = (accessToken: string) => {
try {
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
@@ -42,18 +47,20 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const refresh = useCallback(async () => {
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
try {
await AuthenticationApi.verifyAuthorization();
await verifyAuthorization()
.then(() => {
setMe(AuthenticationApi.decodeMeJWT(accessToken));
setInitialized(true);
} catch (error) {
})
.catch(() => {
setMe(undefined);
setInitialized(true);
}
});
} else {
setMe(undefined);
setInitialized(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {

View File

@@ -1,30 +1,14 @@
import { useCallback, useEffect, useState } from 'react';
import { useRequest } from 'alova';
import { FeaturesContext } from '.';
import type { FC } from 'react';
import type { Features } from 'types';
import type { RequiredChildrenProps } from 'utils';
import * as FeaturesApi from 'api/features';
import { ApplicationError, LoadingSpinner } from 'components';
import { extractErrorMessage } from 'utils';
const FeaturesLoader: FC<RequiredChildrenProps> = (props) => {
const [errorMessage, setErrorMessage] = useState<string>();
const [features, setFeatures] = useState<Features>();
const loadFeatures = useCallback(async () => {
try {
const response = await FeaturesApi.readFeatures();
setFeatures(response.data);
} catch (error) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch application details.'));
}
}, []);
useEffect(() => {
void loadFeatures();
}, [loadFeatures]);
const { data: features, error } = useRequest(FeaturesApi.readFeatures);
if (features) {
return (
@@ -38,8 +22,8 @@ const FeaturesLoader: FC<RequiredChildrenProps> = (props) => {
);
}
if (errorMessage) {
return <ApplicationError message={errorMessage} />;
if (error) {
return <ApplicationError message={error?.message} />;
}
return <LoadingSpinner height="100vh" />;

View File

@@ -28,8 +28,18 @@ export const isAPEnabled = ({ provision_mode }: APSettings) =>
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
const APSettingsForm: FC = () => {
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest<APSettings>({
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<APSettings>({
read: APApi.readAPSettings,
update: APApi.updateAPSettings
});
@@ -38,7 +48,7 @@ const APSettingsForm: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const content = () => {
if (!data) {

View File

@@ -3,6 +3,7 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import RefreshIcon from '@mui/icons-material/Refresh';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
import { useRequest } from 'alova';
import type { Theme } from '@mui/material';
import type { FC } from 'react';
@@ -12,7 +13,6 @@ import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { APNetworkStatus } from 'types';
import { useRest } from 'utils';
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
switch (status) {
@@ -28,7 +28,7 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
};
const APStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
const { LL } = useI18nContext();
@@ -49,7 +49,7 @@ const APStatusForm: FC = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (

View File

@@ -22,8 +22,18 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators';
const MqttSettingsForm: FC = () => {
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest<MqttSettings>({
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<MqttSettings>({
read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings
});
@@ -32,7 +42,7 @@ const MqttSettingsForm: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const content = () => {
if (!data) {

View File

@@ -4,6 +4,7 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import ReportIcon from '@mui/icons-material/Report';
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
import { useRequest } from 'alova';
import type { Theme } from '@mui/material';
import type { FC } from 'react';
@@ -12,7 +13,6 @@ import * as MqttApi from 'api/mqtt';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { MqttDisconnectReason } from 'types';
import { useRest } from 'utils';
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
if (!enabled) {
@@ -26,7 +26,6 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: T
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) => {
if (mqtt_fails === 0) return theme.palette.success.main;
if (mqtt_fails < 10) return theme.palette.warning.main;
return theme.palette.error.main;
@@ -39,7 +38,7 @@ export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatus, theme: Theme) =>
};
const MqttStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<MqttStatus>({ read: MqttApi.readMqttStatus });
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
const { LL } = useI18nContext();
@@ -80,7 +79,7 @@ const MqttStatusForm: FC = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
const renderConnectionStatus = () => (

View File

@@ -18,6 +18,8 @@ import {
InputAdornment,
TextField
} from '@mui/material';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova';
import { useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from '../system/RestartMonitor';
@@ -28,6 +30,7 @@ import type { FC } from 'react';
import type { NetworkSettings } from 'types';
import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system';
import {
BlockFormControlLabel,
ButtonRow,
@@ -39,7 +42,7 @@ import {
BlockNavigation
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
@@ -52,11 +55,12 @@ const WiFiSettingsForm: FC = () => {
const [initialized, setInitialized] = useState(false);
const [restarting, setRestarting] = useState(false);
const {
loadData,
saving,
data,
setData,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
@@ -69,13 +73,17 @@ const WiFiSettingsForm: FC = () => {
update: NetworkApi.updateNetworkSettings
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
useEffect(() => {
if (!initialized && data) {
if (selectedNetwork) {
setData({
updateState('networkSettings', (current_data) => ({
ssid: selectedNetwork.ssid,
password: '',
hostname: data?.hostname,
hostname: current_data?.hostname,
static_ip_config: false,
enableIPv6: false,
bandwidth20: false,
@@ -84,13 +92,13 @@ const WiFiSettingsForm: FC = () => {
enableMDNS: true,
enableCORS: false,
CORSOrigin: '*'
});
}));
}
setInitialized(true);
}
}, [initialized, setInitialized, data, setData, selectedNetwork]);
}, [initialized, setInitialized, data, selectedNetwork]);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -112,12 +120,10 @@ const WiFiSettingsForm: FC = () => {
};
const restart = async () => {
try {
await EMSESP.restart();
await restartCommand().catch((error) => {
toast.error(error.message);
});
setRestarting(true);
} catch (error) {
toast.error(LL.PROBLEM_UPDATING());
}
};
return (

View File

@@ -6,6 +6,7 @@ import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
import WifiIcon from '@mui/icons-material/Wifi';
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material';
import { useRequest } from 'alova';
import type { Theme } from '@mui/material';
import type { FC } from 'react';
@@ -15,7 +16,6 @@ import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { NetworkConnectionStatus } from 'types';
import { useRest } from 'utils';
const isConnected = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
@@ -59,7 +59,7 @@ const IPs = (status: NetworkStatus) => {
};
const NetworkStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<NetworkStatus>({ read: NetworkApi.readNetworkStatus });
const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus);
const { LL } = useI18nContext();
@@ -90,7 +90,7 @@ const NetworkStatusForm: FC = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (

View File

@@ -1,82 +1,52 @@
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import { Button } from '@mui/material';
import { useEffect, useState, useCallback, useRef } from 'react';
import { toast } from 'react-toastify';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova';
import { useState, useRef } from 'react';
import WiFiNetworkSelector from './WiFiNetworkSelector';
import type { FC } from 'react';
import type { WiFiNetwork, WiFiNetworkList } from 'types';
import * as NetworkApi from 'api/network';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const NUM_POLLS = 10;
const POLLING_FREQUENCY = 500;
const compareNetworks = (network1: WiFiNetwork, network2: WiFiNetwork) => {
if (network1.rssi < network2.rssi) return 1;
if (network1.rssi > network2.rssi) return -1;
return 0;
};
const POLLING_FREQUENCY = 1000;
const WiFiNetworkScanner: FC = () => {
const { LL } = useI18nContext();
const pollCount = useRef(0);
const [networkList, setNetworkList] = useState<WiFiNetworkList>();
const { LL } = useI18nContext();
const [errorMessage, setErrorMessage] = useState<string>();
const finishedWithError = useCallback((message: string) => {
toast.error(message);
setNetworkList(undefined);
setErrorMessage(message);
}, []);
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(NetworkApi.scanNetworks); // is called on page load to start network scan
const {
data: networkList,
send: getNetworkList,
onSuccess: onSuccessNetworkList
} = useRequest(NetworkApi.listNetworks, {
immediate: false
});
const pollNetworkList = useCallback(async () => {
try {
const response = await NetworkApi.listNetworks();
if (response.status === 202) {
onSuccessNetworkList((event) => {
if (!event.data) {
const completedPollCount = pollCount.current + 1;
if (completedPollCount < NUM_POLLS) {
pollCount.current = completedPollCount;
setTimeout(pollNetworkList, POLLING_FREQUENCY);
setTimeout(getNetworkList, POLLING_FREQUENCY);
} else {
finishedWithError(LL.PROBLEM_LOADING());
}
} else {
const newNetworkList = response.data;
newNetworkList.networks.sort(compareNetworks);
setNetworkList(newNetworkList);
}
} catch (error) {
if (error.response) {
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
} else {
finishedWithError(LL.PROBLEM_LOADING());
}
}
}, [finishedWithError, LL]);
const startNetworkScan = useCallback(async () => {
setErrorMessage(LL.PROBLEM_LOADING());
pollCount.current = 0;
setNetworkList(undefined);
setErrorMessage(undefined);
try {
await NetworkApi.scanNetworks();
setTimeout(pollNetworkList, POLLING_FREQUENCY);
} catch (error) {
if (error.response) {
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
} else {
finishedWithError(LL.PROBLEM_LOADING());
}
}
}, [finishedWithError, pollNetworkList, LL]);
});
useEffect(() => {
void startNetworkScan();
}, [startNetworkScan]);
onCompleteScanNetworks(() => {
pollCount.current = 0;
setErrorMessage(undefined);
updateState('listNetworks', () => undefined);
void getNetworkList();
});
const renderNetworkScanner = () => {
if (!networkList) {
@@ -93,7 +63,7 @@ const WiFiNetworkScanner: FC = () => {
startIcon={<PermScanWifiIcon />}
variant="outlined"
color="secondary"
onClick={startNetworkScan}
onClick={scanNetworks}
disabled={!errorMessage && !networkList}
>
{LL.SCAN_AGAIN()}&hellip;

View File

@@ -1,6 +1,8 @@
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
import { Button, Checkbox, MenuItem } from '@mui/material';
// eslint-disable-next-line import/named
import { updateState } from 'alova';
import { useState } from 'react';
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
import type { ValidateFieldsError } from 'async-validator';
@@ -22,15 +24,25 @@ import { validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
const NTPSettingsForm: FC = () => {
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest<NTPSettings>({
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<NTPSettings>({
read: NTPApi.readNTPSettings,
update: NTPApi.updateNTPSettings
});
const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -51,11 +63,12 @@ const NTPSettingsForm: FC = () => {
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFormValue(event);
setData({
...data,
updateState('ntpSettings', (settings) => ({
...settings,
tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value]
});
}));
};
return (

View File

@@ -21,6 +21,7 @@ import {
useTheme,
Typography
} from '@mui/material';
import { useRequest } from 'alova';
import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import type { Theme } from '@mui/material';
@@ -33,7 +34,7 @@ import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { NTPSyncStatus } from 'types';
import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from 'utils';
import { formatDateTime, formatLocalDateTime } from 'utils';
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
@@ -52,7 +53,8 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
};
const NTPStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<NTPStatus>({ read: NTPApi.readNTPStatus });
const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false);
@@ -60,6 +62,12 @@ const NTPStatusForm: FC = () => {
const { LL } = useI18nContext();
const { send: updateTime } = useRequest((local_time) => NTPApi.updateTime(local_time), {
immediate: false
});
NTPApi.updateTime;
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
const openSetTime = () => {
@@ -84,18 +92,19 @@ const NTPStatusForm: FC = () => {
const configureTime = async () => {
setProcessing(true);
try {
await NTPApi.updateTime({
local_time: formatLocalDateTime(new Date(localTime))
});
await updateTime({ local_time: formatLocalDateTime(new Date(localTime)) })
.then(async () => {
toast.success(LL.TIME_SET());
setSettingTime(false);
await loadData();
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
})
.catch(() => {
toast.error(LL.PROBLEM_UPDATING());
})
.finally(() => {
setProcessing(false);
}
});
};
const renderSetTimeDialog = () => (
@@ -136,7 +145,7 @@ const NTPStatusForm: FC = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (

View File

@@ -10,16 +10,14 @@ import {
TextField,
Button
} from '@mui/material';
import { useCallback, useState, useEffect } from 'react';
import { useRequest } from 'alova';
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import type { FC } from 'react';
import type { Token } from 'types';
import * as SecurityApi from 'api/security';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
interface GenerateTokenProps {
username?: string;
@@ -27,24 +25,18 @@ interface GenerateTokenProps {
}
const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const [token, setToken] = useState<Token>();
const { LL } = useI18nContext();
const open = !!username;
const { LL } = useI18nContext();
const getToken = useCallback(async () => {
try {
setToken((await SecurityApi.generateToken(username)).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
}, [username, LL]);
const { data: token, send: generateToken } = useRequest(SecurityApi.generateToken(username), {
immediate: false
});
useEffect(() => {
if (open) {
void getToken();
void generateToken();
}
}, [open, getToken]);
}, [open, generateToken]);
return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open={!!username} fullWidth maxWidth="sm">

View File

@@ -25,7 +25,7 @@ import { useRest } from 'utils';
import { createUserValidator } from 'validators';
const ManageUsersForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<SecuritySettings>({
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings
});
@@ -88,7 +88,7 @@ const ManageUsersForm: FC = () => {
const removeUser = (toRemove: User) => {
const users = data.users.filter((u) => u.username !== toRemove.username);
setData({ ...data, users });
updateDataValue({ ...data, users });
setChanged(changed + 1);
};
@@ -113,7 +113,7 @@ const ManageUsersForm: FC = () => {
const doneEditingUser = () => {
if (user) {
const users = [...data.users.filter((u) => u.username !== user.username), user];
setData({ ...data, users });
updateDataValue({ ...data, users });
setUser(undefined);
setChanged(changed + 1);
}

View File

@@ -18,15 +18,25 @@ const SecuritySettingsForm: FC = () => {
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { loadData, saving, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, saveData, errorMessage } =
useRest<SecuritySettings>({
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings
});
const authenticatedContext = useContext(AuthenticatedContext);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const content = () => {
if (!data) {

View File

@@ -1,153 +0,0 @@
import DownloadIcon from '@mui/icons-material/GetApp';
import { Typography, Button, Box } from '@mui/material';
import { toast } from 'react-toastify';
import type { FileUploadConfig } from 'api/endpoints';
import type { AxiosPromise } from 'axios';
import type { FC } from 'react';
import { SingleUpload, useFileUpload } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
import { extractErrorMessage } from 'utils';
interface UploadFileProps {
uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
}
const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
const [uploadFile, cancelUpload, uploading, uploadProgress, md5] = useFileUpload({ upload: uploadGeneralFile });
const { LL } = useI18nContext();
const saveFile = (json: any, endpoint: string) => {
const a = document.createElement('a');
const filename = 'emsesp_' + endpoint + '.json';
a.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
type: 'text/plain'
})
);
a.setAttribute('download', filename);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
const downloadSettings = async () => {
try {
const response = await EMSESP.getSettings();
if (response.status !== 200) {
toast.error(LL.PROBLEM_LOADING());
} else {
saveFile(response.data, 'settings');
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
const downloadCustomizations = async () => {
try {
const response = await EMSESP.getCustomizations();
if (response.status !== 200) {
toast.error(LL.PROBLEM_LOADING());
} else {
saveFile(response.data, 'customizations');
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
const downloadEntities = async () => {
try {
const response = await EMSESP.getEntities();
if (response.status !== 200) {
toast.error(LL.PROBLEM_LOADING());
} else {
saveFile(response.data, 'entities');
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
const downloadSchedule = async () => {
try {
const response = await EMSESP.getSchedule();
if (response.status !== 200) {
toast.error(LL.PROBLEM_LOADING());
} else {
saveFile(response.data, 'schedule');
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
return (
<>
{!uploading && (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()}
</Typography>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.UPLOAD_TEXT()} </Typography>
</Box>
</>
)}
{md5 !== '' && (
<Box mb={2}>
<Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box>
)}
<SingleUpload onDrop={uploadFile} onCancel={cancelUpload} uploading={uploading} progress={uploadProgress} />
{!uploading && (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSettings}>
{LL.SETTINGS_OF('')}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadCustomizations}>
{LL.CUSTOMIZATIONS()}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadEntities}
>
{LL.CUSTOM_ENTITIES(0)}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SCHEDULE_TEXT()}{' '}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSchedule}>
{LL.SCHEDULE(0)}
</Button>
</>
)}
</>
);
};
export default GeneralFileUpload;

View File

@@ -24,15 +24,25 @@ import { validate } from 'validators';
import { OTA_SETTINGS_VALIDATOR } from 'validators/system';
const OTASettingsForm: FC = () => {
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest<OTASettings>({
const {
loadData,
saveData,
saving,
updateDataValue,
data,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
errorMessage
} = useRest<OTASettings>({
read: SystemApi.readOTASettings,
update: SystemApi.updateOTASettings
});
const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();

View File

@@ -1,4 +1,5 @@
import { useRef, useState, useEffect } from 'react';
import { useRetriableRequest } from '@alova/scene-react';
import { useState } from 'react';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
@@ -6,35 +7,26 @@ import { FormLoader } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000;
const POLL_TIMEOUT = 2000;
const POLL_INTERVAL = 5000;
const RestartMonitor: FC = () => {
const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const { LL } = useI18nContext();
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
const poll = useRef(async () => {
try {
await SystemApi.readSystemStatus(POLL_TIMEOUT);
document.location.href = '/fileUpdated';
} catch (error) {
if (new Date().getTime() < timeoutAt.current) {
setTimeoutId(setTimeout(poll.current, POLL_INTERVAL));
} else {
setFailed(true);
}
// eslint-disable-next-line @typescript-eslint/unbound-method
const { onFail, onSuccess } = useRetriableRequest(SystemApi.readSystemStatus(), {
retry: 10,
backoff: {
delay: 1500
}
});
useEffect(() => {
void poll.current();
}, []);
onFail(() => {
setFailed(true);
});
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
onSuccess(() => {
document.location.href = '/fileUpdated';
});
return <FormLoader message={LL.APPLICATION_RESTARTING() + '...'} errorMessage={failed ? 'Timed out' : undefined} />;
};

View File

@@ -1,11 +1,13 @@
import DownloadIcon from '@mui/icons-material/GetApp';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, styled, Button, Checkbox, MenuItem, Grid, TextField } from '@mui/material';
import { useState, useEffect, useCallback } from 'react';
// eslint-disable-next-line import/named
import { useRequest } from 'alova';
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import type { FC } from 'react';
import type { LogSettings, LogEntry, LogEntries } from 'types';
import type { LogSettings, LogEntry } from 'types';
import { addAccessTokenParameter } from 'api/authentication';
import { EVENT_SOURCE_ROOT } from 'api/endpoints';
import * as SystemApi from 'api/system';
@@ -14,7 +16,7 @@ import { SectionContent, FormLoader, BlockFormControlLabel, BlockNavigation } fr
import { useI18nContext } from 'i18n/i18n-react';
import { LogLevel } from 'types';
import { useRest, updateValueDirty, extractErrorMessage } from 'utils';
import { updateValueDirty, useRest } from 'utils';
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
@@ -49,14 +51,20 @@ const levelLabel = (level: LogLevel) => {
const SystemLog: FC = () => {
const { LL } = useI18nContext();
const { loadData, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, setOrigData } = useRest<LogSettings>({
read: SystemApi.readLogSettings
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
useRest<LogSettings>({
read: SystemApi.readLogSettings,
update: SystemApi.updateLogSettings
});
const [errorMessage, setErrorMessage] = useState<string>();
const [logEntries, setLogEntries] = useState<LogEntries>({ events: [] });
// called on page load to reset pointer and fetch all log entries
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { send: fetchLog } = useRequest(SystemApi.fetchLog());
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const [lastIndex, setLastIndex] = useState<number>(0);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const paddedLevelLabel = (level: LogLevel) => {
const label = levelLabel(level);
return data?.compact ? ' ' + label[0] : label.padStart(8, '\xa0');
@@ -72,11 +80,9 @@ const SystemLog: FC = () => {
return data?.compact ? label : label.padEnd(7, '\xa0');
};
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const onDownload = () => {
let result = '';
for (const i of logEntries.events) {
for (const i of logEntries) {
result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
}
const a = document.createElement('a');
@@ -93,29 +99,22 @@ const SystemLog: FC = () => {
const logentry = JSON.parse(rawData as string) as LogEntry;
if (logentry.i > lastIndex) {
setLastIndex(logentry.i);
setLogEntries((old) => ({ events: [...old.events, logentry] }));
setLogEntries((log) => [...log, logentry]);
}
}
};
const fetchLog = useCallback(async () => {
try {
await SystemApi.readLogEntries();
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
void fetchLog();
}, [fetchLog]);
const saveSettings = async () => {
await saveData();
};
useEffect(() => {
const es = new EventSource(addAccessTokenParameter(LOG_EVENTSOURCE_URL));
es.onmessage = onMessage;
es.onerror = () => {
es.close();
window.location.reload();
toast.error('EventSource failed');
// window.location.reload();
};
return () => {
@@ -123,28 +122,6 @@ const SystemLog: FC = () => {
};
});
const saveSettings = async () => {
if (data) {
try {
const response = await SystemApi.updateLogSettings({
level: data.level,
max_messages: data.max_messages,
compact: data.compact
});
if (response.status !== 200) {
toast.error(LL.PROBLEM_UPDATING());
} else {
setOrigData(response.data);
setDirtyFlags([]);
toast.success(LL.UPDATED_OF(LL.SETTINGS()));
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -230,8 +207,7 @@ const SystemLog: FC = () => {
p: 1
}}
>
{logEntries &&
logEntries.events.map((e) => (
{logEntries.map((e) => (
<LogEntryLine key={e.i}>
<span>{e.t}</span>
{data.compact && <span>{paddedLevelLabel(e.l)} </span>}

View File

@@ -28,18 +28,16 @@ import {
Typography
} from '@mui/material';
import axios from 'axios';
import { useContext, useState, useEffect } from 'react';
import { useRequest } from 'alova';
import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from './RestartMonitor';
import type { FC } from 'react';
import type { SystemStatus, Version } from 'types';
import * as SystemApi from 'api/system';
import { ButtonRow, FormLoader, SectionContent, MessageBox } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage, useRest } from 'utils';
export const VERSIONCHECK_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/latest';
export const VERSIONCHECK_DEV_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/latest';
@@ -51,61 +49,75 @@ function formatNumber(num: number) {
const SystemStatusForm: FC = () => {
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>();
const { loadData, data, errorMessage } = useRest<SystemStatus>({ read: SystemApi.readSystemStatus });
const { me } = useContext(AuthenticatedContext);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false);
const [showingVersion, setShowingVersion] = useState<boolean>(false);
const [latestVersion, setLatestVersion] = useState<Version>();
const [latestDevVersion, setLatestDevVersion] = useState<Version>();
const [restarting, setRestarting] = useState<boolean>();
useEffect(() => {
void axios.get(VERSIONCHECK_ENDPOINT).then((response) => {
setLatestVersion({
version: response.data.name,
url: response.data.assets[1].browser_download_url,
changelog: response.data.assets[0].browser_download_url
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
immediate: false
});
void axios.get(VERSIONCHECK_DEV_ENDPOINT).then((response) => {
setLatestDevVersion({
version: response.data.name.split(/\s+/).splice(-1),
url: response.data.assets[1].browser_download_url,
changelog: response.data.assets[0].browser_download_url
const { send: partitionCommand } = useRequest(SystemApi.partition(), {
immediate: false
});
});
}, []);
// fetch versions from GH on load
const { data: latestVersion } = useRequest(SystemApi.getStableVersion);
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion);
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true });
const restart = async () => {
setProcessing(true);
try {
const response = await SystemApi.restart();
if (response.status === 200) {
await restartCommand()
.then(() => {
setRestarting(true);
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
} finally {
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmRestart(false);
setProcessing(false);
}
});
};
const factoryReset = async () => {
setProcessing(true);
await factoryResetCommand()
.then(() => {
setRestarting(true);
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmFactoryReset(false);
setProcessing(false);
});
};
const partition = async () => {
setProcessing(true);
try {
await SystemApi.partition();
await partitionCommand()
.then(() => {
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
} finally {
})
.catch((err) => {
toast.error(err.message);
})
.finally(() => {
setConfirmRestart(false);
setProcessing(false);
}
});
};
const renderRestartDialog = () => (
@@ -200,19 +212,6 @@ const SystemStatusForm: FC = () => {
</Dialog>
);
const factoryReset = async () => {
setProcessing(true);
try {
await SystemApi.factoryReset();
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setConfirmFactoryReset(false);
setProcessing(false);
}
};
const renderFactoryResetDialog = () => (
<Dialog open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
@@ -242,7 +241,7 @@ const SystemStatusForm: FC = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (

View File

@@ -1,30 +1,173 @@
import { useRef, useState } from 'react';
import GeneralFileUpload from './GeneralFileUpload';
import DownloadIcon from '@mui/icons-material/GetApp';
import { Typography, Button, Box } from '@mui/material';
import { useRequest } from 'alova';
import { useState, type FC } from 'react';
import { toast } from 'react-toastify';
import RestartMonitor from './RestartMonitor';
import type { FileUploadConfig } from 'api/endpoints';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
import { SectionContent } from 'components';
import { SectionContent, SingleUpload } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from 'project/api';
const UploadFileForm: FC = () => {
const [restarting, setRestarting] = useState<boolean>();
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>(false);
const [md5, setMd5] = useState<string>();
const uploadFile = useRef(async (file: File, config?: FileUploadConfig) => {
const response = await SystemApi.uploadFile(file, config);
if (response.status === 200) {
setRestarting(true);
}
return response;
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), {
immediate: false
});
const { send: getCustomizations, onSuccess: onSuccessgetCustomizations } = useRequest(EMSESP.getCustomizations(), {
immediate: false
});
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), {
immediate: false
});
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), {
immediate: false
});
const {
loading: isUploading,
uploading: progress,
send: sendUpload,
onSuccess: onSuccessUpload,
abort: cancelUpload
} = useRequest(SystemApi.uploadFile, {
immediate: false,
force: true
});
onSuccessUpload(({ data }: any) => {
if (data) {
setMd5(data.md5);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
} else {
setRestarting(true);
}
});
const startUpload = async (files: File[]) => {
await sendUpload(files[0]).catch((err) => {
if (err.message === 'The user aborted a request') {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else {
toast.warning(err.message);
}
});
};
const saveFile = (json: any, endpoint: string) => {
const anchor = document.createElement('a');
anchor.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
type: 'text/plain'
})
);
anchor.download = 'emsesp_' + endpoint + '.json';
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
onSuccessGetSettings((event) => {
saveFile(event.data, 'settings');
});
onSuccessgetCustomizations((event) => {
saveFile(event.data, 'customizations');
});
onSuccessGetEntities((event) => {
saveFile(event.data, 'entities');
});
onSuccessGetSchedule((event) => {
saveFile(event.data, 'schedule');
});
const downloadSettings = async () => {
await getSettings().catch((error) => {
toast.error(error.message);
});
};
const downloadCustomizations = async () => {
await getCustomizations().catch((error) => {
toast.error(error.message);
});
};
const downloadEntities = async () => {
await getEntities().catch((error) => {
toast.error(error.message);
});
};
const downloadSchedule = async () => {
await getSchedule().catch((error) => {
toast.error(error.message);
});
};
const content = () => (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()}
</Typography>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.UPLOAD_TEXT()} </Typography>
</Box>
{md5 && (
<Box mb={2}>
<Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box>
)}
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} />
{!isUploading && (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSettings}>
{LL.SETTINGS_OF('')}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadCustomizations}>
{LL.CUSTOMIZATIONS()}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadEntities}
>
{LL.CUSTOM_ENTITIES(0)}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SCHEDULE_TEXT()}{' '}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={downloadSchedule}>
{LL.SCHEDULE(0)}
</Button>
</>
)}
</>
);
return (
<SectionContent title={LL.UPLOAD_DOWNLOAD()} titleGutter>
{restarting ? <RestartMonitor /> : <GeneralFileUpload uploadGeneralFile={uploadFile.current} />}
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};

View File

@@ -51,7 +51,6 @@ const de: Translation = {
REMOVE: 'Entfernen',
PROBLEM_UPDATING: 'Problem beim Aktualisieren',
PROBLEM_LOADING: 'Problem beim Laden',
ACCESS_DENIED: 'Zugriff abgelehnt',
ANALOG_SENSOR: 'Analogsensor',
ANALOG_SENSORS: 'Analogsensoren',
SETTINGS: 'Einstellungen',
@@ -71,7 +70,6 @@ const de: Translation = {
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperatursensoren',
WRITE_CMD_SENT: 'Befehl schreiben wurde gesendet',
WRITE_CMD_FAILED: 'Befehl schreiben failed', // TODO translate
EMS_BUS_WARNING: 'EMS-Bus getrennt. Wenn diese Warnung nach einigen Sekunden immer noch besteht, überprüfen Sie bitte die Einstellungen und das Board-Profil',
EMS_BUS_SCANNING: 'Suche nach EMS Geräten...',
CONNECTED: 'Verbunden',

View File

@@ -51,7 +51,6 @@ const en: Translation = {
REMOVE: 'Remove',
PROBLEM_UPDATING: 'Problem updating',
PROBLEM_LOADING: 'Problem loading',
ACCESS_DENIED: 'Access Denied',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analog Sensors',
SETTINGS: 'Settings',
@@ -71,7 +70,6 @@ const en: Translation = {
TEMP_SENSOR: 'Temperature Sensor',
TEMP_SENSORS: 'Temperature Sensors',
WRITE_CMD_SENT: 'Write command sent',
WRITE_CMD_FAILED: 'Write command failed',
EMS_BUS_WARNING: 'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
EMS_BUS_SCANNING: 'Scanning for EMS devices...',
CONNECTED: 'Connected',

View File

@@ -51,7 +51,6 @@ const fr: Translation = {
REMOVE: 'Enlever',
PROBLEM_UPDATING: 'Problème lors de la mise à jour',
PROBLEM_LOADING: 'Problème lors du chargement',
ACCESS_DENIED: 'Accès refusé',
ANALOG_SENSOR: 'Capteur analogique',
ANALOG_SENSORS: 'Capteurs analogiques',
SETTINGS: 'Paramètres',
@@ -71,7 +70,6 @@ const fr: Translation = {
TEMP_SENSOR: 'Capteur de température',
TEMP_SENSORS: 'Capteurs de température',
WRITE_CMD_SENT: 'Envoyer la commande sent', // TODO translate
WRITE_CMD_FAILED: 'Envoyer la commande failed', // TODO translate
EMS_BUS_WARNING: 'Bus EMS déconnecté. Si ce message persiste après quelques secondes, vérifiez les paramètres et la configuration de la carte.',
EMS_BUS_SCANNING: 'Scan des appareils EMS...',
CONNECTED: 'Connecté',

View File

@@ -51,7 +51,6 @@ const nl: Translation = {
REMOVE: 'Verwijderen',
PROBLEM_UPDATING: 'Probleem met updaten',
PROBLEM_LOADING: 'Probleem met laden',
ACCESS_DENIED: 'Toegang geweigerd',
ANALOG_SENSOR: 'Analoge sensor',
ANALOG_SENSORS: 'Analoge Sensoren',
SETTINGS: 'Instellingen',
@@ -71,7 +70,6 @@ const nl: Translation = {
TEMP_SENSOR: 'Temperatuur sensor',
TEMP_SENSORS: 'Temperatuur Sensoren',
WRITE_CMD_SENT: 'Schrijf commando sent', // TODO translate
WRITE_CMD_FAILED: 'Schrijf commando failed', // TODO translate
EMS_BUS_WARNING: 'EMS bus niet gevonden. Als deze waarschuwing blijft staan na een paar seconden dan loop de instellingen na en in het bijzonder het apparaat type profiel na.',
EMS_BUS_SCANNING: 'Scannen naar EMS apparaten...',
CONNECTED: 'Verbonden',

View File

@@ -51,7 +51,6 @@ const no: Translation = {
REMOVE: 'Fjern',
PROBLEM_UPDATING: 'Problem med oppdatering',
PROBLEM_LOADING: 'Problem med opplasting',
ACCESS_DENIED: 'Tilgang nektet',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analoge Sensorer',
SETTINGS: 'Innstillinger',
@@ -71,7 +70,6 @@ const no: Translation = {
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperaturesensorer',
WRITE_CMD_SENT: 'Skriv kommando sent',
WRITE_CMD_FAILED: 'Skriv kommando som har feilet',
EMS_BUS_WARNING: 'EMS bussen koblet ned. Hvis denne advarselen fortsetter etter noen f¨sekunder sjekk instillinger og prosessorkort',
EMS_BUS_SCANNING: 'Søker etter EMS enheter...',
CONNECTED: 'Tilkoblet',

View File

@@ -51,7 +51,6 @@ const pl: BaseTranslation = {
REMOVE: 'Usuń',
PROBLEM_UPDATING: 'Problem z uaktualnieniem!',
PROBLEM_LOADING: 'Problem z załadowaniem!',
ACCESS_DENIED: 'Brak dostępu!',
ANALOG_SENSOR: '{{u|u||ustawienia u|ustawień u}}rządzeni{{a podłączonego do EMS-ESP|e||a podłączonego do EMS-ESP|a podłączonego do EMS-ESP}}',
ANALOG_SENSORS: 'Urządzenia podłączone do EMS-ESP',
SETTINGS: 'ustawienia',
@@ -71,7 +70,6 @@ const pl: BaseTranslation = {
TEMP_SENSOR: 'czujnika temperatury',
TEMP_SENSORS: 'Czujniki temperatury 1-Wire®',
WRITE_CMD_SENT: 'Komenda zapisu została wysłana.',
WRITE_CMD_FAILED: 'Komenda zapisu nie powiodła się!',
EMS_BUS_WARNING: 'Brak połączenia z magistralą EMS. Jeśli ten błąd występuje dłużej niż kilka sekund, sprawdź ustawienia oraz profil płytki interfejsu.',
EMS_BUS_SCANNING: 'Trwa skanowanie urządzeń na magistrali EMS...',
CONNECTED: '{{połączono|połączenie|}}',

View File

@@ -51,7 +51,6 @@ const sv: Translation = {
REMOVE: 'Ta bort',
PROBLEM_UPDATING: 'Problem vid uppdatering',
PROBLEM_LOADING: 'Problem vid hämtning',
ACCESS_DENIED: 'Åtkomst Nekad',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analoga Sensorer',
SETTINGS: 'Inställningar',
@@ -71,7 +70,6 @@ const sv: Translation = {
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperatursensorer',
WRITE_CMD_SENT: 'Skrivkommandon skickade',
WRITE_CMD_FAILED: 'Skrivkommandon misslyckade',
EMS_BUS_WARNING: 'EMS-buss nedkopplad. Om denna varning kvarstår efter några sekunder, kontrollera inställningar och enhets-profil.',
EMS_BUS_SCANNING: 'Söker efter EMS-enheter...',
CONNECTED: 'Ansluten',

View File

@@ -51,7 +51,6 @@ const tr: Translation = {
REMOVE: 'Kaldır',
PROBLEM_UPDATING: 'Güncelleme Sorunu',
PROBLEM_LOADING: 'Yükleme Sorunu',
ACCESS_DENIED: 'Erişim Reddedildi',
ANALOG_SENSOR: 'Analog Sensör',
ANALOG_SENSORS: 'Analog Sensörler',
SETTINGS: 'Ayarlar',
@@ -71,7 +70,6 @@ const tr: Translation = {
TEMP_SENSOR: 'Sıcaklık Sensörü',
TEMP_SENSORS: 'Sıcaklık Sensörleri',
WRITE_CMD_SENT: 'Yazma komutu gönderildi',
WRITE_CMD_FAILED: 'Yazma komutu başarısız oldu',
EMS_BUS_WARNING: 'EMS hat bağlantısı kesildi. Eğer bu uyarı birkaç saniye sonra devam ediyorsa lütfen ayarları ve kart tipini kontrol edin',
EMS_BUS_SCANNING: 'EMS cihazları aranıyor...',
CONNECTED: 'Bağlandı',

View File

@@ -30,6 +30,7 @@ import { useRowSelect } from '@table-library/react-table-library/select';
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useState, useContext, useEffect, useCallback, useLayoutEffect } from 'react';
import { IconContext } from 'react-icons';
@@ -42,27 +43,39 @@ import { formatValue } from './deviceValue';
import { DeviceValueUOM_s, DeviceEntityMask, DeviceType } from './types';
import { deviceValueItemValidation } from './validators';
import type { Device, CoreData, DeviceData, DeviceValue } from './types';
import type { Device, DeviceValue } from './types';
import type { FC } from 'react';
import { ButtonRow, SectionContent, MessageBox } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const DashboardDevices: FC = () => {
const [size, setSize] = useState([0, 0]);
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const [deviceData, setDeviceData] = useState<DeviceData>({ data: [] });
const [selectedDeviceValue, setSelectedDeviceValue] = useState<DeviceValue>();
const [onlyFav, setOnlyFav] = useState(false);
const [deviceValueDialogOpen, setDeviceValueDialogOpen] = useState(false);
const [showDeviceInfo, setShowDeviceInfo] = useState<boolean>(false);
const [selectedDevice, setSelectedDevice] = useState<number>();
const [coreData, setCoreData] = useState<CoreData>({
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), {
initialData: {
connected: true,
devices: []
}
});
const { data: deviceData, send: readDeviceData } = useRequest((id) => EMSESP.readDeviceData(id), {
initialData: {
data: []
},
immediate: false
});
const { loading: submitting, send: writeDeviceValue } = useRequest((data) => EMSESP.writeDeviceValue(data), {
immediate: false
});
useLayoutEffect(() => {
@@ -148,7 +161,7 @@ const DashboardDevices: FC = () => {
common_theme,
{
Table: `
--data-table-library_grid-template-columns: minmax(0, 1fr) minmax(150px, auto) 40px;
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
height: auto;
max-height: 100%;
overflow-y: scroll;
@@ -212,19 +225,10 @@ const DashboardDevices: FC = () => {
}
);
const fetchDeviceData = async (id: number) => {
try {
setDeviceData((await EMSESP.readDeviceData({ id })).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
function onSelectChange(action: any, state: any) {
setDeviceData({ data: [] });
async function onSelectChange(action: any, state: any) {
setSelectedDevice(state.id);
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
void fetchDeviceData(state.id);
await readDeviceData(state.id);
}
}
@@ -257,27 +261,14 @@ const DashboardDevices: FC = () => {
};
}, [escFunction]);
const fetchCoreData = useCallback(async () => {
try {
setSelectedDevice(undefined);
setCoreData((await EMSESP.readCoreData()).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
void fetchCoreData();
}, [fetchCoreData]);
const refreshData = () => {
if (deviceValueDialogOpen) {
return;
}
if (selectedDevice) {
void fetchDeviceData(selectedDevice);
void readDeviceData(selectedDevice);
} else {
void fetchCoreData();
void readCoreData();
}
};
@@ -346,27 +337,20 @@ const DashboardDevices: FC = () => {
};
});
const deviceValueDialogSave = async (dv: DeviceValue) => {
const selectedDeviceID = Number(device_select.state.id);
try {
const response = await EMSESP.writeDeviceValue({
id: selectedDeviceID,
devicevalue: dv
});
if (response.status === 204) {
toast.error(LL.WRITE_CMD_FAILED());
} else if (response.status === 403) {
toast.error(LL.ACCESS_DENIED());
} else {
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
const id = Number(device_select.state.id);
await writeDeviceValue({ id, devicevalue })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
})
.catch((error) => {
toast.error(error.message);
})
.finally(async () => {
setDeviceValueDialogOpen(false);
await fetchDeviceData(selectedDeviceID);
await readDeviceData(id);
setSelectedDeviceValue(undefined);
}
});
};
const renderDeviceDetails = () => {
@@ -500,20 +484,11 @@ const DashboardDevices: FC = () => {
}}
>
<Box sx={{ border: '1px solid #177ac9' }}>
<Grid container justifyContent="space-between">
<Box color="warning.main" ml={1}>
<Typography noWrap variant="h6">
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ mx: 1 }}>
{coreData.devices[deviceIndex].n}
</Typography>
</Box>
<Grid item zeroMinWidth justifyContent="flex-end">
<IconButton onClick={resetDeviceSelect}>
<CancelIcon color="info" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
</Grid>
</Grid>
<Grid item xs>
<Grid container justifyContent="space-between">
<Typography sx={{ ml: 1 }} variant="subtitle2" color="primary">
{shown_data.length + ' ' + LL.ENTITIES(shown_data.length)}
<IconButton onClick={() => setShowDeviceInfo(true)}>
@@ -533,6 +508,11 @@ const DashboardDevices: FC = () => {
<RefreshIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
</Typography>
<Grid item zeroMinWidth justifyContent="flex-end">
<IconButton onClick={resetDeviceSelect}>
<CancelIcon color="info" sx={{ fontSize: 18, verticalAlign: 'middle' }} />
</IconButton>
</Grid>
</Grid>
</Box>
@@ -612,6 +592,7 @@ const DashboardDevices: FC = () => {
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
}
validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting}
/>
)}
<ButtonRow>

View File

@@ -13,8 +13,10 @@ import {
FormHelperText,
Grid,
Box,
Typography
Typography,
CircularProgress
} from '@mui/material';
import { green } from '@mui/material/colors';
import { useState, useEffect } from 'react';
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
@@ -35,6 +37,7 @@ type DashboardDevicesDialogProps = {
selectedItem: DeviceValue;
writeable: boolean;
validator: Schema;
progress: boolean;
};
const DashboarDevicesDialog = ({
@@ -43,7 +46,8 @@ const DashboarDevicesDialog = ({
onSave,
selectedItem,
writeable,
validator
validator,
progress
}: DashboardDevicesDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
@@ -115,7 +119,8 @@ const DashboarDevicesDialog = ({
sx={{
'& .MuiDialog-paper': {
borderRadius: '12px'
}
},
backdropFilter: 'blur(1px)'
}}
>
<DialogTitle>
@@ -184,14 +189,32 @@ const DashboarDevicesDialog = ({
<DialogActions>
{writeable ? (
<>
<Box
sx={{
'& button, & a, & .MuiCard-root': {
mx: 0.6
},
position: 'relative'
}}
>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary">
{LL.CANCEL()}
</Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info">
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
</Button>
</>
{progress && (
<CircularProgress
size={24}
sx={{
color: green[500],
position: 'absolute',
right: '20%',
marginTop: '6px'
}}
/>
)}
</Box>
) : (
<Button variant="outlined" onClick={close} color="secondary">
{LL.CLOSE()}

View File

@@ -7,7 +7,8 @@ import { Button, Typography, Box } from '@mui/material';
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useState, useContext, useCallback, useEffect } from 'react';
import { useRequest } from 'alova';
import { useState, useContext, useEffect } from 'react';
import { toast } from 'react-toastify';
@@ -17,24 +18,38 @@ import * as EMSESP from './api';
import { DeviceValueUOM, DeviceValueUOM_s, AnalogTypeNames } from './types';
import { temperatureSensorItemValidation, analogSensorItemValidation } from './validators';
import type { SensorData, TemperatureSensor, AnalogSensor } from './types';
import type { TemperatureSensor, AnalogSensor } from './types';
import type { FC } from 'react';
import { ButtonRow, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const DashboardSensors: FC = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
const [sensorData, setSensorData] = useState<SensorData>({ ts: [], as: [], analog_enabled: false });
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>();
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
const [creating, setCreating] = useState<boolean>(false);
const { data: sensorData, send: fetchSensorData } = useRequest(() => EMSESP.readSensorData(), {
initialData: {
ts: [],
as: [],
analog_enabled: false
}
});
const { send: writeTemperatureSensor } = useRequest((data) => EMSESP.writeTemperatureSensor(data), {
immediate: false
});
const { send: writeAnalogSensor } = useRequest((data) => EMSESP.writeAnalogSensor(data), {
immediate: false
});
const isAdmin = me.admin;
const common_theme = useTheme({
@@ -101,20 +116,6 @@ const DashboardSensors: FC = () => {
}
]);
const fetchSensorData = useCallback(async () => {
if (!analogDialogOpen && !temperatureDialogOpen) {
try {
setSensorData((await EMSESP.readSensorData()).data);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}
}, [LL, analogDialogOpen, temperatureDialogOpen]);
useEffect(() => {
void fetchSensorData();
}, [fetchSensorData]);
const getSortIcon = (state: any, sortKey: any) => {
if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />;
@@ -229,26 +230,18 @@ const DashboardSensors: FC = () => {
};
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
try {
const response = await EMSESP.writeTemperatureSensor({
id: ts.id,
name: ts.n,
offset: ts.o
});
if (response.status === 204) {
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
} else if (response.status === 403) {
toast.error(LL.ACCESS_DENIED());
} else {
await writeTemperatureSensor({ id: ts.id, name: ts.n, offset: ts.o })
.then(() => {
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
})
.catch(() => {
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
})
.finally(async () => {
setTemperatureDialogOpen(false);
setSelectedTemperatureSensor(undefined);
await fetchSensorData();
}
});
};
const updateAnalogSensor = (as: AnalogSensor) => {
@@ -280,8 +273,7 @@ const DashboardSensors: FC = () => {
};
const onAnalogDialogSave = async (as: AnalogSensor) => {
try {
const response = await EMSESP.writeAnalogSensor({
await writeAnalogSensor({
id: as.id,
gpio: as.g,
name: as.n,
@@ -290,22 +282,18 @@ const DashboardSensors: FC = () => {
uom: as.u,
type: as.t,
deleted: as.d
});
if (response.status === 204) {
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
} else if (response.status === 403) {
toast.error(LL.ACCESS_DENIED());
} else {
})
.then(() => {
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
})
.catch(() => {
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
})
.finally(async () => {
setAnalogDialogOpen(false);
setSelectedAnalogSensor(undefined);
await fetchSensorData();
}
});
};
const RenderTemperatureSensors = () => (
@@ -431,7 +419,7 @@ const DashboardSensors: FC = () => {
{sensorData?.analog_enabled === true && (
<>
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
{LL.ANALOG_SENSORS(0)}
{LL.ANALOG_SENSORS()}
</Typography>
<RenderAnalogSensors />
{selectedAnalogSensor && (

View File

@@ -19,6 +19,7 @@ import {
} from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
@@ -32,7 +33,6 @@ import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage, useRest } from 'utils';
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
@@ -64,7 +64,7 @@ const showQuality = (stat: Stat) => {
};
const DashboardStatus: FC = () => {
const { loadData, data, errorMessage } = useRest<Status>({ read: EMSESP.readStatus });
const { data: data, send: loadData, error } = useRequest(EMSESP.readStatus);
const { LL } = useI18nContext();
@@ -73,6 +73,10 @@ const DashboardStatus: FC = () => {
const { me } = useContext(AuthenticatedContext);
const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
immediate: false
});
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
@@ -158,14 +162,14 @@ const DashboardStatus: FC = () => {
};
const scan = async () => {
try {
await EMSESP.scanDevices();
await scanDevices()
.then(() => {
toast.info(LL.SCANNING() + '...');
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
})
.catch((err) => {
toast.error(err.message);
});
setConfirmScan(false);
}
};
const renderScanDialog = () => (
@@ -185,7 +189,7 @@ const DashboardStatus: FC = () => {
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (

View File

@@ -4,6 +4,7 @@ import DownloadIcon from '@mui/icons-material/GetApp';
import GitHubIcon from '@mui/icons-material/GitHub';
import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
import { Typography, Button, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@mui/material';
import { useRequest } from 'alova';
import { toast } from 'react-toastify';
import * as EMSESP from './api';
import type { FC } from 'react';
@@ -11,16 +12,19 @@ import type { FC } from 'react';
import { SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const HelpInformation: FC = () => {
const { LL } = useI18nContext();
const saveFile = (json: any, endpoint: string) => {
const { send: API, onSuccess: onSuccessAPI } = useRequest((data) => EMSESP.API(data), {
immediate: false
});
onSuccessAPI((event) => {
const a = document.createElement('a');
const filename = 'emsesp_' + endpoint + '.txt';
const filename = 'emsesp_info.txt';
a.href = URL.createObjectURL(
new Blob([JSON.stringify(json, null, 2)], {
new Blob([JSON.stringify(event.data, null, 2)], {
type: 'text/plain'
})
);
@@ -29,23 +33,12 @@ const HelpInformation: FC = () => {
a.click();
document.body.removeChild(a);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
const callAPI = async (endpoint: string) => {
try {
const response = await EMSESP.API({
device: 'system',
entity: endpoint,
id: 0
});
if (response.status !== 200) {
toast.error(LL.PROBLEM_LOADING());
} else {
saveFile(response.data, endpoint);
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
const callAPI = async () => {
await API({ device: 'system', entity: 'info', id: 0 }).catch((error) => {
toast.error(error.message);
});
};
return (
@@ -96,7 +89,7 @@ const HelpInformation: FC = () => {
size="small"
variant="outlined"
color="primary"
onClick={() => callAPI('info')}
onClick={() => callAPI()}
>
{LL.SUPPORT_INFO()}
</Button>

View File

@@ -2,6 +2,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment, TextField } from '@mui/material';
import { useRequest } from 'alova';
import { useState } from 'react';
import { toast } from 'react-toastify';
@@ -11,6 +12,7 @@ import { createSettingsValidator } from './validators';
import type { Settings } from './types';
import type { ValidateFieldsError } from 'async-validator';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
import {
SectionContent,
FormLoader,
@@ -23,7 +25,7 @@ import {
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, extractErrorMessage, updateValueDirty, useRest } from 'utils';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
export function boardProfileSelectItems() {
@@ -39,7 +41,7 @@ const SettingsApplication: FC = () => {
loadData,
saveData,
saving,
setData,
updateDataValue,
data,
origData,
dirtyFlags,
@@ -51,39 +53,48 @@ const SettingsApplication: FC = () => {
read: EMSESP.readSettings,
update: EMSESP.writeSettings
});
const [restarting, setRestarting] = useState<boolean>();
const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, setData);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [processingBoard, setProcessingBoard] = useState<boolean>(false);
const updateBoardProfile = async (boardProfile: string) => {
setProcessingBoard(true);
try {
const response = await EMSESP.getBoardProfile({ board_profile: boardProfile });
if (data) {
setData({
...data,
board_profile: boardProfile,
led_gpio: response.data.led_gpio,
dallas_gpio: response.data.dallas_gpio,
rx_gpio: response.data.rx_gpio,
tx_gpio: response.data.tx_gpio,
pbutton_gpio: response.data.pbutton_gpio,
phy_type: response.data.phy_type,
eth_power: response.data.eth_power,
eth_phy_addr: response.data.eth_phy_addr,
eth_clock_mode: response.data.eth_clock_mode
const {
loading: processingBoard,
send: readBoardProfile,
onSuccess: onSuccessBoardProfile
} = useRequest((boardProfile) => EMSESP.getBoardProfile(boardProfile), {
immediate: false
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
onSuccessBoardProfile((event) => {
const response = event.data as Settings;
updateDataValue({
...data,
board_profile: response.board_profile,
led_gpio: response.led_gpio,
dallas_gpio: response.dallas_gpio,
rx_gpio: response.rx_gpio,
tx_gpio: response.tx_gpio,
pbutton_gpio: response.pbutton_gpio,
phy_type: response.phy_type,
eth_power: response.eth_power,
eth_phy_addr: response.eth_phy_addr,
eth_clock_mode: response.eth_clock_mode
});
});
const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error) => {
toast.error(error.message);
});
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setProcessingBoard(false);
}
};
const content = () => {
@@ -95,9 +106,10 @@ const SettingsApplication: FC = () => {
try {
setFieldErrors(undefined);
await validate(createSettingsValidator(data), data);
await saveData();
} catch (errors: any) {
setFieldErrors(errors);
} finally {
await saveData();
}
};
@@ -105,7 +117,7 @@ const SettingsApplication: FC = () => {
const boardProfile = event.target.value;
updateFormValue(event);
if (boardProfile === 'CUSTOM') {
setData({
updateDataValue({
...data,
board_profile: boardProfile
});
@@ -116,12 +128,10 @@ const SettingsApplication: FC = () => {
const restart = async () => {
await validateAndSubmit();
try {
await EMSESP.restart();
await restartCommand().catch((error) => {
toast.error(error.message);
});
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
};
return (

View File

@@ -21,6 +21,7 @@ import {
} from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -32,13 +33,13 @@ import SettingsCustomizationDialog from './SettingsCustomizationDialog';
import * as EMSESP from './api';
import { DeviceEntityMask } from './types';
import type { DeviceShort, Devices, DeviceEntity } from './types';
import type { DeviceShort, DeviceEntity } from './types';
import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, MessageBox, BlockNavigation } from 'components';
import * as SystemApi from 'api/system';
import { ButtonRow, SectionContent, MessageBox, BlockNavigation } from 'components';
import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
export const APIURL = window.location.origin + '/api/';
@@ -46,11 +47,10 @@ const SettingsCustomization: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [restarting, setRestarting] = useState<boolean>(false);
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>();
const [devices, setDevices] = useState<Devices>();
const [errorMessage, setErrorMessage] = useState<string>();
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([]);
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
const [confirmReset, setConfirmReset] = useState<boolean>(false);
const [selectedFilters, setSelectedFilters] = useState<number>(0);
@@ -58,6 +58,31 @@ const SettingsCustomization: FC = () => {
const [selectedDeviceEntity, setSelectedDeviceEntity] = useState<DeviceEntity>();
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
immediate: false
});
const { data: devices } = useRequest(EMSESP.readDevices);
const { send: writeCustomEntities } = useRequest((data) => EMSESP.writeCustomEntities(data), { immediate: false });
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest((data) => EMSESP.readDeviceEntities(data), {
initialData: [],
immediate: false
});
const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
};
onSuccess((event) => {
setOriginalSettings(event.data);
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const entities_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
@@ -131,7 +156,7 @@ const SettingsCustomization: FC = () => {
}
useEffect(() => {
if (deviceEntities) {
if (deviceEntities.length) {
setNumChanges(
deviceEntities
.filter((de) => hasEntityChanged(de))
@@ -148,29 +173,11 @@ const SettingsCustomization: FC = () => {
}
}, [deviceEntities]);
const fetchDevices = useCallback(async () => {
try {
setDevices((await EMSESP.readDevices()).data);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
void fetchDevices();
}, [fetchDevices]);
const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma })));
};
const fetchDeviceEntities = async (unique_id: number) => {
try {
const new_deviceEntities = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
setOriginalSettings(new_deviceEntities);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
const restart = async () => {
await restartCommand().catch((error) => {
toast.error(error.message);
});
setRestarting(true);
};
function formatValue(value: any) {
@@ -226,7 +233,7 @@ const SettingsCustomization: FC = () => {
const maskDisabled = (set: boolean) => {
setDeviceEntities(
deviceEntities?.map(function (de) {
deviceEntities.map(function (de) {
if ((de.m & selectedFilters || !selectedFilters) && de.id.toLowerCase().includes(search.toLowerCase())) {
return {
...de,
@@ -246,31 +253,22 @@ const SettingsCustomization: FC = () => {
const selected_device = parseInt(event.target.value, 10);
setSelectedDevice(selected_device);
setNumChanges(0);
void fetchDeviceEntities(devices?.devices[selected_device].i);
void readDeviceEntities(devices?.devices[selected_device].i);
setRestartNeeded(false);
}
};
const resetCustomization = async () => {
try {
await EMSESP.resetCustomizations();
await resetCustomizations();
toast.info(LL.CUSTOMIZATIONS_RESTART());
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
toast.error(error.message);
} finally {
setConfirmReset(false);
}
};
const restart = async () => {
try {
await EMSESP.restart();
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
};
const onDialogClose = () => {
setDialogOpen(false);
};
@@ -300,7 +298,7 @@ const SettingsCustomization: FC = () => {
const saveCustomization = async () => {
if (devices && deviceEntities && selectedDevice !== -1) {
const masked_entities = deviceEntities
.filter((de) => hasEntityChanged(de))
.filter((de: DeviceEntity) => hasEntityChanged(de))
.map(
(new_de) =>
new_de.m.toString(16).padStart(2, '0') +
@@ -318,31 +316,20 @@ const SettingsCustomization: FC = () => {
return;
}
try {
const response = await EMSESP.writeCustomEntities({
id: devices?.devices[selectedDevice].i,
entity_ids: masked_entities
});
if (response.status === 200) {
toast.success(LL.CUSTOMIZATIONS_SAVED());
} else if (response.status === 201) {
await writeCustomEntities({ id: devices?.devices[selectedDevice].i, entity_ids: masked_entities }).catch(
(error) => {
if (error.message === 'Reboot required') {
setRestartNeeded(true);
} else {
toast.error(LL.PROBLEM_UPDATING());
toast.error(error.message);
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
);
setOriginalSettings(deviceEntities);
}
};
const renderDeviceList = () => {
if (!devices) {
return <FormLoader errorMessage={errorMessage} />;
}
return (
const renderDeviceList = () => (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
@@ -376,14 +363,9 @@ const SettingsCustomization: FC = () => {
</TextField>
</>
);
};
const renderDeviceData = () => {
if (!deviceEntities) {
return;
}
if (devices?.devices.length === 0 || deviceEntities[0].id === '') {
if (deviceEntities.length === 0) {
return;
}
@@ -521,7 +503,7 @@ const SettingsCustomization: FC = () => {
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DEVICE_ENTITIES()}
</Typography>
{renderDeviceList()}
{devices && renderDeviceList()}
{renderDeviceData()}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
@@ -539,7 +521,7 @@ const SettingsCustomization: FC = () => {
startIcon={<CancelIcon />}
variant="outlined"
color="secondary"
onClick={() => devices && fetchDeviceEntities(devices.devices[selectedDevice].i)}
onClick={() => devices && readDeviceEntities(devices.devices[selectedDevice].i)}
>
{LL.CANCEL()}
</Button>

View File

@@ -124,7 +124,7 @@ const SettingsCustomizationDialog = ({ open, onClose, onSave, selectedItem }: Se
{LL.CANCEL()}
</Button>
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary">
{LL.UPDATE(0)}
{LL.UPDATE()}
</Button>
</DialogActions>
</Dialog>

View File

@@ -4,7 +4,9 @@ import WarningIcon from '@mui/icons-material/Warning';
import { Button, Typography, Box } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useState, useEffect, useCallback } from 'react';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova';
import { useState, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -18,18 +20,26 @@ import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const SettingsEntities: FC = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [entities, setEntities] = useState<EntityItem[]>();
const [selectedEntityItem, setSelectedEntityItem] = useState<EntityItem>();
const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const {
data: entities,
send: fetchEntities,
error
} = useRequest(EMSESP.readEntities, {
initialData: [],
force: true
});
const { send: writeEntities } = useRequest((data) => EMSESP.writeEntities(data), { immediate: false });
function hasEntityChanged(ei: EntityItem) {
return (
ei.id !== ei.o_id ||
@@ -45,12 +55,6 @@ const SettingsEntities: FC = () => {
);
}
useEffect(() => {
if (entities) {
setNumChanges(entities ? entities.filter((ei) => hasEntityChanged(ei)).length : 0);
}
}, [entities]);
const entity_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px;
@@ -105,37 +109,8 @@ const SettingsEntities: FC = () => {
`
});
const fetchEntities = useCallback(async () => {
try {
const response = await EMSESP.readEntities();
setEntities(
response.data.entities.map((ei) => ({
...ei,
o_id: ei.id,
o_device_id: ei.device_id,
o_type_id: ei.type_id,
o_offset: ei.offset,
o_factor: ei.factor,
o_uom: ei.uom,
o_value_type: ei.value_type,
o_name: ei.name,
o_writeable: ei.writeable,
o_deleted: ei.deleted
}))
);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
void fetchEntities();
}, [fetchEntities]);
const saveEntities = async () => {
if (entities) {
try {
const response = await EMSESP.writeEntities({
await writeEntities({
entities: entities
.filter((ei) => !ei.deleted)
.map((condensed_ei) => ({
@@ -149,18 +124,17 @@ const SettingsEntities: FC = () => {
writeable: condensed_ei.writeable,
value_type: condensed_ei.value_type
}))
});
if (response.status === 200) {
})
.then(() => {
toast.success(LL.ENTITIES_UPDATED());
} else {
toast.error(LL.PROBLEM_UPDATING());
}
})
.catch((err) => {
toast.error(err.message);
})
.finally(async () => {
await fetchEntities();
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
}
setNumChanges(0);
});
};
const editEntityItem = useCallback((ei: EntityItem) => {
@@ -173,13 +147,22 @@ const SettingsEntities: FC = () => {
setDialogOpen(false);
};
const onDialogCancel = async () => {
await fetchEntities().then(() => {
setNumChanges(0);
});
};
const onDialogSave = (updatedItem: EntityItem) => {
setDialogOpen(false);
if (entities && creating) {
setEntities([...entities.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]);
} else {
setEntities(entities?.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei)));
}
updateState('entities', (data) => {
const new_data = creating
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem]
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei));
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
return new_data;
});
};
const addEntityItem = () => {
@@ -208,14 +191,12 @@ const SettingsEntities: FC = () => {
}
function showHex(value: number, digit: number) {
return digit === 4
? '0x' + ('000' + value.toString(16).toUpperCase()).slice(-4)
: '0x' + ('0' + value.toString(16).toUpperCase()).slice(-2);
return '0x' + value.toString(16).toUpperCase().padStart(digit, '0');
}
const renderEntity = () => {
if (!entities) {
return <FormLoader errorMessage={errorMessage} />;
return <FormLoader onRetry={fetchEntities} errorMessage={error?.message} />;
}
return (
@@ -236,7 +217,7 @@ const SettingsEntities: FC = () => {
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell>{ei.name}</Cell>
<Cell>{showHex(ei.device_id as number, 2)}</Cell>
<Cell>{showHex(ei.type_id as number, 4)}</Cell>
<Cell>{showHex(ei.type_id as number, 3)}</Cell>
<Cell>{ei.offset}</Cell>
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
</Row>
@@ -272,7 +253,7 @@ const SettingsEntities: FC = () => {
<Box flexGrow={1}>
{numChanges > 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={fetchEntities} color="secondary">
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
{LL.CANCEL()}
</Button>
<Button

View File

@@ -58,8 +58,8 @@ const SettingsEntitiesDialog = ({
// convert to hex strings straight away
setEditItem({
...selectedItem,
device_id: selectedItem.device_id.toString(16).toUpperCase().slice(-2),
type_id: selectedItem.type_id.toString(16).toUpperCase().slice(-4)
device_id: selectedItem.device_id.toString(16).toUpperCase(),
type_id: selectedItem.type_id.toString(16).toUpperCase()
});
}
}, [open, selectedItem]);

View File

@@ -6,6 +6,8 @@ import WarningIcon from '@mui/icons-material/Warning';
import { Box, Typography, Divider, Stack, Button } from '@mui/material';
import { Table, Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
// eslint-disable-next-line import/named
import { updateState, useRequest } from 'alova';
import { useState, useEffect, useCallback } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -19,23 +21,31 @@ import type { FC } from 'react';
import { ButtonRow, FormLoader, SectionContent, BlockNavigation } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { extractErrorMessage } from 'utils';
const SettingsScheduler: FC = () => {
const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
const [schedule, setSchedule] = useState<ScheduleItem[]>([]);
const [selectedScheduleItem, setSelectedScheduleItem] = useState<ScheduleItem>();
const [dow, setDow] = useState<string[]>([]);
const [errorMessage, setErrorMessage] = useState<string>();
const [creating, setCreating] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const {
data: schedule,
send: fetchSchedule,
error
} = useRequest(EMSESP.readSchedule, {
initialData: [],
force: true
});
const { send: writeSchedule } = useRequest((data) => EMSESP.writeSchedule(data), { immediate: false });
function hasScheduleChanged(si: ScheduleItem) {
return (
si.id !== si.o_id ||
(si?.name || '') !== (si?.o_name || '') ||
(si.name || '') !== (si.o_name || '') ||
si.active !== si.o_active ||
si.deleted !== si.o_deleted ||
si.flags !== si.o_flags ||
@@ -46,10 +56,13 @@ const SettingsScheduler: FC = () => {
}
useEffect(() => {
if (schedule) {
setNumChanges(schedule ? schedule.filter((si) => hasScheduleChanged(si)).length : 0);
}
}, [schedule]);
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
});
setDow(days.map((date) => formatter.format(date)));
}, [locale]);
const schedule_theme = useTheme({
Table: `
@@ -96,41 +109,8 @@ const SettingsScheduler: FC = () => {
`
});
const fetchSchedule = useCallback(async () => {
try {
const response = await EMSESP.readSchedule();
setSchedule(
response.data.schedule.map((si) => ({
...si,
o_id: si.id,
o_active: si.active,
o_deleted: si.deleted,
o_flags: si.flags,
o_time: si.time,
o_cmd: si.cmd,
o_value: si.value,
o_name: si.name
}))
);
} catch (error) {
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, [LL]);
useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' });
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`);
});
setDow(days.map((date) => formatter.format(date)));
void fetchSchedule();
}, [locale, fetchSchedule]);
const saveSchedule = async () => {
if (schedule) {
try {
const response = await EMSESP.writeSchedule({
await writeSchedule({
schedule: schedule
.filter((si) => !si.deleted)
.map((condensed_si) => ({
@@ -142,17 +122,17 @@ const SettingsScheduler: FC = () => {
value: condensed_si.value,
name: condensed_si.name
}))
});
if (response.status === 200) {
})
.then(() => {
toast.success(LL.SCHEDULE_UPDATED());
} else {
toast.error(LL.PROBLEM_UPDATING());
}
})
.catch((err) => {
toast.error(err.message);
})
.finally(async () => {
await fetchSchedule();
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
}
setNumChanges(0);
});
};
const editScheduleItem = useCallback((si: ScheduleItem) => {
@@ -165,13 +145,22 @@ const SettingsScheduler: FC = () => {
setDialogOpen(false);
};
const onDialogCancel = async () => {
await fetchSchedule().then(() => {
setNumChanges(0);
});
};
const onDialogSave = (updatedItem: ScheduleItem) => {
setDialogOpen(false);
if (schedule && creating) {
setSchedule([...schedule.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]);
} else {
setSchedule(schedule?.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si)));
}
updateState('schedule', (data) => {
const new_data = creating
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem]
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si));
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
return new_data;
});
};
const addScheduleItem = () => {
@@ -191,7 +180,7 @@ const SettingsScheduler: FC = () => {
const renderSchedule = () => {
if (!schedule) {
return <FormLoader errorMessage={errorMessage} />;
return <FormLoader onRetry={fetchSchedule} errorMessage={error?.message} />;
}
const dayBox = (si: ScheduleItem, flag: number) => (
@@ -270,7 +259,7 @@ const SettingsScheduler: FC = () => {
onClose={onDialogClose}
onSave={onDialogSave}
selectedItem={selectedScheduleItem}
validator={schedulerItemValidation()}
validator={schedulerItemValidation(schedule, selectedScheduleItem)}
dow={dow}
/>
)}
@@ -279,7 +268,7 @@ const SettingsScheduler: FC = () => {
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={fetchSchedule} color="secondary">
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary">
{LL.CANCEL()}
</Button>
<Button

View File

@@ -215,7 +215,7 @@ const SettingsSchedulerDialog = ({
/>
<TextField
name="value"
label={LL.VALUE(0)}
label={LL.VALUE(1)}
multiline
margin="normal"
fullWidth

View File

@@ -1,121 +1,107 @@
import type {
BoardProfile,
BoardProfileName,
APIcall,
Settings,
Status,
CoreData,
Devices,
DeviceData,
DeviceEntity,
UniqueID,
CustomEntities,
WriteDeviceValue,
WriteTemperatureSensor,
WriteAnalogSensor,
SensorData,
Schedule,
Entities
Entities,
DeviceData,
ScheduleItem,
EntityItem
} from './types';
import type { AxiosPromise } from 'axios';
import { AXIOS, AXIOS_API, AXIOS_BIN } from 'api/endpoints';
import { alovaInstance } from 'api/endpoints';
export function restart(): AxiosPromise<void> {
return AXIOS.post('/restart');
}
// DashboardDevices
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
export const readDeviceData = (id: number) =>
alovaInstance.Get<DeviceData>('/rest/deviceData', {
params: { id },
responseType: 'arraybuffer' // uses msgpack
});
export const writeDeviceValue = (data: any) => alovaInstance.Post('/rest/writeDeviceValue', data);
export function readSettings(): AxiosPromise<Settings> {
return AXIOS.get('/settings');
}
// SettingsApplication
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
export const writeSettings = (data: any) => alovaInstance.Post('/rest/settings', data);
export const getBoardProfile = (boardProfile: string) =>
alovaInstance.Get('/rest/boardProfile', {
params: { boardProfile }
});
export function writeSettings(settings: Settings): AxiosPromise<Settings> {
return AXIOS.post('/settings', settings);
}
// DashboardSensors
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData');
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
alovaInstance.Post('/rest/writeTemperatureSensor', ts);
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as);
export function getBoardProfile(boardProfile: BoardProfileName): AxiosPromise<BoardProfile> {
return AXIOS.post('/boardProfile', boardProfile);
}
// DashboardStatus
export const readStatus = () => alovaInstance.Get<Status>('/rest/status');
export const scanDevices = () => alovaInstance.Post('/rest/scanDevices');
export function readStatus(): AxiosPromise<Status> {
return AXIOS.get('/status');
}
// HelpInformation
export const API = (apiCall: APIcall) => alovaInstance.Post('/api', apiCall);
export function readCoreData(): AxiosPromise<CoreData> {
return AXIOS.get('/coreData');
}
// UploadFileForm
export const getSettings = () => alovaInstance.Get('/rest/getSettings');
export const getCustomizations = () => alovaInstance.Get('/rest/getCustomizations');
export const getEntities = () => alovaInstance.Get<Entities>('/rest/getEntities');
export const getSchedule = () => alovaInstance.Get('/rest/getSchedule');
export function readDevices(): AxiosPromise<Devices> {
return AXIOS.get('/devices');
}
// SettingsCustomization
export const readDeviceEntities = (id: number) =>
alovaInstance.Get<DeviceEntity[]>('/rest/deviceEntities', {
params: { id },
responseType: 'arraybuffer',
transformData(data: any) {
return data.map((de: DeviceEntity) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }));
}
});
export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices');
export const resetCustomizations = () => alovaInstance.Post('/rest/resetCustomizations');
export const writeCustomEntities = (data: any) => alovaInstance.Post('/rest/customEntities', data);
export function scanDevices(): AxiosPromise<void> {
return AXIOS.post('/scanDevices');
}
// SettingsScheduler
export const readSchedule = () =>
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
name: 'schedule',
transformData(data: any) {
return data.schedule.map((si: ScheduleItem) => ({
...si,
o_id: si.id,
o_active: si.active,
o_deleted: si.deleted,
o_flags: si.flags,
o_time: si.time,
o_cmd: si.cmd,
o_value: si.value,
o_name: si.name
}));
}
});
export const writeSchedule = (data: any) => alovaInstance.Post('/rest/schedule', data);
export function readDeviceData(unique_id: UniqueID): AxiosPromise<DeviceData> {
return AXIOS_BIN.post('/deviceData', unique_id);
}
export function readSensorData(): AxiosPromise<SensorData> {
return AXIOS.get('/sensorData');
}
export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEntity[]> {
return AXIOS_BIN.post('/deviceEntities', unique_id);
}
export function writeCustomEntities(customEntities: CustomEntities): AxiosPromise<void> {
return AXIOS.post('/customEntities', customEntities);
}
export function writeDeviceValue(dv: WriteDeviceValue): AxiosPromise<void> {
return AXIOS.post('/writeDeviceValue', dv);
}
export function writeTemperatureSensor(ts: WriteTemperatureSensor): AxiosPromise<void> {
return AXIOS.post('/writeTemperatureSensor', ts);
}
export function writeAnalogSensor(as: WriteAnalogSensor): AxiosPromise<void> {
return AXIOS.post('/writeAnalogSensor', as);
}
export function resetCustomizations(): AxiosPromise<void> {
return AXIOS.post('/resetCustomizations');
}
export function API(apiCall: APIcall): AxiosPromise<void> {
return AXIOS_API.post('/', apiCall);
}
export function getSettings(): AxiosPromise<void> {
return AXIOS.get('/getSettings');
}
export function getCustomizations(): AxiosPromise<void> {
return AXIOS.get('/getCustomizations');
}
export function getSchedule(): AxiosPromise<Schedule> {
return AXIOS.get('/getSchedule');
}
export function readSchedule(): AxiosPromise<Schedule> {
return AXIOS.get('/schedule');
}
export function writeSchedule(schedule: Schedule): AxiosPromise<void> {
return AXIOS.post('/schedule', schedule);
}
export function getEntities(): AxiosPromise<Entities> {
return AXIOS.get('/getEntities');
}
export function readEntities(): AxiosPromise<Entities> {
return AXIOS.get('/entities');
}
export function writeEntities(entities: Entities): AxiosPromise<void> {
return AXIOS.post('/entities', entities);
}
// SettingsEntities
export const readEntities = () =>
alovaInstance.Get<EntityItem[]>('/rest/entities', {
name: 'entities',
transformData(data: any) {
return data.entities.map((ei: EntityItem) => ({
...ei,
o_id: ei.id,
o_device_id: ei.device_id,
o_type_id: ei.type_id,
o_offset: ei.offset,
o_factor: ei.factor,
o_uom: ei.uom,
o_value_type: ei.value_type,
o_name: ei.name,
o_writeable: ei.writeable,
o_deleted: ei.deleted
}));
}
});
export const writeEntities = (data: any) => alovaInstance.Post('/rest/entities', data);

View File

@@ -131,7 +131,6 @@ export interface DeviceValue {
m?: number; // min, optional
x?: number; // max, optional
}
export interface DeviceData {
data: DeviceValue[];
}
@@ -151,15 +150,6 @@ export interface DeviceEntity {
o_ma?: number; // original max value
}
export interface CustomEntities {
id: number;
entity_ids: string[];
}
export interface UniqueID {
id: number;
}
export enum DeviceValueUOM {
NONE = 0,
DEGREES,
@@ -256,10 +246,6 @@ export const BOARD_PROFILES: BoardProfiles = {
S3MINI: 'Liligo S3'
};
export interface BoardProfileName {
board_profile: string;
}
export interface BoardProfile {
board_profile: string;
led_gpio: number;
@@ -278,12 +264,6 @@ export interface APIcall {
entity: string;
id: any;
}
export interface WriteDeviceValue {
id: number;
devicevalue: DeviceValue;
}
export interface WriteAnalogSensor {
id: number;
gpio: number;
@@ -312,7 +292,7 @@ export interface ScheduleItem {
time: string;
cmd: string;
value: string;
name?: string; // optional
name: string; // optional
o_id?: number;
o_active?: boolean;
o_deleted?: boolean;
@@ -323,10 +303,6 @@ export interface ScheduleItem {
o_name?: string;
}
export interface Schedule {
schedule: ScheduleItem[];
}
export enum ScheduleFlag {
SCHEDULE_SUN = 1,
SCHEDULE_MON = 2,

View File

@@ -1,5 +1,5 @@
import Schema from 'async-validator';
import type { AnalogSensor, DeviceValue, Settings } from './types';
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
import type { InternalRuleItem } from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
@@ -86,14 +86,26 @@ export const createSettingsValidator = (settings: Settings) =>
})
});
export const schedulerItemValidation = () =>
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) {
if ((o_name === undefined || o_name !== name) && schedule.find((si) => si.name === name)) {
callback('Name already in use');
} else {
callback();
}
}
});
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) =>
new Schema({
name: [
{
required: true,
type: 'string',
pattern: /^[a-zA-Z0-9_\\.]{0,15}$/,
message: "Must be <15 characters: alpha numeric, '_' or '.'"
}
},
...[uniqueNameValidator(schedule, scheduleItem.o_name)]
],
cmd: [
{ required: true, message: 'Command is required' },

View File

@@ -42,10 +42,6 @@ export interface LogEntry {
m: string;
}
export interface LogEntries {
events: LogEntry[];
}
export interface LogSettings {
level: number;
max_messages: number;

View File

@@ -1,5 +1,3 @@
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
export const numberValue = (value: number) => (isNaN(value) ? '' : value.toString());
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -13,6 +11,8 @@ export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) =>
}
};
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
export const updateValue =
<S>(updateEntity: UpdateEntity<S>) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
@@ -23,11 +23,12 @@ export const updateValue =
};
export const updateValueDirty =
<S>(origData: any, dirtyFlags: any, setDirtyFlags: any, updateEntity: UpdateEntity<S>) =>
(origData: any, dirtyFlags: any, setDirtyFlags: any, updateDataValue: any) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
const updated_value = extractEventValue(event);
const name = event.target.name;
updateEntity((prevState) => ({
updateDataValue((prevState) => ({
...prevState,
[name]: updated_value
}));

View File

@@ -1,8 +0,0 @@
export const extractErrorMessage = (error: any, defaultMessage: string) => {
if (error.request) {
return defaultMessage + ' (' + error.request.status + ': ' + error.request.statusText + ')';
} else if (error instanceof Error) {
return defaultMessage + ' (' + error.message + ')';
}
return defaultMessage;
};

View File

@@ -1,5 +1,4 @@
export * from './binding';
export * from './endpoints';
export * from './route';
export * from './submit';
export * from './time';

View File

@@ -1,85 +1,76 @@
import { useCallback, useEffect, useState } from 'react';
import { useRequest, type Method } from 'alova';
import { useState } from 'react';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import { extractErrorMessage } from '.';
import type { AxiosPromise } from 'axios';
import { useI18nContext } from 'i18n/i18n-react';
export interface RestRequestOptions<D> {
read: () => AxiosPromise<D>;
update?: (value: D) => AxiosPromise<D>;
export interface RestRequestOptions2<D> {
read: () => Method<any, any, any, any, any, any, any>;
update: (value: D) => Method<any, any, any, any, any, any, any>;
}
export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
export const useRest = <D>({ read, update }: RestRequestOptions2<D>) => {
const { LL } = useI18nContext();
const [data, setData] = useState<D>();
const [saving, setSaving] = useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<string>();
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
const [origData, setOrigData] = useState<D>();
const [dirtyFlags, setDirtyFlags] = useState<string[]>();
const blocker = useBlocker(dirtyFlags?.length !== 0);
const [dirtyFlags, setDirtyFlags] = useState<string[]>([]);
const blocker = useBlocker(dirtyFlags.length !== 0);
const loadData = useCallback(async () => {
setData(undefined);
const { data: data, send: readData, update: updateData, onComplete: onReadComplete } = useRequest(read());
const {
loading: saving,
send: writeData,
onSuccess: onWriteSuccess
} = useRequest((newData: D) => update(newData), { immediate: false });
const updateDataValue = (new_data: D) => {
updateData({ data: new_data });
};
onWriteSuccess(() => {
toast.success(LL.UPDATED_OF(LL.SETTINGS()));
setDirtyFlags([]);
});
onReadComplete((event) => {
setOrigData(event.data);
});
const loadData = async () => {
setDirtyFlags([]);
setErrorMessage(undefined);
try {
const fetch_data = (await read()).data;
setData(fetch_data);
setOrigData(fetch_data);
} catch (error) {
const message = extractErrorMessage(error, LL.PROBLEM_LOADING());
toast.error(message);
setErrorMessage(message);
}
}, [read, LL]);
await readData().catch((error) => {
toast.error(error.message);
setErrorMessage(error.message);
});
};
const save = useCallback(
async (toSave: D) => {
if (!update) {
const saveData = async () => {
if (!data) {
return;
}
setSaving(true);
setRestartNeeded(false);
setErrorMessage(undefined);
try {
const response = await update(toSave);
setOrigData(response.data);
setData(response.data);
if (response.status === 202) {
await writeData(data).catch((error) => {
if (error.message === 'Reboot required') {
setRestartNeeded(true);
} else {
toast.success(LL.UPDATED_OF(LL.SETTINGS()));
toast.error(error.message);
setErrorMessage(error.message);
}
} catch (error) {
const message = extractErrorMessage(error, LL.PROBLEM_UPDATING());
toast.error(message);
setErrorMessage(message);
} finally {
setSaving(false);
setDirtyFlags([]);
}
},
[update, LL]
);
const saveData = () => data && save(data);
useEffect(() => {
void loadData();
}, [loadData]);
});
};
return {
loadData,
saveData,
saving,
setData,
updateDataValue,
data,
origData,
dirtyFlags,

File diff suppressed because it is too large Load Diff

View File

@@ -16,16 +16,15 @@ ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE
template <typename TReader, typename TStringStorage>
class MsgPackDeserializer {
public:
MsgPackDeserializer(MemoryPool* pool, TReader reader,
TStringStorage stringStorage)
: _pool(pool),
_reader(reader),
_stringStorage(stringStorage),
_foundSomething(false) {}
MsgPackDeserializer(MemoryPool * pool, TReader reader, TStringStorage stringStorage)
: _pool(pool)
, _reader(reader)
, _stringStorage(stringStorage)
, _foundSomething(false) {
}
template <typename TFilter>
DeserializationError parse(VariantData& variant, TFilter filter,
DeserializationOption::NestingLimit nestingLimit) {
DeserializationError parse(VariantData & variant, TFilter filter, DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code err;
err = parseVariant(&variant, filter, nestingLimit);
return _foundSomething ? err : DeserializationError::EmptyInput;
@@ -33,12 +32,10 @@ class MsgPackDeserializer {
private:
template <typename TFilter>
DeserializationError::Code parseVariant(
VariantData* variant, TFilter filter,
DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code parseVariant(VariantData * variant, TFilter filter, DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code err;
uint8_t code = 0; // TODO: why do we need to initialize this variable?
uint8_t code = 0;
err = readByte(code);
if (err)
return err;
@@ -223,7 +220,7 @@ class MsgPackDeserializer {
return DeserializationError::Ok;
}
DeserializationError::Code readByte(uint8_t& value) {
DeserializationError::Code readByte(uint8_t & value) {
int c = _reader.read();
if (c < 0)
return DeserializationError::IncompleteInput;
@@ -231,15 +228,15 @@ class MsgPackDeserializer {
return DeserializationError::Ok;
}
DeserializationError::Code readBytes(uint8_t* p, size_t n) {
if (_reader.readBytes(reinterpret_cast<char*>(p), n) == n)
DeserializationError::Code readBytes(uint8_t * p, size_t n) {
if (_reader.readBytes(reinterpret_cast<char *>(p), n) == n)
return DeserializationError::Ok;
return DeserializationError::IncompleteInput;
}
template <typename T>
DeserializationError::Code readBytes(T& value) {
return readBytes(reinterpret_cast<uint8_t*>(&value), sizeof(value));
DeserializationError::Code readBytes(T & value) {
return readBytes(reinterpret_cast<uint8_t *>(&value), sizeof(value));
}
DeserializationError::Code skipBytes(size_t n) {
@@ -251,7 +248,7 @@ class MsgPackDeserializer {
}
template <typename T>
DeserializationError::Code readInteger(T& value) {
DeserializationError::Code readInteger(T & value) {
DeserializationError::Code err;
err = readBytes(value);
@@ -264,7 +261,7 @@ class MsgPackDeserializer {
}
template <typename T>
DeserializationError::Code readInteger(VariantData* variant) {
DeserializationError::Code readInteger(VariantData * variant) {
DeserializationError::Code err;
T value;
@@ -278,8 +275,7 @@ class MsgPackDeserializer {
}
template <typename T>
typename enable_if<sizeof(T) == 4, DeserializationError::Code>::type
readFloat(VariantData* variant) {
typename enable_if<sizeof(T) == 4, DeserializationError::Code>::type readFloat(VariantData * variant) {
DeserializationError::Code err;
T value;
@@ -294,8 +290,7 @@ class MsgPackDeserializer {
}
template <typename T>
typename enable_if<sizeof(T) == 8, DeserializationError::Code>::type
readDouble(VariantData* variant) {
typename enable_if<sizeof(T) == 8, DeserializationError::Code>::type readDouble(VariantData * variant) {
DeserializationError::Code err;
T value;
@@ -310,12 +305,11 @@ class MsgPackDeserializer {
}
template <typename T>
typename enable_if<sizeof(T) == 4, DeserializationError::Code>::type
readDouble(VariantData* variant) {
typename enable_if<sizeof(T) == 4, DeserializationError::Code>::type readDouble(VariantData * variant) {
DeserializationError::Code err;
uint8_t i[8]; // input is 8 bytes
T value; // output is 4 bytes
uint8_t* o = reinterpret_cast<uint8_t*>(&value);
uint8_t * o = reinterpret_cast<uint8_t *>(&value);
err = readBytes(i, 8);
if (err)
@@ -329,7 +323,7 @@ class MsgPackDeserializer {
}
template <typename T>
DeserializationError::Code readString(VariantData* variant) {
DeserializationError::Code readString(VariantData * variant) {
DeserializationError::Code err;
T size;
@@ -364,7 +358,7 @@ class MsgPackDeserializer {
return skipBytes(size);
}
DeserializationError::Code readString(VariantData* variant, size_t n) {
DeserializationError::Code readString(VariantData * variant, size_t n) {
DeserializationError::Code err;
err = readString(n);
@@ -396,9 +390,7 @@ class MsgPackDeserializer {
}
template <typename TSize, typename TFilter>
DeserializationError::Code readArray(
VariantData* variant, TFilter filter,
DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code readArray(VariantData * variant, TFilter filter, DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code err;
TSize size;
@@ -410,9 +402,7 @@ class MsgPackDeserializer {
}
template <typename TFilter>
DeserializationError::Code readArray(
VariantData* variant, size_t n, TFilter filter,
DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code readArray(VariantData * variant, size_t n, TFilter filter, DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code err;
if (nestingLimit.reached())
@@ -420,7 +410,7 @@ class MsgPackDeserializer {
bool allowArray = filter.allowArray();
CollectionData* array;
CollectionData * array;
if (allowArray) {
ARDUINOJSON_ASSERT(variant != 0);
array = &variant->toArray();
@@ -431,7 +421,7 @@ class MsgPackDeserializer {
TFilter memberFilter = filter[0U];
for (; n; --n) {
VariantData* value;
VariantData * value;
if (memberFilter.allow()) {
ARDUINOJSON_ASSERT(array != 0);
@@ -451,9 +441,7 @@ class MsgPackDeserializer {
}
template <typename TSize, typename TFilter>
DeserializationError::Code readObject(
VariantData* variant, TFilter filter,
DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code readObject(VariantData * variant, TFilter filter, DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code err;
TSize size;
@@ -465,15 +453,13 @@ class MsgPackDeserializer {
}
template <typename TFilter>
DeserializationError::Code readObject(
VariantData* variant, size_t n, TFilter filter,
DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code readObject(VariantData * variant, size_t n, TFilter filter, DeserializationOption::NestingLimit nestingLimit) {
DeserializationError::Code err;
if (nestingLimit.reached())
return DeserializationError::TooDeep;
CollectionData* object;
CollectionData * object;
if (filter.allowObject()) {
ARDUINOJSON_ASSERT(variant != 0);
object = &variant->toObject();
@@ -488,7 +474,7 @@ class MsgPackDeserializer {
JsonString key = _stringStorage.str();
TFilter memberFilter = filter[key.c_str()];
VariantData* member;
VariantData * member;
if (memberFilter.allow()) {
ARDUINOJSON_ASSERT(object != 0);
@@ -497,7 +483,7 @@ class MsgPackDeserializer {
// This MUST be done before adding the slot.
key = _stringStorage.save();
VariantSlot* slot = object->addSlot(_pool);
VariantSlot * slot = object->addSlot(_pool);
if (!slot)
return DeserializationError::NoMemory;
@@ -554,7 +540,7 @@ class MsgPackDeserializer {
return skipBytes(size + 1U);
}
MemoryPool* _pool;
MemoryPool * _pool;
TReader _reader;
TStringStorage _stringStorage;
bool _foundSomething;
@@ -567,7 +553,7 @@ ARDUINOJSON_BEGIN_PUBLIC_NAMESPACE
// Parses a MessagePack input and puts the result in a JsonDocument.
// https://arduinojson.org/v6/api/msgpack/deserializemsgpack/
template <typename... Args>
DeserializationError deserializeMsgPack(JsonDocument& doc, Args&&... args) {
DeserializationError deserializeMsgPack(JsonDocument & doc, Args &&... args) {
using namespace detail;
return deserialize<MsgPackDeserializer>(doc, detail::forward<Args>(args)...);
}
@@ -575,11 +561,9 @@ DeserializationError deserializeMsgPack(JsonDocument& doc, Args&&... args) {
// Parses a MessagePack input and puts the result in a JsonDocument.
// https://arduinojson.org/v6/api/msgpack/deserializemsgpack/
template <typename TChar, typename... Args>
DeserializationError deserializeMsgPack(JsonDocument& doc, TChar* input,
Args&&... args) {
DeserializationError deserializeMsgPack(JsonDocument & doc, TChar * input, Args &&... args) {
using namespace detail;
return deserialize<MsgPackDeserializer>(doc, input,
detail::forward<Args>(args)...);
return deserialize<MsgPackDeserializer>(doc, input, detail::forward<Args>(args)...);
}
ARDUINOJSON_END_PUBLIC_NAMESPACE

View File

@@ -20,13 +20,14 @@ template <typename T, typename Enable = void>
struct Comparer;
template <typename T>
struct Comparer<T, typename enable_if<IsString<T>::value>::type>
: ComparerBase {
T rhs; // TODO: store adapted string?
struct Comparer<T, typename enable_if<IsString<T>::value>::type> : ComparerBase {
T rhs;
explicit Comparer(T value) : rhs(value) {}
explicit Comparer(T value)
: rhs(value) {
}
CompareResult visitString(const char* lhs, size_t n) {
CompareResult visitString(const char * lhs, size_t n) {
int i = stringCompare(adaptString(rhs), adaptString(lhs, n));
if (i < 0)
return COMPARE_RESULT_GREATER;
@@ -45,12 +46,12 @@ struct Comparer<T, typename enable_if<IsString<T>::value>::type>
};
template <typename T>
struct Comparer<T, typename enable_if<is_integral<T>::value ||
is_floating_point<T>::value>::type>
: ComparerBase {
struct Comparer<T, typename enable_if<is_integral<T>::value || is_floating_point<T>::value>::type> : ComparerBase {
T rhs;
explicit Comparer(T value) : rhs(value) {}
explicit Comparer(T value)
: rhs(value) {
}
CompareResult visitFloat(JsonFloat lhs) {
return arithmeticCompare(lhs, rhs);
@@ -77,15 +78,19 @@ struct NullComparer : ComparerBase {
template <>
struct Comparer<decltype(nullptr), void> : NullComparer {
explicit Comparer(decltype(nullptr)) : NullComparer() {}
explicit Comparer(decltype(nullptr))
: NullComparer() {
}
};
struct ArrayComparer : ComparerBase {
const CollectionData* _rhs;
const CollectionData * _rhs;
explicit ArrayComparer(const CollectionData& rhs) : _rhs(&rhs) {}
explicit ArrayComparer(const CollectionData & rhs)
: _rhs(&rhs) {
}
CompareResult visitArray(const CollectionData& lhs) {
CompareResult visitArray(const CollectionData & lhs) {
if (JsonArrayConst(&lhs) == JsonArrayConst(_rhs))
return COMPARE_RESULT_EQUAL;
else
@@ -94,11 +99,13 @@ struct ArrayComparer : ComparerBase {
};
struct ObjectComparer : ComparerBase {
const CollectionData* _rhs;
const CollectionData * _rhs;
explicit ObjectComparer(const CollectionData& rhs) : _rhs(&rhs) {}
explicit ObjectComparer(const CollectionData & rhs)
: _rhs(&rhs) {
}
CompareResult visitObject(const CollectionData& lhs) {
CompareResult visitObject(const CollectionData & lhs) {
if (JsonObjectConst(&lhs) == JsonObjectConst(_rhs))
return COMPARE_RESULT_EQUAL;
else
@@ -107,13 +114,15 @@ struct ObjectComparer : ComparerBase {
};
struct RawComparer : ComparerBase {
const char* _rhsData;
const char * _rhsData;
size_t _rhsSize;
explicit RawComparer(const char* rhsData, size_t rhsSize)
: _rhsData(rhsData), _rhsSize(rhsSize) {}
explicit RawComparer(const char * rhsData, size_t rhsSize)
: _rhsData(rhsData)
, _rhsSize(rhsSize) {
}
CompareResult visitRawJson(const char* lhsData, size_t lhsSize) {
CompareResult visitRawJson(const char * lhsData, size_t lhsSize) {
size_t size = _rhsSize < lhsSize ? _rhsSize : lhsSize;
int n = memcmp(lhsData, _rhsData, size);
if (n < 0)
@@ -126,16 +135,18 @@ struct RawComparer : ComparerBase {
};
struct VariantComparer : ComparerBase {
const VariantData* rhs;
const VariantData * rhs;
explicit VariantComparer(const VariantData* value) : rhs(value) {}
explicit VariantComparer(const VariantData * value)
: rhs(value) {
}
CompareResult visitArray(const CollectionData& lhs) {
CompareResult visitArray(const CollectionData & lhs) {
ArrayComparer comparer(lhs);
return accept(comparer);
}
CompareResult visitObject(const CollectionData& lhs) {
CompareResult visitObject(const CollectionData & lhs) {
ObjectComparer comparer(lhs);
return accept(comparer);
}
@@ -145,12 +156,12 @@ struct VariantComparer : ComparerBase {
return accept(comparer);
}
CompareResult visitString(const char* lhs, size_t) {
Comparer<const char*> comparer(lhs);
CompareResult visitString(const char * lhs, size_t) {
Comparer<const char *> comparer(lhs);
return accept(comparer);
}
CompareResult visitRawJson(const char* lhsData, size_t lhsSize) {
CompareResult visitRawJson(const char * lhsData, size_t lhsSize) {
RawComparer comparer(lhsData, lhsSize);
return accept(comparer);
}
@@ -177,7 +188,7 @@ struct VariantComparer : ComparerBase {
private:
template <typename TComparer>
CompareResult accept(TComparer& comparer) {
CompareResult accept(TComparer & comparer) {
CompareResult reversedResult = variantAccept(rhs, comparer);
switch (reversedResult) {
case COMPARE_RESULT_GREATER:
@@ -191,15 +202,14 @@ struct VariantComparer : ComparerBase {
};
template <typename T>
struct Comparer<T, typename enable_if<is_convertible<
T, ArduinoJson::JsonVariantConst>::value>::type>
: VariantComparer {
explicit Comparer(const T& value)
: VariantComparer(VariantAttorney::getData(value)) {}
struct Comparer<T, typename enable_if<is_convertible<T, ArduinoJson::JsonVariantConst>::value>::type> : VariantComparer {
explicit Comparer(const T & value)
: VariantComparer(VariantAttorney::getData(value)) {
}
};
template <typename T>
CompareResult compare(ArduinoJson::JsonVariantConst lhs, const T& rhs) {
CompareResult compare(ArduinoJson::JsonVariantConst lhs, const T & rhs) {
Comparer<T> comparer(rhs);
return variantAccept(VariantAttorney::getData(lhs), comparer);
}

View File

@@ -23,11 +23,10 @@
#include "cbuf.h"
// Since ESP8266 does not link memchr by default, here's its implementation.
void* memchr(void* ptr, int ch, size_t count)
{
unsigned char* p = static_cast<unsigned char*>(ptr);
while(count--)
if(*p++ == static_cast<unsigned char>(ch))
void * memchr(void * ptr, int ch, size_t count) {
unsigned char * p = static_cast<unsigned char *>(ptr);
while (count--)
if (*p++ == static_cast<unsigned char>(ch))
return --p;
return nullptr;
}
@@ -36,60 +35,102 @@ void* memchr(void* ptr, int ch, size_t count)
/*
* Abstract Response
* */
const char* AsyncWebServerResponse::_responseCodeToString(int code) {
const char * AsyncWebServerResponse::_responseCodeToString(int code) {
switch (code) {
case 100: return ("Continue");
case 101: return ("Switching Protocols");
case 200: return ("OK");
case 201: return ("Created");
case 202: return ("Accepted");
case 203: return ("Non-Authoritative Information");
case 204: return ("No Content");
case 205: return ("Reset Content");
case 206: return ("Partial Content");
case 300: return ("Multiple Choices");
case 301: return ("Moved Permanently");
case 302: return ("Found");
case 303: return ("See Other");
case 304: return ("Not Modified");
case 305: return ("Use Proxy");
case 307: return ("Temporary Redirect");
case 400: return ("Bad Request");
case 401: return ("Unauthorized");
case 402: return ("Payment Required");
case 403: return ("Forbidden");
case 404: return ("Not Found");
case 405: return ("Method Not Allowed");
case 406: return ("Not Acceptable");
case 407: return ("Proxy Authentication Required");
case 408: return ("Request Time-out");
case 409: return ("Conflict");
case 410: return ("Gone");
case 411: return ("Length Required");
case 412: return ("Precondition Failed");
case 413: return ("Request Entity Too Large");
case 414: return ("Request-URI Too Large");
case 415: return ("Unsupported Media Type");
case 416: return ("Requested range not satisfiable");
case 417: return ("Expectation Failed");
case 500: return ("Internal Server Error");
case 501: return ("Not Implemented");
case 502: return ("Bad Gateway");
case 503: return ("Service Unavailable");
case 504: return ("Gateway Time-out");
case 505: return ("HTTP Version not supported");
case 507: return ("Insufficient Storage");
default: return ("");
case 100:
return ("Continue");
case 101:
return ("Switching Protocols");
case 200:
return ("OK");
case 201:
return ("Created");
case 202:
return ("Accepted"); // proddy: used in wifi
case 203:
return ("Non-Authoritative Information");
case 204:
return ("No Content");
case 205:
return ("Reset Content"); // proddy: reboot required
case 206:
return ("Partial Content");
case 300:
return ("Multiple Choices");
case 301:
return ("Moved Permanently");
case 302:
return ("Found");
case 303:
return ("See Other");
case 304:
return ("Not Modified");
case 305:
return ("Use Proxy");
case 307:
return ("Temporary Redirect");
case 400:
return ("Bad Request");
case 401:
return ("Unauthorized");
case 402:
return ("Payment Required");
case 403:
return ("Forbidden");
case 404:
return ("Not Found");
case 405:
return ("Method Not Allowed");
case 406:
return ("Not Acceptable");
case 407:
return ("Proxy Authentication Required");
case 408:
return ("Request Time-out");
case 409:
return ("Conflict");
case 410:
return ("Gone");
case 411:
return ("Length Required");
case 412:
return ("Precondition Failed");
case 413:
return ("Request Entity Too Large");
case 414:
return ("Request-URI Too Large");
case 415:
return ("Unsupported Media Type");
case 416:
return ("Requested range not satisfiable");
case 417:
return ("Expectation Failed");
case 500:
return ("Internal Server Error");
case 501:
return ("Not Implemented");
case 502:
return ("Bad Gateway");
case 503:
return ("Service Unavailable");
case 504:
return ("Gateway Time-out");
case 505:
return ("HTTP Version not supported");
case 507:
return ("Insufficient Storage");
default:
return ("");
}
}
const __FlashStringHelper *AsyncWebServerResponse::responseCodeToString(int code) {
return reinterpret_cast<const __FlashStringHelper*>(responseCodeToString(code));
const __FlashStringHelper * AsyncWebServerResponse::responseCodeToString(int code) {
return reinterpret_cast<const __FlashStringHelper *>(responseCodeToString(code));
}
AsyncWebServerResponse::AsyncWebServerResponse()
: _code(0)
, _headers(LinkedList<AsyncWebHeader *>([](AsyncWebHeader *h){ delete h; }))
, _headers(LinkedList<AsyncWebHeader *>([](AsyncWebHeader * h) { delete h; }))
, _contentType()
, _contentLength(0)
, _sendContentLength(true)
@@ -98,40 +139,39 @@ AsyncWebServerResponse::AsyncWebServerResponse()
, _sentLength(0)
, _ackedLength(0)
, _writtenLength(0)
, _state(RESPONSE_SETUP)
{
for(auto header: DefaultHeaders::Instance()) {
, _state(RESPONSE_SETUP) {
for (auto header : DefaultHeaders::Instance()) {
_headers.add(new AsyncWebHeader(header->name(), header->value()));
}
}
AsyncWebServerResponse::~AsyncWebServerResponse(){
AsyncWebServerResponse::~AsyncWebServerResponse() {
_headers.free();
}
void AsyncWebServerResponse::setCode(int code){
if(_state == RESPONSE_SETUP)
void AsyncWebServerResponse::setCode(int code) {
if (_state == RESPONSE_SETUP)
_code = code;
}
void AsyncWebServerResponse::setContentLength(size_t len){
if(_state == RESPONSE_SETUP)
void AsyncWebServerResponse::setContentLength(size_t len) {
if (_state == RESPONSE_SETUP)
_contentLength = len;
}
void AsyncWebServerResponse::setContentType(const String& type){
if(_state == RESPONSE_SETUP)
void AsyncWebServerResponse::setContentType(const String & type) {
if (_state == RESPONSE_SETUP)
_contentType = type;
}
void AsyncWebServerResponse::addHeader(const String& name, const String& value){
void AsyncWebServerResponse::addHeader(const String & name, const String & value) {
_headers.add(new AsyncWebHeader(name, value));
}
String AsyncWebServerResponse::_assembleHead(uint8_t version){
if(version){
String AsyncWebServerResponse::_assembleHead(uint8_t version) {
if (version) {
addHeader(F("Accept-Ranges"), F("none"));
if(_chunked)
if (_chunked)
addHeader(F("Transfer-Encoding"), F("chunked"));
}
String out = String();
@@ -141,16 +181,16 @@ String AsyncWebServerResponse::_assembleHead(uint8_t version){
snprintf_P(buf, bufSize, PSTR("HTTP/1.%d %d %s\r\n"), version, _code, _responseCodeToString(_code));
out.concat(buf);
if(_sendContentLength) {
if (_sendContentLength) {
snprintf_P(buf, bufSize, PSTR("Content-Length: %d\r\n"), _contentLength);
out.concat(buf);
}
if(_contentType.length()) {
if (_contentType.length()) {
snprintf_P(buf, bufSize, PSTR("Content-Type: %s\r\n"), _contentType.c_str());
out.concat(buf);
}
for(const auto& header: _headers){
for (const auto & header : _headers) {
snprintf_P(buf, bufSize, PSTR("%s: %s\r\n"), header->name().c_str(), header->value().c_str());
out.concat(buf);
}
@@ -161,48 +201,64 @@ String AsyncWebServerResponse::_assembleHead(uint8_t version){
return out;
}
bool AsyncWebServerResponse::_started() const { return _state > RESPONSE_SETUP; }
bool AsyncWebServerResponse::_finished() const { return _state > RESPONSE_WAIT_ACK; }
bool AsyncWebServerResponse::_failed() const { return _state == RESPONSE_FAILED; }
bool AsyncWebServerResponse::_sourceValid() const { return false; }
void AsyncWebServerResponse::_respond(AsyncWebServerRequest *request){ _state = RESPONSE_END; request->client()->close(); }
size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){ (void)request; (void)len; (void)time; return 0; }
bool AsyncWebServerResponse::_started() const {
return _state > RESPONSE_SETUP;
}
bool AsyncWebServerResponse::_finished() const {
return _state > RESPONSE_WAIT_ACK;
}
bool AsyncWebServerResponse::_failed() const {
return _state == RESPONSE_FAILED;
}
bool AsyncWebServerResponse::_sourceValid() const {
return false;
}
void AsyncWebServerResponse::_respond(AsyncWebServerRequest * request) {
_state = RESPONSE_END;
request->client()->close();
}
size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest * request, size_t len, uint32_t time) {
(void)request;
(void)len;
(void)time;
return 0;
}
/*
* String/Code Response
* */
AsyncBasicResponse::AsyncBasicResponse(int code, const String& contentType, const String& content){
AsyncBasicResponse::AsyncBasicResponse(int code, const String & contentType, const String & content) {
_code = code;
_content = content;
_contentType = contentType;
if(_content.length()){
if (_content.length()) {
_contentLength = _content.length();
if(!_contentType.length())
if (!_contentType.length())
_contentType = F("text/plain");
}
addHeader(F("Connection"), F("close"));
}
void AsyncBasicResponse::_respond(AsyncWebServerRequest *request){
void AsyncBasicResponse::_respond(AsyncWebServerRequest * request) {
_state = RESPONSE_HEADERS;
String out = _assembleHead(request->version());
size_t outLen = out.length();
size_t space = request->client()->space();
if(!_contentLength && space >= outLen){
if (!_contentLength && space >= outLen) {
_writtenLength += request->client()->write(out.c_str(), outLen);
_state = RESPONSE_WAIT_ACK;
} else if(_contentLength && space >= outLen + _contentLength){
} else if (_contentLength && space >= outLen + _contentLength) {
out += _content;
outLen += _contentLength;
_writtenLength += request->client()->write(out.c_str(), outLen);
_state = RESPONSE_WAIT_ACK;
} else if(space && space < outLen){
} else if (space && space < outLen) {
String partial = out.substring(0, space);
_content = out.substring(space) + _content;
_contentLength += outLen - space;
_writtenLength += request->client()->write(partial.c_str(), partial.length());
_state = RESPONSE_CONTENT;
} else if(space > outLen && space < (outLen + _contentLength)){
} else if (space > outLen && space < (outLen + _contentLength)) {
size_t shift = space - outLen;
outLen += shift;
_sentLength += shift;
@@ -217,14 +273,14 @@ void AsyncBasicResponse::_respond(AsyncWebServerRequest *request){
}
}
size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){
size_t AsyncBasicResponse::_ack(AsyncWebServerRequest * request, size_t len, uint32_t time) {
(void)time;
_ackedLength += len;
if(_state == RESPONSE_CONTENT){
if (_state == RESPONSE_CONTENT) {
size_t available = _contentLength - _sentLength;
size_t space = request->client()->space();
//we can fit in this packet
if(space > available){
if (space > available) {
_writtenLength += request->client()->write(_content.c_str(), available);
_content = String();
_state = RESPONSE_WAIT_ACK;
@@ -236,8 +292,8 @@ size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint
_sentLength += space;
_writtenLength += request->client()->write(out.c_str(), space);
return space;
} else if(_state == RESPONSE_WAIT_ACK){
if(_ackedLength >= _writtenLength){
} else if (_state == RESPONSE_WAIT_ACK) {
if (_ackedLength >= _writtenLength) {
_state = RESPONSE_END;
}
}
@@ -249,26 +305,26 @@ size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint
* Abstract Response
* */
AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback): _callback(callback)
{
AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback)
: _callback(callback) {
// In case of template processing, we're unable to determine real response size
if(callback) {
if (callback) {
_contentLength = 0;
_sendContentLength = false;
_chunked = true;
}
}
void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request){
void AsyncAbstractResponse::_respond(AsyncWebServerRequest * request) {
addHeader(F("Connection"), F("close"));
_head = _assembleHead(request->version());
_state = RESPONSE_HEADERS;
_ack(request, 0, 0);
}
size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time){
size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest * request, size_t len, uint32_t time) {
(void)time;
if(!_sourceValid()){
if (!_sourceValid()) {
_state = RESPONSE_FAILED;
request->client()->close();
return 0;
@@ -277,8 +333,8 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
size_t space = request->client()->space();
size_t headLen = _head.length();
if(_state == RESPONSE_HEADERS){
if(space >= headLen){
if (_state == RESPONSE_HEADERS) {
if (space >= headLen) {
_state = RESPONSE_CONTENT;
space -= headLen;
} else {
@@ -289,64 +345,65 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
}
}
if(_state == RESPONSE_CONTENT){
if (_state == RESPONSE_CONTENT) {
size_t outLen;
if(_chunked){
if(space <= 8){
if (_chunked) {
if (space <= 8) {
return 0;
}
outLen = space;
} else if(!_sendContentLength){
} else if (!_sendContentLength) {
outLen = space;
} else {
outLen = ((_contentLength - _sentLength) > space)?space:(_contentLength - _sentLength);
outLen = ((_contentLength - _sentLength) > space) ? space : (_contentLength - _sentLength);
}
uint8_t *buf = (uint8_t *)malloc(outLen+headLen);
uint8_t * buf = (uint8_t *)malloc(outLen + headLen);
if (!buf) {
// os_printf("_ack malloc %d failed\n", outLen+headLen);
return 0;
}
if(headLen){
if (headLen) {
memcpy(buf, _head.c_str(), _head.length());
}
size_t readLen = 0;
if(_chunked){
if (_chunked) {
// HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added.
// See RFC2616 sections 2, 3.6.1.
readLen = _fillBufferAndProcessTemplates(buf+headLen+6, outLen - 8);
if(readLen == RESPONSE_TRY_AGAIN){
readLen = _fillBufferAndProcessTemplates(buf + headLen + 6, outLen - 8);
if (readLen == RESPONSE_TRY_AGAIN) {
free(buf);
return 0;
}
outLen = snprintf_P((char*)buf+headLen, sizeof(buf)-headLen-2, PSTR("%x"), readLen) + headLen;
while(outLen < headLen + 4) buf[outLen++] = ' ';
outLen = snprintf_P((char *)buf + headLen, sizeof(buf) - headLen - 2, PSTR("%x"), readLen) + headLen;
while (outLen < headLen + 4)
buf[outLen++] = ' ';
buf[outLen++] = '\r';
buf[outLen++] = '\n';
outLen += readLen;
buf[outLen++] = '\r';
buf[outLen++] = '\n';
} else {
readLen = _fillBufferAndProcessTemplates(buf+headLen, outLen);
if(readLen == RESPONSE_TRY_AGAIN){
readLen = _fillBufferAndProcessTemplates(buf + headLen, outLen);
if (readLen == RESPONSE_TRY_AGAIN) {
free(buf);
return 0;
}
outLen = readLen + headLen;
}
if(headLen){
if (headLen) {
_head = String();
}
if(outLen){
_writtenLength += request->client()->write((const char*)buf, outLen);
if (outLen) {
_writtenLength += request->client()->write((const char *)buf, outLen);
}
if(_chunked){
if (_chunked) {
_sentLength += readLen;
} else {
_sentLength += outLen - headLen;
@@ -354,26 +411,25 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
free(buf);
if((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)){
if ((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)) {
_state = RESPONSE_WAIT_ACK;
}
return outLen;
} else if(_state == RESPONSE_WAIT_ACK){
if(!_sendContentLength || _ackedLength >= _writtenLength){
} else if (_state == RESPONSE_WAIT_ACK) {
if (!_sendContentLength || _ackedLength >= _writtenLength) {
_state = RESPONSE_END;
if(!_chunked && !_sendContentLength)
if (!_chunked && !_sendContentLength)
request->client()->close(true);
}
}
return 0;
}
size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t* data, const size_t len)
{
size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t * data, const size_t len) {
// If we have something in cache, copy it to buffer
const size_t readFromCache = std::min(len, _cache.size());
if(readFromCache) {
if (readFromCache) {
memcpy(data, _cache.data(), readFromCache);
_cache.erase(_cache.begin(), _cache.begin() + readFromCache);
}
@@ -383,87 +439,86 @@ size_t AsyncAbstractResponse::_readDataFromCacheOrContent(uint8_t* data, const s
return readFromCache + readFromContent;
}
size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t* data, size_t len)
{
if(!_callback)
size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t * data, size_t len) {
if (!_callback)
return _fillBuffer(data, len);
const size_t originalLen = len;
len = _readDataFromCacheOrContent(data, len);
// Now we've read 'len' bytes, either from cache or from file
// Search for template placeholders
uint8_t* pTemplateStart = data;
while((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t*)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) { // data[0] ... data[len - 1]
uint8_t* pTemplateEnd = (pTemplateStart < &data[len - 1]) ? (uint8_t*)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr;
uint8_t * pTemplateStart = data;
while ((pTemplateStart < &data[len])
&& (pTemplateStart = (uint8_t *)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) { // data[0] ... data[len - 1]
uint8_t * pTemplateEnd =
(pTemplateStart < &data[len - 1]) ? (uint8_t *)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr;
// temporary buffer to hold parameter name
uint8_t buf[TEMPLATE_PARAM_NAME_LENGTH + 1];
String paramName;
// If closing placeholder is found:
if(pTemplateEnd) {
if (pTemplateEnd) {
// prepare argument to callback
const size_t paramNameLength = std::min((size_t)sizeof(buf) - 1, (size_t)(pTemplateEnd - pTemplateStart - 1));
if(paramNameLength) {
if (paramNameLength) {
memcpy(buf, pTemplateStart + 1, paramNameLength);
buf[paramNameLength] = 0;
paramName = String(reinterpret_cast<char*>(buf));
paramName = String(reinterpret_cast<char *>(buf));
} else { // double percent sign encountered, this is single percent sign escaped.
// remove the 2nd percent sign
memmove(pTemplateEnd, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1);
len += _readDataFromCacheOrContent(&data[len - 1], 1) - 1;
++pTemplateStart;
}
} else if(&data[len - 1] - pTemplateStart + 1 < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data
} else if (&data[len - 1] - pTemplateStart + 1 < TEMPLATE_PARAM_NAME_LENGTH + 2) { // closing placeholder not found, check if it's in the remaining file data
memcpy(buf, pTemplateStart + 1, &data[len - 1] - pTemplateStart);
const size_t readFromCacheOrContent = _readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1));
if(readFromCacheOrContent) {
pTemplateEnd = (uint8_t*)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent);
if(pTemplateEnd) {
const size_t readFromCacheOrContent =
_readDataFromCacheOrContent(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PARAM_NAME_LENGTH + 2 - (&data[len - 1] - pTemplateStart + 1));
if (readFromCacheOrContent) {
pTemplateEnd = (uint8_t *)memchr(buf + (&data[len - 1] - pTemplateStart), TEMPLATE_PLACEHOLDER, readFromCacheOrContent);
if (pTemplateEnd) {
// prepare argument to callback
*pTemplateEnd = 0;
paramName = String(reinterpret_cast<char*>(buf));
paramName = String(reinterpret_cast<char *>(buf));
// Copy remaining read-ahead data into cache
_cache.insert(_cache.begin(), pTemplateEnd + 1, buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent);
pTemplateEnd = &data[len - 1];
}
else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position
} else // closing placeholder not found in file data, store found percent symbol as is and advance to the next position
{
// but first, store read file data in cache
_cache.insert(_cache.begin(), buf + (&data[len - 1] - pTemplateStart), buf + (&data[len - 1] - pTemplateStart) + readFromCacheOrContent);
++pTemplateStart;
}
}
else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position
} else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position
++pTemplateStart;
}
else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position
} else // closing placeholder not found in content data, store found percent symbol as is and advance to the next position
++pTemplateStart;
if(paramName.length()) {
if (paramName.length()) {
// call callback and replace with result.
// Everything in range [pTemplateStart, pTemplateEnd] can be safely replaced with parameter value.
// Data after pTemplateEnd may need to be moved.
// The first byte of data after placeholder is located at pTemplateEnd + 1.
// It should be located at pTemplateStart + numBytesCopied (to begin right after inserted parameter value).
const String paramValue(_callback(paramName));
const char* pvstr = paramValue.c_str();
const char * pvstr = paramValue.c_str();
const unsigned int pvlen = paramValue.length();
const size_t numBytesCopied = std::min(pvlen, static_cast<unsigned int>(&data[originalLen - 1] - pTemplateStart + 1));
// make room for param value
// 1. move extra data to cache if parameter value is longer than placeholder AND if there is no room to store
if((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) {
if ((pTemplateEnd + 1 < pTemplateStart + numBytesCopied) && (originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1) < len)) {
_cache.insert(_cache.begin(), &data[originalLen - (pTemplateStart + numBytesCopied - pTemplateEnd - 1)], &data[len]);
//2. parameter value is longer than placeholder text, push the data after placeholder which not saved into cache further to the end
memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[originalLen] - pTemplateStart - numBytesCopied);
len = originalLen; // fix issue with truncated data, not sure if it has any side effects
} else if(pTemplateEnd + 1 != pTemplateStart + numBytesCopied)
} else if (pTemplateEnd + 1 != pTemplateStart + numBytesCopied)
//2. Either parameter value is shorter than placeholder text OR there is enough free space in buffer to fit.
// Move the entire data after the placeholder
memmove(pTemplateStart + numBytesCopied, pTemplateEnd + 1, &data[len] - pTemplateEnd - 1);
// 3. replace placeholder with actual value
memcpy(pTemplateStart, pvstr, numBytesCopied);
// If result is longer than buffer, copy the remainder into cache (this could happen only if placeholder text itself did not fit entirely in buffer)
if(numBytesCopied < pvlen) {
if (numBytesCopied < pvlen) {
_cache.insert(_cache.begin(), pvstr + numBytesCopied, pvstr + pvlen);
} else if(pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text...
} else if (pTemplateStart + numBytesCopied < pTemplateEnd + 1) { // result is copied fully; if result is shorter than placeholder text...
// there is some free room, fill it from cache
const size_t roomFreed = pTemplateEnd + 1 - pTemplateStart - numBytesCopied;
const size_t totalFreeRoom = originalLen - len + roomFreed;
@@ -482,43 +537,63 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t* data, size
* File Response
* */
AsyncFileResponse::~AsyncFileResponse(){
if(_content)
AsyncFileResponse::~AsyncFileResponse() {
if (_content)
_content.close();
}
void AsyncFileResponse::_setContentType(const String& path){
void AsyncFileResponse::_setContentType(const String & path) {
#if HAVE_EXTERN_GET_CONTENT_TYPE_FUNCTION
extern const __FlashStringHelper *getContentType(const String &path);
extern const __FlashStringHelper * getContentType(const String & path);
_contentType = getContentType(path);
#else
if (path.endsWith(F(".html"))) _contentType = F("text/html");
else if (path.endsWith(F(".htm"))) _contentType = F("text/html");
else if (path.endsWith(F(".css"))) _contentType = F("text/css");
else if (path.endsWith(F(".json"))) _contentType = F("application/json");
else if (path.endsWith(F(".js"))) _contentType = F("application/javascript");
else if (path.endsWith(F(".png"))) _contentType = F("image/png");
else if (path.endsWith(F(".gif"))) _contentType = F("image/gif");
else if (path.endsWith(F(".jpg"))) _contentType = F("image/jpeg");
else if (path.endsWith(F(".ico"))) _contentType = F("image/x-icon");
else if (path.endsWith(F(".svg"))) _contentType = F("image/svg+xml");
else if (path.endsWith(F(".eot"))) _contentType = F("font/eot");
else if (path.endsWith(F(".woff"))) _contentType = F("font/woff");
else if (path.endsWith(F(".woff2"))) _contentType = F("font/woff2");
else if (path.endsWith(F(".ttf"))) _contentType = F("font/ttf");
else if (path.endsWith(F(".xml"))) _contentType = F("text/xml");
else if (path.endsWith(F(".pdf"))) _contentType = F("application/pdf");
else if (path.endsWith(F(".zip"))) _contentType = F("application/zip");
else if(path.endsWith(F(".gz"))) _contentType = F("application/x-gzip");
else _contentType = F("text/plain");
if (path.endsWith(F(".html")))
_contentType = F("text/html");
else if (path.endsWith(F(".htm")))
_contentType = F("text/html");
else if (path.endsWith(F(".css")))
_contentType = F("text/css");
else if (path.endsWith(F(".json")))
_contentType = F("application/json");
else if (path.endsWith(F(".js")))
_contentType = F("application/javascript");
else if (path.endsWith(F(".png")))
_contentType = F("image/png");
else if (path.endsWith(F(".gif")))
_contentType = F("image/gif");
else if (path.endsWith(F(".jpg")))
_contentType = F("image/jpeg");
else if (path.endsWith(F(".ico")))
_contentType = F("image/x-icon");
else if (path.endsWith(F(".svg")))
_contentType = F("image/svg+xml");
else if (path.endsWith(F(".eot")))
_contentType = F("font/eot");
else if (path.endsWith(F(".woff")))
_contentType = F("font/woff");
else if (path.endsWith(F(".woff2")))
_contentType = F("font/woff2");
else if (path.endsWith(F(".ttf")))
_contentType = F("font/ttf");
else if (path.endsWith(F(".xml")))
_contentType = F("text/xml");
else if (path.endsWith(F(".pdf")))
_contentType = F("application/pdf");
else if (path.endsWith(F(".zip")))
_contentType = F("application/zip");
else if (path.endsWith(F(".gz")))
_contentType = F("application/x-gzip");
else
_contentType = F("text/plain");
#endif
}
AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){
AsyncFileResponse::AsyncFileResponse(FS & fs, const String & path, const String & contentType, bool download, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) {
_code = 200;
_path = path;
if(!download && !fs.exists(_path) && fs.exists(_path + F(".gz"))){
if (!download && !fs.exists(_path) && fs.exists(_path + F(".gz"))) {
_path = _path + F(".gz");
addHeader(F("Content-Encoding"), F("gzip"));
_callback = nullptr; // Unable to process zipped templates
@@ -529,30 +604,31 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& c
_content = fs.open(_path, fs::FileOpenMode::read);
_contentLength = _content.size();
if(contentType.length() == 0)
if (contentType.length() == 0)
_setContentType(path);
else
_contentType = contentType;
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26+path.length()-filenameStart];
char* filename = (char*)path.c_str() + filenameStart;
char buf[26 + path.length() - filenameStart];
char * filename = (char *)path.c_str() + filenameStart;
if(download) {
if (download) {
// set filename and force download
snprintf_P(buf, sizeof (buf), PSTR("attachment; filename=\"%s\""), filename);
snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename);
} else {
// set filename and force rendering
snprintf_P(buf, sizeof (buf), PSTR("inline; filename=\"%s\""), filename);
snprintf_P(buf, sizeof(buf), PSTR("inline; filename=\"%s\""), filename);
}
addHeader(F("Content-Disposition"), buf);
}
AsyncFileResponse::AsyncFileResponse(File content, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){
AsyncFileResponse::AsyncFileResponse(File content, const String & path, const String & contentType, bool download, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) {
_code = 200;
_path = path;
if(!download && String(content.name()).endsWith(F(".gz")) && !path.endsWith(F(".gz"))){
if (!download && String(content.name()).endsWith(F(".gz")) && !path.endsWith(F(".gz"))) {
addHeader(F("Content-Encoding"), F("gzip"));
_callback = nullptr; // Unable to process gzipped templates
_sendContentLength = true;
@@ -562,24 +638,24 @@ AsyncFileResponse::AsyncFileResponse(File content, const String& path, const Str
_content = content;
_contentLength = _content.size();
if(contentType.length() == 0)
if (contentType.length() == 0)
_setContentType(path);
else
_contentType = contentType;
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26+path.length()-filenameStart];
char* filename = (char*)path.c_str() + filenameStart;
char buf[26 + path.length() - filenameStart];
char * filename = (char *)path.c_str() + filenameStart;
if(download) {
snprintf_P(buf, sizeof (buf), PSTR("attachment; filename=\"%s\""), filename);
if (download) {
snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename);
} else {
snprintf_P(buf, sizeof (buf), PSTR("inline; filename=\"%s\""), filename);
snprintf_P(buf, sizeof(buf), PSTR("inline; filename=\"%s\""), filename);
}
addHeader(F("Content-Disposition"), buf);
}
size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len){
size_t AsyncFileResponse::_fillBuffer(uint8_t * data, size_t len) {
return _content.read(data, len);
}
@@ -587,18 +663,19 @@ size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len){
* Stream Response
* */
AsyncStreamResponse::AsyncStreamResponse(Stream &stream, const String& contentType, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) {
AsyncStreamResponse::AsyncStreamResponse(Stream & stream, const String & contentType, size_t len, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) {
_code = 200;
_content = &stream;
_contentLength = len;
_contentType = contentType;
}
size_t AsyncStreamResponse::_fillBuffer(uint8_t *data, size_t len){
size_t AsyncStreamResponse::_fillBuffer(uint8_t * data, size_t len) {
size_t available = _content->available();
size_t outLen = (available > len)?len:available;
size_t outLen = (available > len) ? len : available;
size_t i;
for(i=0;i<outLen;i++)
for (i = 0; i < outLen; i++)
data[i] = _content->read();
return outLen;
}
@@ -607,19 +684,20 @@ size_t AsyncStreamResponse::_fillBuffer(uint8_t *data, size_t len){
* Callback Response
* */
AsyncCallbackResponse::AsyncCallbackResponse(const String& contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback): AsyncAbstractResponse(templateCallback) {
AsyncCallbackResponse::AsyncCallbackResponse(const String & contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback)
: AsyncAbstractResponse(templateCallback) {
_code = 200;
_content = callback;
_contentLength = len;
if(!len)
if (!len)
_sendContentLength = false;
_contentType = contentType;
_filledLength = 0;
}
size_t AsyncCallbackResponse::_fillBuffer(uint8_t *data, size_t len){
size_t AsyncCallbackResponse::_fillBuffer(uint8_t * data, size_t len) {
size_t ret = _content(data, len, _filledLength);
if(ret != RESPONSE_TRY_AGAIN){
if (ret != RESPONSE_TRY_AGAIN) {
_filledLength += ret;
}
return ret;
@@ -629,7 +707,8 @@ size_t AsyncCallbackResponse::_fillBuffer(uint8_t *data, size_t len){
* Chunked Response
* */
AsyncChunkedResponse::AsyncChunkedResponse(const String& contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback): AsyncAbstractResponse(processorCallback) {
AsyncChunkedResponse::AsyncChunkedResponse(const String & contentType, AwsResponseFiller callback, AwsTemplateProcessor processorCallback)
: AsyncAbstractResponse(processorCallback) {
_code = 200;
_content = callback;
_contentLength = 0;
@@ -639,9 +718,9 @@ AsyncChunkedResponse::AsyncChunkedResponse(const String& contentType, AwsRespons
_filledLength = 0;
}
size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len){
size_t AsyncChunkedResponse::_fillBuffer(uint8_t * data, size_t len) {
size_t ret = _content(data, len, _filledLength);
if(ret != RESPONSE_TRY_AGAIN){
if (ret != RESPONSE_TRY_AGAIN) {
_filledLength += ret;
}
return ret;
@@ -651,7 +730,8 @@ size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len){
* Progmem Response
* */
AsyncProgmemResponse::AsyncProgmemResponse(int code, const String& contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback): AsyncAbstractResponse(callback) {
AsyncProgmemResponse::AsyncProgmemResponse(int code, const String & contentType, const uint8_t * content, size_t len, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) {
_code = code;
_content = content;
_contentType = contentType;
@@ -659,7 +739,7 @@ AsyncProgmemResponse::AsyncProgmemResponse(int code, const String& contentType,
_readLength = 0;
}
size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len){
size_t AsyncProgmemResponse::_fillBuffer(uint8_t * data, size_t len) {
size_t left = _contentLength - _readLength;
if (left > len) {
memcpy_P(data, _content + _readLength, len);
@@ -676,34 +756,34 @@ size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len){
* Response Stream (You can print/write/printf to it, up to the contentLen bytes)
* */
AsyncResponseStream::AsyncResponseStream(const String& contentType, size_t bufferSize){
AsyncResponseStream::AsyncResponseStream(const String & contentType, size_t bufferSize) {
_code = 200;
_contentLength = 0;
_contentType = contentType;
_content = new cbuf(bufferSize);
}
AsyncResponseStream::~AsyncResponseStream(){
AsyncResponseStream::~AsyncResponseStream() {
delete _content;
}
size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen){
return _content->read((char*)buf, maxLen);
size_t AsyncResponseStream::_fillBuffer(uint8_t * buf, size_t maxLen) {
return _content->read((char *)buf, maxLen);
}
size_t AsyncResponseStream::write(const uint8_t *data, size_t len){
if(_started())
size_t AsyncResponseStream::write(const uint8_t * data, size_t len) {
if (_started())
return 0;
if(len > _content->room()){
if (len > _content->room()) {
size_t needed = len - _content->room();
_content->resizeAdd(needed);
}
size_t written = _content->write((const char*)data, len);
size_t written = _content->write((const char *)data, len);
_contentLength += written;
return written;
}
size_t AsyncResponseStream::write(uint8_t data){
size_t AsyncResponseStream::write(uint8_t data) {
return write(&data, 1);
}

View File

@@ -115,7 +115,7 @@ class HttpPostEndpoint {
response->setLength();
if (outcome == StateUpdateResult::CHANGED_RESTART) {
response->setCode(202); // added by proddy
response->setCode(205); // added by proddy, reboot required
}
request->send(response);
}

View File

@@ -42,7 +42,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
return;
} else {
md5[0] = '\0';
return; // not support file type
return; // unsupported file type
}
if (is_firmware) {
@@ -115,7 +115,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
}
// check if it was a firmware upgrade
// if no error, send the success response
// if no error, send the success response as a JSON
if (is_firmware && !request->_tempObject) {
request->onDisconnect(RestartService::restartNow);
AsyncWebServerResponse * response = request->beginResponse(200);
@@ -123,8 +123,13 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) {
return;
}
if (strlen(md5) == 32) {
AsyncWebServerResponse * response = request->beginResponse(201, "text/plain", md5); // created
auto * response = new AsyncJsonResponse(false, 256);
JsonObject root = response->getRoot();
root["md5"] = md5;
response->setLength();
request->send(response);
// AsyncWebServerResponse * response = request->beginResponse(201, "text/plain", md5); // created
// request->send(response);
return;
}

View File

@@ -12,11 +12,12 @@ WiFiScanner::WiFiScanner(AsyncWebServer * server, SecurityManager * securityMana
};
void WiFiScanner::scanNetworks(AsyncWebServerRequest * request) {
request->send(202); // special code to indicate scan in progress
if (WiFi.scanComplete() != -1) {
WiFi.scanDelete();
WiFi.scanNetworks(true);
}
request->send(202);
}
void WiFiScanner::listNetworks(AsyncWebServerRequest * request) {
@@ -36,7 +37,7 @@ void WiFiScanner::listNetworks(AsyncWebServerRequest * request) {
response->setLength();
request->send(response);
} else if (numNetworks == -1) {
request->send(202);
request->send(202); // special code to indicate scan in progress
} else {
scanNetworks(request);
}

View File

@@ -12,6 +12,7 @@
"@msgpack/msgpack": "^2.8.0",
"compression": "^1.7.4",
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"nodemon": "^2.0.22"
},
"packageManager": "yarn@3.4.1"

View File

@@ -2,19 +2,51 @@ const express = require('express');
const compression = require('compression');
const path = require('path');
const msgpack = require('@msgpack/msgpack');
const multer = require('multer'); // https://www.npmjs.com/package/multer#readme
// REST API
const rest_server = express();
const port = 3080;
rest_server.use(compression());
rest_server.use(express.static(path.join(__dirname, '../interface/build')));
rest_server.use(express.json());
// rest_server.use(express.static(path.join(__dirname, '../interface/build')));
// rest_server.use(express.json());
// uploads
const upload = multer({ dest: '../mock-api/uploads' });
function progress_middleware(req, res, next) {
let progress = 0;
const file_size = req.headers['content-length'];
// set event listener
req.on('data', async (chunk) => {
progress += chunk.length;
const percentage = (progress / file_size) * 100;
console.log(`Progress: ${Math.round(percentage)}%`);
// await delay(1000); // slow it down
delay_blocking(200); // slow it down
});
next(); // invoke next middleware which is multer
}
// delays
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
function delay_blocking(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if (new Date().getTime() - start > milliseconds) {
break;
}
}
}
// endpoints
const API_ENDPOINT_ROOT = '/api/';
const REST_ENDPOINT_ROOT = '/rest/';
// network poll
let countWifiScanPoll = 0;
// LOG
const LOG_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'logSettings';
log_settings = {
@@ -24,7 +56,7 @@ log_settings = {
};
const FETCH_LOG_ENDPOINT = REST_ENDPOINT_ROOT + 'fetchLog';
const fetch_log = {
let fetch_log = {
events: [
{
t: '000+00:00:00.001',
@@ -265,7 +297,8 @@ const FACTORY_RESET_ENDPOINT = REST_ENDPOINT_ROOT + 'factoryReset';
const UPLOAD_FILE_ENDPOINT = REST_ENDPOINT_ROOT + 'uploadFile';
const SIGN_IN_ENDPOINT = REST_ENDPOINT_ROOT + 'signIn';
const GENERATE_TOKEN_ENDPOINT = REST_ENDPOINT_ROOT + 'generateToken';
const system_status = {
let system_status = {
emsesp_version: '3.6.0-demo',
esp_platform: 'ESP32',
max_alloc_heap: 89,
@@ -393,6 +426,7 @@ const emsesp_coredata = {
tn: 'Boiler',
b: 'Nefit',
n: 'GBx72/Trendline/Cerapur/Greenstar Si/27i',
// n: 'Enviline/Compress 6000AW/Hybrid 3000-7000iAW/SupraEco/Geo 5xx/WLW196i/WSW196i',
d: 8,
p: 123,
v: '06.01'
@@ -476,7 +510,7 @@ const emsesp_sensordata = {
{ id: '28-233D-9497-0C03', n: 'Dallas 1', t: 25.7, o: 1.2, u: 1 },
{ id: '28-243D-7437-1E3A', n: 'Dallas 2 outside', t: 26.1, o: 0, u: 1 },
{ id: '28-243E-7437-1E3B', n: 'Zolder', t: 27.1, o: 0, u: 16 },
{ id: '28-183D-1892-0C33', n: 'Roof', o: 2, u: 1 }
{ id: '28-183D-1892-0C33', n: 'Roof', o: 2, u: 1 } // no temperature
],
// as: [],
as: [
@@ -508,95 +542,15 @@ const status = {
};
// Dashboard data
// 7 - Nefit Trendline boiler
// 1 - RC35 thermo
// 2 - RC20 thermo
// 3 - Buderus GB125 boiler
// 4 - RC100 themo
// 5 - Mixer MM10
// 6 - Solar SM10
// 7 - Nefit Trendline boiler
// 99 - Custom
const emsesp_devicedata_7 = {
data: [
{ v: '', u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] },
{ v: 'off', u: 0, id: '08heating active' },
{ v: 'off', u: 0, id: '04tapwater active' },
{ v: 5, u: 1, id: '04selected flow temperature', c: 'selflowtemp' },
{ v: 0, u: 3, id: '0Eburner selected max power', c: 'selburnpow' },
{ v: 0, u: 3, id: '00heating pump modulation' },
{ v: 53.4, u: 1, id: '00current flow temperature' },
{ v: 52.7, u: 1, id: '00return temperature' },
{ v: 1.3, u: 10, id: '00system pressure' },
{ v: 54.9, u: 1, id: '00actual boiler temperature' },
{ v: 'off', u: 0, id: '00gas' },
{ v: 'off', u: 0, id: '00gas stage 2' },
{ v: 0, u: 9, id: '00flame current' },
{ v: 'off', u: 0, id: '00heating pump' },
{ v: 'off', u: 0, id: '00fan' },
{ v: 'off', u: 0, id: '00ignition' },
{ v: 'off', u: 0, id: '00oil preheating' },
{ v: 'on', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] },
{ v: 80, u: 1, id: '00heating temperature', c: 'heatingtemp' },
{ v: 70, u: 3, id: '00burner pump max power', c: 'pumpmodmax' },
{ v: 30, u: 3, id: '00burner pump min power', c: 'pumpmodmin' },
{ v: 1, u: 8, id: '00pump delay', c: 'pumpdelay' },
{ v: 10, u: 8, id: '00burner min period', c: 'burnminperiod' },
{ v: 0, u: 3, id: '00burner min power', c: 'burnminpower' },
{ v: 50, u: 3, id: '00burner max power', c: 'burnmaxpower' },
{ v: -6, u: 2, id: '00hysteresis on temperature', c: 'boilhyston' },
{ v: 6, u: 2, id: '00hysteresis off temperature', c: 'boilhystoff' },
{ v: 0, u: 1, id: '00set flow temperature' },
{ v: 0, u: 3, id: '00burner set power' },
{ v: 0, u: 3, id: '00burner current power' },
{ v: 326323, u: 0, id: '00burner starts' },
{ v: 553437, u: 8, id: '00total burner operating time' },
{ v: 451286, u: 8, id: '00total heat operating time' },
{ v: 4672173, u: 8, id: '00total UBA operating time' },
{ v: '1C(210) 06.06.2020 12:07 (0 min)', u: 0, id: '00last error code' },
{ v: '0H', u: 0, id: '00service code' },
{ v: 203, u: 0, id: '00service code number' },
{ v: 'H00', u: 0, id: '00maintenance message' },
{ v: 'manual', u: 0, id: '00maintenance scheduled', c: 'maintenance', l: ['off', 'time', 'date', 'manual'] },
{ v: 6000, u: 7, id: '00time to next maintenance', c: 'maintenancetime' },
{ v: '01.01.2012', u: 0, id: '00next maintenance date', c: 'maintenancedate', o: 'Format: < dd.mm.yyyy >' },
{ v: 'on', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] },
{ v: 62, u: 1, id: '00dhw set temperature' },
{ v: 60, u: 1, id: '00dhw selected temperature', c: 'wwseltemp' },
{ v: 'flow', u: 0, id: '00dhw type' },
{ v: 'hot', u: 0, id: '00dhw comfort', c: 'wwcomfort', l: ['hot', 'eco', 'intelligent'] },
{ v: 40, u: 2, id: '00dhw flow temperature offset', c: 'wwflowtempoffset' },
{ v: 100, u: 3, id: '00dhw max power', c: 'wwmaxpower' },
{ v: 'off', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] },
{ v: '3-way valve', u: 0, id: '00dhw charging type' },
{ v: -5, u: 2, id: '00dhw hysteresis on temperature', c: 'wwhyston' },
{ v: 0, u: 2, id: '00dhw hysteresis off temperature', c: 'wwhystoff' },
{ v: 70, u: 1, id: '00dhw disinfection temperature', c: 'wwdisinfectiontemp' },
{
v: 'off',
u: 0,
id: '00dhw circulation pump mode',
c: 'wwcircmode',
l: ['off', '1x3min', '2x3min', '3x3min', '4x3min', '5x3min', '6x3min', 'continuous']
},
{ v: 'off', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] },
{ v: 47.3, u: 1, id: '00dhw current intern temperature' },
{ v: 0, u: 4, id: '00dhw current tap water flow' },
{ v: 47.3, u: 1, id: '00dhw storage intern temperature' },
{ v: 'on', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw charging' },
{ v: 'off', u: 0, id: '00dhw recharging' },
{ v: 'on', u: 0, id: '00dhw temperature ok' },
{ v: 'off', u: 0, id: '00dhw active' },
{ v: 'on', u: 0, id: '00dhw 3way valve active' },
{ v: 0, u: 3, id: '00dhw set pump power' },
{ v: 288768, u: 0, id: '00dhw starts' },
{ v: 102151, u: 8, id: '00dhw active time' }
]
};
const emsesp_devicedata_1 = {
data: [
{
@@ -1764,6 +1718,86 @@ const emsesp_devicedata_6 = {
]
};
const emsesp_devicedata_7 = {
data: [
{ v: '', u: 0, id: '08reset', c: 'reset', l: ['-', 'maintenance', 'error'] },
{ v: 'off', u: 0, id: '08heating active' },
{ v: 'off', u: 0, id: '04tapwater active' },
{ v: 5, u: 1, id: '04selected flow temperature', c: 'selflowtemp' },
{ v: 0, u: 3, id: '0Eburner selected max power', c: 'selburnpow' },
{ v: 0, u: 3, id: '00heating pump modulation' },
{ v: 53.4, u: 1, id: '00current flow temperature' },
{ v: 52.7, u: 1, id: '00return temperature' },
{ v: 1.3, u: 10, id: '00system pressure' },
{ v: 54.9, u: 1, id: '00actual boiler temperature' },
{ v: 'off', u: 0, id: '00gas' },
{ v: 'off', u: 0, id: '00gas stage 2' },
{ v: 0, u: 9, id: '00flame current' },
{ v: 'off', u: 0, id: '00heating pump' },
{ v: 'off', u: 0, id: '00fan' },
{ v: 'off', u: 0, id: '00ignition' },
{ v: 'off', u: 0, id: '00oil preheating' },
{ v: 'on', u: 0, id: '00heating activated', c: 'heatingactivated', l: ['off', 'on'] },
{ v: 80, u: 1, id: '00heating temperature', c: 'heatingtemp' },
{ v: 70, u: 3, id: '00burner pump max power', c: 'pumpmodmax' },
{ v: 30, u: 3, id: '00burner pump min power', c: 'pumpmodmin' },
{ v: 1, u: 8, id: '00pump delay', c: 'pumpdelay' },
{ v: 10, u: 8, id: '00burner min period', c: 'burnminperiod' },
{ v: 0, u: 3, id: '00burner min power', c: 'burnminpower' },
{ v: 50, u: 3, id: '00burner max power', c: 'burnmaxpower' },
{ v: -6, u: 2, id: '00hysteresis on temperature', c: 'boilhyston' },
{ v: 6, u: 2, id: '00hysteresis off temperature', c: 'boilhystoff' },
{ v: 0, u: 1, id: '00set flow temperature' },
{ v: 0, u: 3, id: '00burner set power' },
{ v: 0, u: 3, id: '00burner current power' },
{ v: 326323, u: 0, id: '00burner starts' },
{ v: 553437, u: 8, id: '00total burner operating time' },
{ v: 451286, u: 8, id: '00total heat operating time' },
{ v: 4672173, u: 8, id: '00total UBA operating time' },
{ v: '1C(210) 06.06.2020 12:07 (0 min)', u: 0, id: '00last error code' },
{ v: '0H', u: 0, id: '00service code' },
{ v: 203, u: 0, id: '00service code number' },
{ v: 'H00', u: 0, id: '00maintenance message' },
{ v: 'manual', u: 0, id: '00maintenance scheduled', c: 'maintenance', l: ['off', 'time', 'date', 'manual'] },
{ v: 6000, u: 7, id: '00time to next maintenance', c: 'maintenancetime' },
{ v: '01.01.2012', u: 0, id: '00next maintenance date', c: 'maintenancedate', o: 'Format: < dd.mm.yyyy >' },
{ v: 'on', u: 0, id: '00dhw turn on/off', c: 'wwtapactivated', l: ['off', 'on'] },
{ v: 62, u: 1, id: '00dhw set temperature' },
{ v: 60, u: 1, id: '00dhw selected temperature', c: 'wwseltemp' },
{ v: 'flow', u: 0, id: '00dhw type' },
{ v: 'hot', u: 0, id: '00dhw comfort', c: 'wwcomfort', l: ['hot', 'eco', 'intelligent'] },
{ v: 40, u: 2, id: '00dhw flow temperature offset', c: 'wwflowtempoffset' },
{ v: 100, u: 3, id: '00dhw max power', c: 'wwmaxpower' },
{ v: 'off', u: 0, id: '00dhw circulation pump available', c: 'wwcircpump', l: ['off', 'on'] },
{ v: '3-way valve', u: 0, id: '00dhw charging type' },
{ v: -5, u: 2, id: '00dhw hysteresis on temperature', c: 'wwhyston' },
{ v: 0, u: 2, id: '00dhw hysteresis off temperature', c: 'wwhystoff' },
{ v: 70, u: 1, id: '00dhw disinfection temperature', c: 'wwdisinfectiontemp' },
{
v: 'off',
u: 0,
id: '00dhw circulation pump mode',
c: 'wwcircmode',
l: ['off', '1x3min', '2x3min', '3x3min', '4x3min', '5x3min', '6x3min', 'continuous']
},
{ v: 'off', u: 0, id: '00dhw circulation active', c: 'wwcirc', l: ['off', 'on'] },
{ v: 47.3, u: 1, id: '00dhw current intern temperature' },
{ v: 0, u: 4, id: '00dhw current tap water flow' },
{ v: 47.3, u: 1, id: '00dhw storage intern temperature' },
{ v: 'on', u: 0, id: '00dhw activated', c: 'wwactivated', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw one time charging', c: 'wwonetime', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw disinfecting', c: 'wwdisinfecting', l: ['off', 'on'] },
{ v: 'off', u: 0, id: '00dhw charging' },
{ v: 'off', u: 0, id: '00dhw recharging' },
{ v: 'on', u: 0, id: '00dhw temperature ok' },
{ v: 'off', u: 0, id: '00dhw active' },
{ v: 'on', u: 0, id: '00dhw 3way valve active' },
{ v: 0, u: 3, id: '00dhw set pump power' },
{ v: 288768, u: 0, id: '00dhw starts' },
{ v: 102151, u: 8, id: '00dhw active time' }
]
};
const emsesp_devicedata_99 = {
data: [
{
@@ -1836,7 +1870,6 @@ let emsesp_schedule = {
};
// CUSTOMIZATIONS
const emsesp_deviceentities_1 = [{}];
const emsesp_deviceentities_3 = [{}];
const emsesp_deviceentities_5 = [{}];
@@ -1991,11 +2024,9 @@ const emsesp_deviceentities_4 = [
];
// LOG
rest_server.get(FETCH_LOG_ENDPOINT, (req, res) => {
const encoded = msgpack.encode(fetch_log);
console.log('fetchlog');
res.write(encoded, 'binary');
res.end(null, 'binary');
rest_server.post(FETCH_LOG_ENDPOINT, (req, res) => {
console.log('command: fetchLog');
res.sendStatus(200);
});
rest_server.get(LOG_SETTINGS_ENDPOINT, (req, res) => {
res.json(log_settings);
@@ -2003,7 +2034,7 @@ rest_server.get(LOG_SETTINGS_ENDPOINT, (req, res) => {
rest_server.post(LOG_SETTINGS_ENDPOINT, (req, res) => {
log_settings = req.body;
console.log(JSON.stringify(log_settings));
res.json(log_settings);
res.sendStatus(200);
});
// NETWORK
@@ -2016,13 +2047,21 @@ rest_server.get(NETWORK_SETTINGS_ENDPOINT, (req, res) => {
rest_server.post(NETWORK_SETTINGS_ENDPOINT, (req, res) => {
network_settings = req.body;
console.log(JSON.stringify(network_settings));
res.json(network_settings);
res.sendStatus(200);
});
rest_server.get(LIST_NETWORKS_ENDPOINT, (req, res) => {
res.json(list_networks);
if (countWifiScanPoll++ === 3) {
// console.log('done, have list');
res.json(list_networks); // send list
} else {
// console.log('...waiting #' + countWifiScanPoll);
res.sendStatus(200); // waiting....
}
});
rest_server.get(SCAN_NETWORKS_ENDPOINT, (req, res) => {
res.sendStatus(202);
console.log('start scan networks');
countWifiScanPoll = 0; // stop the poll
res.sendStatus(200); // always 202, poll for list
});
// AP
@@ -2030,12 +2069,13 @@ rest_server.get(AP_SETTINGS_ENDPOINT, (req, res) => {
res.json(ap_settings);
});
rest_server.get(AP_STATUS_ENDPOINT, (req, res) => {
console.log('get apStatus', ap_status);
res.json(ap_status);
});
rest_server.post(AP_SETTINGS_ENDPOINT, (req, res) => {
ap_status = req.body;
console.log(JSON.stringify(ap_settings));
res.json(ap_settings);
ap_settings = req.body;
console.log('post apSettings', ap_settings);
res.sendStatus(200);
});
// OTA
@@ -2045,7 +2085,7 @@ rest_server.get(OTA_SETTINGS_ENDPOINT, (req, res) => {
rest_server.post(OTA_SETTINGS_ENDPOINT, (req, res) => {
ota_settings = req.body;
console.log(JSON.stringify(ota_settings));
res.json(ota_settings);
res.sendStatus(200);
});
// MQTT
@@ -2055,7 +2095,7 @@ rest_server.get(MQTT_SETTINGS_ENDPOINT, (req, res) => {
rest_server.post(MQTT_SETTINGS_ENDPOINT, (req, res) => {
mqtt_settings = req.body;
console.log(JSON.stringify(mqtt_settings));
res.json(mqtt_settings);
res.sendStatus(200);
});
rest_server.get(MQTT_STATUS_ENDPOINT, (req, res) => {
res.json(mqtt_status);
@@ -2068,7 +2108,7 @@ rest_server.get(NTP_SETTINGS_ENDPOINT, (req, res) => {
rest_server.post(NTP_SETTINGS_ENDPOINT, (req, res) => {
ntp_settings = req.body;
console.log(JSON.stringify(ntp_settings));
res.json(ntp_settings);
res.sendStatus(200);
});
rest_server.get(NTP_STATUS_ENDPOINT, (req, res) => {
res.json(ntp_status);
@@ -2079,6 +2119,9 @@ rest_server.post(TIME_ENDPOINT, (req, res) => {
// SYSTEM
rest_server.get(SYSTEM_STATUS_ENDPOINT, (req, res) => {
console.log('get systemStatus');
// create some random data to see if caching works
system_status.fs_used = Math.floor(Math.random() * (Math.floor(200) - 100) + 100);
res.json(system_status);
});
rest_server.get(SECURITY_SETTINGS_ENDPOINT, (req, res) => {
@@ -2087,7 +2130,7 @@ rest_server.get(SECURITY_SETTINGS_ENDPOINT, (req, res) => {
rest_server.post(SECURITY_SETTINGS_ENDPOINT, (req, res) => {
security_settings = req.body;
console.log(JSON.stringify(security_settings));
res.json(security_settings);
res.sendStatus(200);
});
rest_server.get(FEATURES_ENDPOINT, (req, res) => {
res.json(features);
@@ -2095,19 +2138,33 @@ rest_server.get(FEATURES_ENDPOINT, (req, res) => {
rest_server.get(VERIFY_AUTHORIZATION_ENDPOINT, (req, res) => {
res.json(verify_authentication);
});
rest_server.post(RESTART_ENDPOINT, (req, res) => {
rest_server.post(RESTART_ENDPOINT, async (req, res) => {
console.log('command: restart');
// await delay(1000);
res.sendStatus(200);
});
rest_server.post(FACTORY_RESET_ENDPOINT, (req, res) => {
console.log('command: reset');
res.sendStatus(200);
});
rest_server.post(UPLOAD_FILE_ENDPOINT, (req, res) => {
res.sendStatus(200);
});
rest_server.post(SIGN_IN_ENDPOINT, (req, res) => {
console.log('Signed in as ' + req.body.username);
res.json(signin);
rest_server.post(UPLOAD_FILE_ENDPOINT, progress_middleware, upload.single('file'), (req, res) => {
console.log('command: uploadFile completed.');
if (req.file) {
const filename = req.file.originalname;
const ext = filename.substring(filename.lastIndexOf('.') + 1);
console.log(req.file);
console.log('ext: ' + ext);
if (ext === 'bin') {
return res.sendStatus(200);
} else if (ext === 'md5') {
return res.json({ md5: 'ef4304fc4d9025a58dcf25d71c882d2c' });
}
}
return res.sendStatus(400);
});
rest_server.get(GENERATE_TOKEN_ENDPOINT, (req, res) => {
res.json(generate_token);
});
@@ -2124,8 +2181,8 @@ rest_server.get(EMSESP_SETTINGS_ENDPOINT, (req, res) => {
rest_server.post(EMSESP_SETTINGS_ENDPOINT, (req, res) => {
settings = req.body;
console.log('Write settings: ' + JSON.stringify(settings));
res.status(202).json(settings); // restart needed
// res.status(200).json(settings); // no restart needed
// res.sendStatus(205); // restart needed
res.sendStatus(200); // no restart needed
});
rest_server.get(EMSESP_CORE_DATA_ENDPOINT, (req, res) => {
console.log('send back core data...');
@@ -2146,8 +2203,9 @@ rest_server.post(EMSESP_SCANDEVICES_ENDPOINT, (req, res) => {
rest_server.get(EMSESP_STATUS_ENDPOINT, (req, res) => {
res.json(status);
});
rest_server.post(EMSESP_DEVICEDATA_ENDPOINT, (req, res) => {
const id = req.body.id;
rest_server.get(EMSESP_DEVICEDATA_ENDPOINT, (req, res) => {
const id = Number(req.query.id);
console.log('send back device data for ' + id);
let data = {};
@@ -2179,8 +2237,9 @@ rest_server.post(EMSESP_DEVICEDATA_ENDPOINT, (req, res) => {
res.end(null, 'binary');
});
rest_server.post(EMSESP_DEVICEENTITIES_ENDPOINT, (req, res) => {
const id = req.body.id;
rest_server.get(EMSESP_DEVICEENTITIES_ENDPOINT, (req, res) => {
const id = Number(req.query.id);
console.log('deviceentities for device ' + id + ' received');
let data = null;
if (id === 1) {
@@ -2204,7 +2263,6 @@ rest_server.post(EMSESP_DEVICEENTITIES_ENDPOINT, (req, res) => {
if (id === 7) {
data = emsesp_deviceentities_7;
}
res.write(msgpack.encode(data), 'binary');
res.end(null, 'binary');
});
@@ -2229,6 +2287,7 @@ function updateMask(entity, de, dd) {
}
// find in dd, either looking for fullname or custom name
// console.log('looking for ' + fullname + ' in ' + dd.data);
dd_objIndex = dd.data.findIndex((obj) => obj.id.slice(2) === fullname);
if (dd_objIndex !== -1) {
let changed = new Boolean(false);
@@ -2320,63 +2379,60 @@ rest_server.post(EMSESP_CUSTOM_ENTITIES_ENDPOINT, (req, res) => {
rest_server.post(EMSESP_WRITE_SCHEDULE_ENDPOINT, (req, res) => {
console.log('write schedule');
console.log(req.body.schedule);
console.log(req.body);
emsesp_schedule = req.body;
res.sendStatus(200);
});
rest_server.post(EMSESP_WRITE_ENTITIES_ENDPOINT, (req, res) => {
console.log('write entities');
console.log(req.body.entities);
console.log(req.body);
emsesp_entities = req.body;
res.sendStatus(200);
});
rest_server.post(EMSESP_WRITE_VALUE_ENDPOINT, (req, res) => {
rest_server.post(EMSESP_WRITE_VALUE_ENDPOINT, async (req, res) => {
console.log(req.body);
const devicevalue = req.body.devicevalue;
const id = req.body.id;
console.log('Write device value for id : ' + id);
console.log(' devicedata: ' + JSON.stringify(devicevalue));
if (id === 1) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_1.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_1.data[objIndex] = devicevalue;
}
if (id === 2) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_2.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_2.data[objIndex] = devicevalue;
}
if (id === 3) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_3.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_3.data[objIndex] = devicevalue;
}
if (id === 4) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_4.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_4.data[objIndex] = devicevalue;
}
if (id === 5) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_5.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_5.data[objIndex] = devicevalue;
}
if (id === 6) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_6.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_6.data[objIndex] = devicevalue;
}
if (id === 7) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_7.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_7.data[objIndex] = devicevalue;
}
if (id === 99) {
console.log('Write device value for: ' + JSON.stringify(devicevalue));
objIndex = emsesp_devicedata_99.data.findIndex((obj) => obj.c == devicevalue.c);
emsesp_devicedata_99.data[objIndex] = devicevalue;
}
// await delay(2000); // wait 2 seconds to show spinner
// res.sendStatus(400); // bad request
res.sendStatus(200);
});
@@ -2431,10 +2487,12 @@ rest_server.post(EMSESP_WRITE_ANALOG_ENDPOINT, (req, res) => {
res.sendStatus(200);
});
rest_server.post(EMSESP_BOARDPROFILE_ENDPOINT, (req, res) => {
const board_profile = req.body.board_profile;
rest_server.get(EMSESP_BOARDPROFILE_ENDPOINT, (req, res) => {
const board_profile = req.query.boardProfile;
// default values
const data = {
board_profile: board_profile,
led_gpio: settings.led_gpio,
dallas_gpio: settings.dallas_gpio,
rx_gpio: settings.rx_gpio,
@@ -2558,9 +2616,10 @@ rest_server.post(EMSESP_BOARDPROFILE_ENDPOINT, (req, res) => {
data.eth_clock_mode = 0;
}
console.log('boardProfile POST. Sending back, profile: ' + board_profile + ', ' + 'data: ' + JSON.stringify(data));
console.log('boardProfile GET. Sending back, profile: ' + board_profile + ', ' + 'data: ' + JSON.stringify(data));
res.send(data);
// res.sendStatus(400); // send back an error, for testing
res.json(data);
});
// EMS-ESP API specific
@@ -2618,6 +2677,7 @@ rest_server.post(API_ENDPOINT_ROOT, (req, res) => {
if (req.body.device === 'system') {
if (req.body.entity === 'info') {
console.log('sending system info: ' + JSON.stringify(emsesp_info));
res.json(emsesp_info);
} else if (req.body.entity === 'settings') {
console.log('sending system settings: ' + JSON.stringify(settings));
res.json(settings);
@@ -2645,26 +2705,38 @@ rest_server.get(SYSTEM_INFO_ENDPOINT, (req, res) => {
const GET_SETTINGS_ENDPOINT = REST_ENDPOINT_ROOT + 'getSettings';
rest_server.get(GET_SETTINGS_ENDPOINT, (req, res) => {
console.log('System Settings:');
console.log('getSettings');
res.json(settings);
});
const GET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'getCustomizations';
rest_server.get(GET_CUSTOMIZATIONS_ENDPOINT, (req, res) => {
console.log('Customization');
console.log('getCustomization');
// not implemented yet
res.sendStatus(200);
});
const GET_SCHEDULE_ENDPOINT = REST_ENDPOINT_ROOT + 'schedule';
const GET_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'getEntities';
rest_server.get(GET_ENTITIES_ENDPOINT, (req, res) => {
console.log('getEntities');
res.json(emsesp_entities);
});
const GET_SCHEDULE_ENDPOINT = REST_ENDPOINT_ROOT + 'getSchedule';
rest_server.get(GET_SCHEDULE_ENDPOINT, (req, res) => {
console.log('getSchedule');
res.json(emsesp_schedule);
});
const SCHEDULE_ENDPOINT = REST_ENDPOINT_ROOT + 'schedule';
rest_server.get(SCHEDULE_ENDPOINT, (req, res) => {
console.log('Sending Schedule data');
res.json(emsesp_schedule);
});
const GET_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'entities';
rest_server.get(GET_ENTITIES_ENDPOINT, (req, res) => {
console.log('Sending Entities data');
const ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'entities';
rest_server.get(ENTITIES_ENDPOINT, (req, res) => {
console.log('Sending Custom Entities data');
res.json(emsesp_entities);
});
@@ -2705,5 +2777,5 @@ rest_server.get(ES_LOG_ENDPOINT, function (req, res) {
log_index = 0;
}
fetch_log.events.push(data); // append to buffer
}, 1000);
}, 3000);
});

View File

@@ -0,0 +1 @@
leave empty

View File

@@ -117,10 +117,18 @@ __metadata:
"@msgpack/msgpack": ^2.8.0
compression: ^1.7.4
express: ^4.18.2
multer: ^1.4.5-lts.1
nodemon: ^2.0.22
languageName: unknown
linkType: soft
"append-field@npm:^1.0.0":
version: 1.0.0
resolution: "append-field@npm:1.0.0"
checksum: 482ba08acc0ecef00fe7da6bf2f8e48359a9905ee1af525f3120c9260c02e91eedf0579b59d898e8d8455b6c199e340bc0a2fd4b9e02adaa29a8a86c722b37f9
languageName: node
linkType: hard
"aproba@npm:^1.0.3 || ^2.0.0":
version: 2.0.0
resolution: "aproba@npm:2.0.0"
@@ -207,6 +215,22 @@ __metadata:
languageName: node
linkType: hard
"buffer-from@npm:^1.0.0":
version: 1.1.2
resolution: "buffer-from@npm:1.1.2"
checksum: 0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb
languageName: node
linkType: hard
"busboy@npm:^1.0.0":
version: 1.6.0
resolution: "busboy@npm:1.6.0"
dependencies:
streamsearch: ^1.1.0
checksum: 32801e2c0164e12106bf236291a00795c3c4e4b709ae02132883fe8478ba2ae23743b11c5735a0aae8afe65ac4b6ca4568b91f0d9fed1fdbc32ede824a73746e
languageName: node
linkType: hard
"bytes@npm:3.0.0":
version: 3.0.0
resolution: "bytes@npm:3.0.0"
@@ -330,6 +354,18 @@ __metadata:
languageName: node
linkType: hard
"concat-stream@npm:^1.5.2":
version: 1.6.2
resolution: "concat-stream@npm:1.6.2"
dependencies:
buffer-from: ^1.0.0
inherits: ^2.0.3
readable-stream: ^2.2.2
typedarray: ^0.0.6
checksum: 1ef77032cb4459dcd5187bd710d6fc962b067b64ec6a505810de3d2b8cc0605638551b42f8ec91edf6fcd26141b32ef19ad749239b58fae3aba99187adc32285
languageName: node
linkType: hard
"console-control-strings@npm:^1.1.0":
version: 1.1.0
resolution: "console-control-strings@npm:1.1.0"
@@ -367,6 +403,13 @@ __metadata:
languageName: node
linkType: hard
"core-util-is@npm:~1.0.0":
version: 1.0.3
resolution: "core-util-is@npm:1.0.3"
checksum: 9de8597363a8e9b9952491ebe18167e3b36e7707569eed0ebf14f8bba773611376466ae34575bca8cfe3c767890c859c74056084738f09d4e4a6f902b2ad7d99
languageName: node
linkType: hard
"debug@npm:2.6.9":
version: 2.6.9
resolution: "debug@npm:2.6.9"
@@ -808,7 +851,7 @@ __metadata:
languageName: node
linkType: hard
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3":
"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:~2.0.3":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1
@@ -875,6 +918,13 @@ __metadata:
languageName: node
linkType: hard
"isarray@npm:~1.0.0":
version: 1.0.0
resolution: "isarray@npm:1.0.0"
checksum: f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab
languageName: node
linkType: hard
"isexe@npm:^2.0.0":
version: 2.0.0
resolution: "isexe@npm:2.0.0"
@@ -986,6 +1036,13 @@ __metadata:
languageName: node
linkType: hard
"minimist@npm:^1.2.6":
version: 1.2.8
resolution: "minimist@npm:1.2.8"
checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0
languageName: node
linkType: hard
"minipass-collect@npm:^1.0.2":
version: 1.0.2
resolution: "minipass-collect@npm:1.0.2"
@@ -1063,6 +1120,17 @@ __metadata:
languageName: node
linkType: hard
"mkdirp@npm:^0.5.4":
version: 0.5.6
resolution: "mkdirp@npm:0.5.6"
dependencies:
minimist: ^1.2.6
bin:
mkdirp: bin/cmd.js
checksum: 0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2
languageName: node
linkType: hard
"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4":
version: 1.0.4
resolution: "mkdirp@npm:1.0.4"
@@ -1093,6 +1161,21 @@ __metadata:
languageName: node
linkType: hard
"multer@npm:^1.4.5-lts.1":
version: 1.4.5-lts.1
resolution: "multer@npm:1.4.5-lts.1"
dependencies:
append-field: ^1.0.0
busboy: ^1.0.0
concat-stream: ^1.5.2
mkdirp: ^0.5.4
object-assign: ^4.1.1
type-is: ^1.6.4
xtend: ^4.0.0
checksum: d6dfa78a6ec592b74890412f8962da8a87a3dcfe20f612e039b735b8e0faa72c735516c447f7de694ee0d981eb0a1b892fb9e2402a0348dc6091d18c38d89ecc
languageName: node
linkType: hard
"negotiator@npm:0.6.3, negotiator@npm:^0.6.3":
version: 0.6.3
resolution: "negotiator@npm:0.6.3"
@@ -1181,6 +1264,13 @@ __metadata:
languageName: node
linkType: hard
"object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f
languageName: node
linkType: hard
"object-inspect@npm:^1.9.0":
version: 1.12.3
resolution: "object-inspect@npm:1.12.3"
@@ -1250,6 +1340,13 @@ __metadata:
languageName: node
linkType: hard
"process-nextick-args@npm:~2.0.0":
version: 2.0.1
resolution: "process-nextick-args@npm:2.0.1"
checksum: 1d38588e520dab7cea67cbbe2efdd86a10cc7a074c09657635e34f035277b59fbb57d09d8638346bf7090f8e8ebc070c96fa5fd183b777fff4f5edff5e9466cf
languageName: node
linkType: hard
"promise-inflight@npm:^1.0.1":
version: 1.0.1
resolution: "promise-inflight@npm:1.0.1"
@@ -1312,6 +1409,21 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:^2.2.2":
version: 2.3.8
resolution: "readable-stream@npm:2.3.8"
dependencies:
core-util-is: ~1.0.0
inherits: ~2.0.3
isarray: ~1.0.0
process-nextick-args: ~2.0.0
safe-buffer: ~5.1.1
string_decoder: ~1.1.1
util-deprecate: ~1.0.1
checksum: 65645467038704f0c8aaf026a72fbb588a9e2ef7a75cd57a01702ee9db1c4a1e4b03aaad36861a6a0926546a74d174149c8c207527963e0c2d3eee2f37678a42
languageName: node
linkType: hard
"readable-stream@npm:^3.6.0":
version: 3.6.0
resolution: "readable-stream@npm:3.6.0"
@@ -1350,7 +1462,7 @@ __metadata:
languageName: node
linkType: hard
"safe-buffer@npm:5.1.2":
"safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1":
version: 5.1.2
resolution: "safe-buffer@npm:5.1.2"
checksum: f2f1f7943ca44a594893a852894055cf619c1fbcb611237fc39e461ae751187e7baf4dc391a72125e0ac4fb2d8c5c0b3c71529622e6a58f46b960211e704903c
@@ -1518,6 +1630,13 @@ __metadata:
languageName: node
linkType: hard
"streamsearch@npm:^1.1.0":
version: 1.1.0
resolution: "streamsearch@npm:1.1.0"
checksum: 1cce16cea8405d7a233d32ca5e00a00169cc0e19fbc02aa839959985f267335d435c07f96e5e0edd0eadc6d39c98d5435fb5bbbdefc62c41834eadc5622ad942
languageName: node
linkType: hard
"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.2.3":
version: 4.2.3
resolution: "string-width@npm:4.2.3"
@@ -1538,6 +1657,15 @@ __metadata:
languageName: node
linkType: hard
"string_decoder@npm:~1.1.1":
version: 1.1.1
resolution: "string_decoder@npm:1.1.1"
dependencies:
safe-buffer: ~5.1.0
checksum: 9ab7e56f9d60a28f2be697419917c50cac19f3e8e6c28ef26ed5f4852289fe0de5d6997d29becf59028556f2c62983790c1d9ba1e2a3cc401768ca12d5183a5b
languageName: node
linkType: hard
"strip-ansi@npm:^6.0.1":
version: 6.0.1
resolution: "strip-ansi@npm:6.0.1"
@@ -1597,7 +1725,7 @@ __metadata:
languageName: node
linkType: hard
"type-is@npm:~1.6.18":
"type-is@npm:^1.6.4, type-is@npm:~1.6.18":
version: 1.6.18
resolution: "type-is@npm:1.6.18"
dependencies:
@@ -1607,6 +1735,13 @@ __metadata:
languageName: node
linkType: hard
"typedarray@npm:^0.0.6":
version: 0.0.6
resolution: "typedarray@npm:0.0.6"
checksum: 33b39f3d0e8463985eeaeeacc3cb2e28bc3dfaf2a5ed219628c0b629d5d7b810b0eb2165f9f607c34871d5daa92ba1dc69f49051cf7d578b4cbd26c340b9d1b1
languageName: node
linkType: hard
"undefsafe@npm:^2.0.5":
version: 2.0.5
resolution: "undefsafe@npm:2.0.5"
@@ -1639,7 +1774,7 @@ __metadata:
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1":
"util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"
checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2
@@ -1687,6 +1822,13 @@ __metadata:
languageName: node
linkType: hard
"xtend@npm:^4.0.0":
version: 4.0.2
resolution: "xtend@npm:4.0.2"
checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a
languageName: node
linkType: hard
"yallist@npm:^4.0.0":
version: 4.0.0
resolution: "yallist@npm:4.0.0"

View File

@@ -49,6 +49,7 @@ platform = espressif32
board_build.partitions = esp32_partition_debug.csv
upload_protocol = esptool
build_type = debug
monitor_raw = no
monitor_filters = esp32_exception_decoder
debug_tool = esp-prog
debug_init_break = tbreak setup

View File

@@ -479,7 +479,7 @@ void AnalogSensor::publish_values(const bool force) {
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/analogsensor_data", Mqtt::base().c_str()); // use base path
snprintf(stat_t, sizeof(stat_t), "%s/analogsensor_data", Mqtt::basename().c_str()); // use basename
config["stat_t"] = stat_t;
char val_obj[50];
@@ -570,7 +570,7 @@ void AnalogSensor::publish_values(const bool force) {
JsonObject dev = config.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
ids.add(Mqtt::basename());
// add "availability" section
Mqtt::add_avty_to_doc(stat_t, config.as<JsonObject>(), val_cond);

View File

@@ -1451,6 +1451,7 @@ void EMSESP::start() {
esp8266React.begin(); // loads core system services settings (network, mqtt, ap, ntp etc)
webLogService.begin(); // start web log service. now we can start capturing logs to the web log
LOG_INFO("Starting EMS-ESP version %s", EMSESP_APP_VERSION); // welcome message
LOG_DEBUG("System is running in Debug mode");
LOG_INFO("Last system reset reason Core0: %s, Core1: %s", system_.reset_reason(0).c_str(), system_.reset_reason(1).c_str());
@@ -1485,13 +1486,8 @@ void EMSESP::start() {
#endif
}
// start all the EMS-ESP services
mqtt_.start(); // mqtt init
system_.start(); // starts commands, led, adc, button, network, syslog & uart
LOG_INFO(("Starting EMS-ESP version %s (hostname: %s)"), EMSESP_APP_VERSION, system_.hostname().c_str()); // welcome message
system_.start(); // starts commands, led, adc, button, network (sets hostname), syslog & uart
shower_.start(); // initialize shower timer and shower alert
temperaturesensor_.start(); // Temperature external sensors
analogsensor_.start(); // Analog external sensors

View File

@@ -536,7 +536,7 @@ void Mqtt::ha_status() {
doc["uniq_id"] = uniq;
doc["obj_id"] = uniq;
doc["stat_t"] = mqtt_base_ + "/status";
doc["stat_t"] = mqtt_basename_ + "/status";
doc["name"] = "EMS-ESP status";
doc["pl_on"] = "online";
doc["pl_off"] = "offline";
@@ -548,7 +548,7 @@ void Mqtt::ha_status() {
// doc["json_attr_t"] = "~/heartbeat"; // store also as HA attributes
JsonObject dev = doc.createNestedObject("dev");
dev["name"] = "EMS-ESP";
dev["name"] = Mqtt::basename(); // take basename
dev["sw"] = "v" + std::string(EMSESP_APP_VERSION);
dev["mf"] = "proddy";
dev["mdl"] = "EMS-ESP";
@@ -556,7 +556,7 @@ void Mqtt::ha_status() {
dev["cu"] = "http://" + (EMSESP::system_.ethernet_connected() ? ETH.localIP().toString() : WiFi.localIP().toString());
#endif
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
ids.add(Mqtt::basename());
char topic[MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "binary_sensor/%s/system_status/config", mqtt_basename_.c_str());
@@ -715,13 +715,13 @@ bool Mqtt::publish_ha_sensor_config(DeviceValue & dv, const char * model, const
JsonArray ids = dev_json.createNestedArray("ids");
char ha_device[40];
auto device_type_name = EMSdevice::device_type_2_device_name(dv.device_type);
snprintf(ha_device, sizeof(ha_device), "ems-esp-%s", device_type_name);
snprintf(ha_device, sizeof(ha_device), "%s-%s", Mqtt::basename().c_str(), device_type_name);
ids.add(ha_device);
if (create_device_config) {
auto cap_name = strdup(device_type_name);
Helpers::CharToUpperUTF8(cap_name); // capitalize first letter
dev_json["name"] = std::string("EMS-ESP ") + cap_name;
dev_json["name"] = Mqtt::basename() + " " + cap_name;
dev_json["mf"] = brand;
dev_json["mdl"] = model;
dev_json["via_device"] = "ems-esp";
@@ -760,7 +760,7 @@ bool Mqtt::publish_system_ha_sensor_config(uint8_t type, const char * name, cons
JsonObject dev_json = doc.createNestedObject("dev");
JsonArray ids = dev_json.createNestedArray("ids");
ids.add("ems-esp");
ids.add(Mqtt::basename());
return publish_ha_sensor_config(
type, DeviceValueTAG::TAG_HEARTBEAT, name, name, EMSdevice::DeviceType::SYSTEM, entity, uom, false, false, nullptr, 0, 0, 0, 0, dev_json);
@@ -1212,7 +1212,10 @@ bool Mqtt::publish_ha_climate_config(const uint8_t tag, const bool has_roomtemp,
JsonObject dev = doc.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp-thermostat");
char ha_device[40];
snprintf(ha_device, sizeof(ha_device), "%s-thermostat", Mqtt::basename().c_str());
ids.add(ha_device);
// add "availability" section
add_avty_to_doc(topic_t, doc.as<JsonObject>(), seltemp_cond, has_roomtemp ? currtemp_cond : nullptr, hc_mode_cond);

View File

@@ -164,7 +164,7 @@ void Shower::set_shower_state(bool state, bool force) {
doc["object_id"] = str;
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/shower_active", Mqtt::base().c_str()); // use base path
snprintf(stat_t, sizeof(stat_t), "%s/shower_active", Mqtt::basename().c_str());
doc["stat_t"] = stat_t;
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
@@ -181,7 +181,7 @@ void Shower::set_shower_state(bool state, bool force) {
JsonObject dev = doc.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
ids.add(Mqtt::basename());
// add "availability" section
Mqtt::add_avty_to_doc(stat_t, doc.as<JsonObject>());

View File

@@ -511,7 +511,7 @@ void TemperatureSensor::publish_values(const bool force) {
config["dev_cla"] = "temperature";
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/temperaturesensor_data", Mqtt::base().c_str()); // use base path
snprintf(stat_t, sizeof(stat_t), "%s/temperaturesensor_data", Mqtt::basename().c_str());
config["stat_t"] = stat_t;
config["unit_of_meas"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);
@@ -543,7 +543,7 @@ void TemperatureSensor::publish_values(const bool force) {
JsonObject dev = config.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
ids.add(Mqtt::basename());
// add "availability" section
Mqtt::add_avty_to_doc(stat_t, config.as<JsonObject>(), val_cond);

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.6.0-dev.12"
#define EMSESP_APP_VERSION "3.6.0-dev.13b"

View File

@@ -33,10 +33,12 @@ WebCustomizationService::WebCustomizationService(AsyncWebServer * server, FS * f
, _fsPersistence(WebCustomization::read, WebCustomization::update, this, fs, EMSESP_CUSTOMIZATION_FILE)
, _masked_entities_handler(CUSTOM_ENTITIES_PATH,
securityManager->wrapCallback(std::bind(&WebCustomizationService::custom_entities, this, _1, _2),
AuthenticationPredicates::IS_AUTHENTICATED))
, _device_entities_handler(DEVICE_ENTITIES_PATH,
securityManager->wrapCallback(std::bind(&WebCustomizationService::device_entities, this, _1, _2),
AuthenticationPredicates::IS_AUTHENTICATED)) {
server->on(DEVICE_ENTITIES_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebCustomizationService::device_entities, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(DEVICES_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebCustomizationService::devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
@@ -49,10 +51,6 @@ WebCustomizationService::WebCustomizationService(AsyncWebServer * server, FS * f
_masked_entities_handler.setMaxContentLength(2048);
_masked_entities_handler.setMaxJsonBufferSize(2048);
server->addHandler(&_masked_entities_handler);
_device_entities_handler.setMethod(HTTP_POST);
_device_entities_handler.setMaxContentLength(256);
server->addHandler(&_device_entities_handler);
}
// this creates the customization file, saving it to the FS
@@ -165,13 +163,13 @@ StateUpdateResult WebCustomization::update(JsonObject & root, WebCustomization &
void WebCustomizationService::reset_customization(AsyncWebServerRequest * request) {
#ifndef EMSESP_STANDALONE
if (LittleFS.remove(EMSESP_CUSTOMIZATION_FILE)) {
AsyncWebServerResponse * response = request->beginResponse(200); // OK
AsyncWebServerResponse * response = request->beginResponse(205); // restart needed
request->send(response);
EMSESP::system_.restart_requested(true);
return;
}
// failed
AsyncWebServerResponse * response = request->beginResponse(204); // no content error
AsyncWebServerResponse * response = request->beginResponse(400); // bad request
request->send(response);
#endif
}
@@ -199,17 +197,21 @@ void WebCustomizationService::devices(AsyncWebServerRequest * request) {
}
// send back list of device entities
void WebCustomizationService::device_entities(AsyncWebServerRequest * request, JsonVariant & json) {
if (json.is<JsonObject>()) {
void WebCustomizationService::device_entities(AsyncWebServerRequest * request) {
uint8_t id;
if (request->hasParam(F_(id))) {
id = Helpers::atoint(request->getParam(F_(id))->value().c_str()); // get id from url
size_t buffer = EMSESP_JSON_SIZE_XXXXLARGE;
auto * response = new MsgpackAsyncJsonResponse(true, buffer);
while (!response->getSize()) {
delete response;
buffer -= 1024;
response = new MsgpackAsyncJsonResponse(true, buffer);
}
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->unique_id() == json["id"]) {
if (emsdevice->unique_id() == id) {
#ifndef EMSESP_STANDALONE
JsonArray output = response->getRoot();
emsdevice->generate_values_web_customization(output);
@@ -319,7 +321,7 @@ void WebCustomizationService::custom_entities(AsyncWebServerRequest * request, J
}
}
AsyncWebServerResponse * response = request->beginResponse(need_reboot ? 201 : 200); // OK
AsyncWebServerResponse * response = request->beginResponse(need_reboot ? 205 : 200); // reboot or just OK
request->send(response);
}

View File

@@ -24,9 +24,9 @@
// GET
#define DEVICES_SERVICE_PATH "/rest/devices"
#define EMSESP_CUSTOMIZATION_SERVICE_PATH "/rest/customization"
#define DEVICE_ENTITIES_PATH "/rest/deviceEntities"
// POST
#define DEVICE_ENTITIES_PATH "/rest/deviceEntities"
#define CUSTOM_ENTITIES_PATH "/rest/customEntities"
#define RESET_CUSTOMIZATION_SERVICE_PATH "/rest/resetCustomizations"
@@ -91,13 +91,13 @@ class WebCustomizationService : public StatefulService<WebCustomization> {
// GET
void devices(AsyncWebServerRequest * request);
void device_entities(AsyncWebServerRequest * request);
// POST
void custom_entities(AsyncWebServerRequest * request, JsonVariant & json);
void device_entities(AsyncWebServerRequest * request, JsonVariant & json);
void reset_customization(AsyncWebServerRequest * request);
void reset_customization(AsyncWebServerRequest * request); // command
AsyncCallbackJsonWebHandler _masked_entities_handler, _device_entities_handler;
AsyncCallbackJsonWebHandler _masked_entities_handler;
};
} // namespace emsesp

View File

@@ -23,15 +23,18 @@ namespace emsesp {
using namespace std::placeholders; // for `_1` etc
WebDataService::WebDataService(AsyncWebServer * server, SecurityManager * securityManager)
: _device_data_handler(DEVICE_DATA_SERVICE_PATH,
securityManager->wrapCallback(std::bind(&WebDataService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED))
, _write_value_handler(WRITE_DEVICE_VALUE_SERVICE_PATH,
: _write_value_handler(WRITE_DEVICE_VALUE_SERVICE_PATH,
securityManager->wrapCallback(std::bind(&WebDataService::write_device_value, this, _1, _2), AuthenticationPredicates::IS_ADMIN))
, _write_temperature_handler(WRITE_TEMPERATURE_SENSOR_SERVICE_PATH,
securityManager->wrapCallback(std::bind(&WebDataService::write_temperature_sensor, this, _1, _2),
AuthenticationPredicates::IS_ADMIN))
, _write_analog_handler(WRITE_ANALOG_SENSOR_SERVICE_PATH,
securityManager->wrapCallback(std::bind(&WebDataService::write_analog_sensor, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) {
// GET's
server->on(DEVICE_DATA_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebDataService::device_data, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(CORE_DATA_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebDataService::core_data, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
@@ -40,14 +43,11 @@ WebDataService::WebDataService(AsyncWebServer * server, SecurityManager * securi
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebDataService::sensor_data, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
// POST's
server->on(SCAN_DEVICES_SERVICE_PATH,
HTTP_POST,
securityManager->wrapRequest(std::bind(&WebDataService::scan_devices, this, _1), AuthenticationPredicates::IS_ADMIN));
_device_data_handler.setMethod(HTTP_POST);
_device_data_handler.setMaxContentLength(256);
server->addHandler(&_device_data_handler);
_write_value_handler.setMethod(HTTP_POST);
_write_value_handler.setMaxContentLength(256);
server->addHandler(&_write_value_handler);
@@ -88,7 +88,6 @@ void WebDataService::core_data(AsyncWebServerRequest * request) {
obj["d"] = emsdevice->device_id(); // deviceid
obj["p"] = emsdevice->product_id(); // productid
obj["v"] = emsdevice->version(); // version
// obj["e"] = emsdevice->count_entities(); // number of entities (device values)
}
}
@@ -103,7 +102,6 @@ void WebDataService::core_data(AsyncWebServerRequest * request) {
obj["d"] = 0; // deviceid
obj["p"] = 0; // productid
obj["v"] = 0; // version
// obj["e"] = EMSESP::webEntityService.count_entities(); // number of entities (device values)
}
root["connected"] = EMSESP::bus_status() != 2;
@@ -171,17 +169,23 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) {
// The unique_id is the unique record ID from the Web table to identify which device to load
// Compresses the JSON using MsgPack https://msgpack.org/index.html
void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant & json) {
if (json.is<JsonObject>()) {
void WebDataService::device_data(AsyncWebServerRequest * request) {
uint8_t id;
if (request->hasParam(F_(id))) {
id = Helpers::atoint(request->getParam(F_(id))->value().c_str()); // get id from url
size_t buffer = EMSESP_JSON_SIZE_XXXXLARGE;
auto * response = new MsgpackAsyncJsonResponse(false, buffer);
// check size
while (!response->getSize()) {
delete response;
buffer -= 1024;
response = new MsgpackAsyncJsonResponse(false, buffer);
}
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->unique_id() == json["id"]) {
if (emsdevice->unique_id() == id) {
// wait max 2.5 sec for updated data (post_send_delay is 2 sec)
for (uint16_t i = 0; i < (emsesp::TxService::POST_SEND_DELAY + 500) && EMSESP::wait_validate(); i++) {
delay(1);
@@ -202,8 +206,9 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant &
return;
}
}
#ifndef EMSESP_STANDALONE
if (json["id"] == 99) {
if (id == 99) {
JsonObject output = response->getRoot();
EMSESP::webEntityService.generate_value_web(output);
response->setLength();
@@ -213,13 +218,11 @@ void WebDataService::device_data(AsyncWebServerRequest * request, JsonVariant &
#endif
}
// invalid but send ok
AsyncWebServerResponse * response = request->beginResponse(200);
// invalid
AsyncWebServerResponse * response = request->beginResponse(400);
request->send(response);
}
// takes a command and its data value from a specific EMS Device, from the Web
// assumes the service has been checked for admin authentication
void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVariant & json) {
if (json.is<JsonObject>()) {
@@ -265,7 +268,7 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar
#endif
}
response->setCode((return_code == CommandRet::OK) ? 200 : 204);
response->setCode((return_code == CommandRet::OK) ? 200 : 400); // bad request
response->setLength();
request->send(response);
return;
@@ -297,14 +300,14 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar
EMSESP::logger().debug("Write command successful");
#endif
}
response->setCode((return_code == CommandRet::OK) ? 200 : 204);
response->setCode((return_code == CommandRet::OK) ? 200 : 400); // bad request
response->setLength();
request->send(response);
return;
}
}
AsyncWebServerResponse * response = request->beginResponse(204); // Write command failed
AsyncWebServerResponse * response = request->beginResponse(400); // bad request
request->send(response);
}
@@ -324,10 +327,11 @@ void WebDataService::write_temperature_sensor(AsyncWebServerRequest * request, J
if (EMSESP::system_.fahrenheit()) {
offset10 = offset / 0.18;
}
ok = EMSESP::temperaturesensor_.update(id, name, offset10);
}
AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 204);
AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 400); // bad request
request->send(response);
}
@@ -347,7 +351,7 @@ void WebDataService::write_analog_sensor(AsyncWebServerRequest * request, JsonVa
ok = EMSESP::analogsensor_.update(gpio, name, offset, factor, uom, type, deleted);
}
AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 204);
AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 400); // bad request
request->send(response);
}

View File

@@ -21,7 +21,6 @@
// GET
#define CORE_DATA_SERVICE_PATH "/rest/coreData"
#define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices"
#define DEVICE_DATA_SERVICE_PATH "/rest/deviceData"
#define SENSOR_DATA_SERVICE_PATH "/rest/sensorData"
@@ -29,6 +28,7 @@
#define WRITE_DEVICE_VALUE_SERVICE_PATH "/rest/writeDeviceValue"
#define WRITE_TEMPERATURE_SENSOR_SERVICE_PATH "/rest/writeTemperatureSensor"
#define WRITE_ANALOG_SENSOR_SERVICE_PATH "/rest/writeAnalogSensor"
#define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices"
namespace emsesp {
@@ -44,15 +44,15 @@ class WebDataService {
// GET
void core_data(AsyncWebServerRequest * request);
void sensor_data(AsyncWebServerRequest * request);
void device_data(AsyncWebServerRequest * request);
// POST
void device_data(AsyncWebServerRequest * request, JsonVariant & json);
void write_device_value(AsyncWebServerRequest * request, JsonVariant & json);
void write_temperature_sensor(AsyncWebServerRequest * request, JsonVariant & json);
void write_analog_sensor(AsyncWebServerRequest * request, JsonVariant & json);
void scan_devices(AsyncWebServerRequest * request);
void scan_devices(AsyncWebServerRequest * request); // command
AsyncCallbackJsonWebHandler _device_data_handler, _write_value_handler, _write_temperature_handler, _write_analog_handler;
AsyncCallbackJsonWebHandler _write_value_handler, _write_temperature_handler, _write_analog_handler;
};
} // namespace emsesp

View File

@@ -297,7 +297,7 @@ void WebEntityService::publish(const bool force) {
if (Mqtt::ha_enabled() && !ha_registered_) {
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/custom_data", Mqtt::base().c_str());
snprintf(stat_t, sizeof(stat_t), "%s/custom_data", Mqtt::basename().c_str());
config["stat_t"] = stat_t;
char val_obj[50];
@@ -348,7 +348,7 @@ void WebEntityService::publish(const bool force) {
}
JsonObject dev = config.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
ids.add(Mqtt::basename());
// add "availability" section
Mqtt::add_avty_to_doc(stat_t, config.as<JsonObject>(), val_cond);

View File

@@ -24,25 +24,16 @@ namespace emsesp {
WebLogService::WebLogService(AsyncWebServer * server, SecurityManager * securityManager)
: events_(EVENT_SOURCE_LOG_PATH)
, setValues_(LOG_SETTINGS_PATH, std::bind(&WebLogService::setValues, this, _1, _2), 256) { // for POSTS
, setValues_(LOG_SETTINGS_PATH, std::bind(&WebLogService::setValues, this, _1, _2), 256) {
events_.setFilter(securityManager->filterRequest(AuthenticationPredicates::IS_ADMIN));
server->addHandler(&events_);
server->on(EVENT_SOURCE_LOG_PATH, HTTP_GET, std::bind(&WebLogService::forbidden, this, _1));
server->on(LOG_SETTINGS_PATH, HTTP_GET, std::bind(&WebLogService::getValues, this, _1)); // get settings
// for bring back the whole log
server->on(FETCH_LOG_PATH, HTTP_GET, std::bind(&WebLogService::fetchLog, this, _1));
// for bring back the whole log - is a command, hence a POST
server->on(FETCH_LOG_PATH, HTTP_POST, std::bind(&WebLogService::fetchLog, this, _1));
// get when page is loaded
server->on(LOG_SETTINGS_PATH, HTTP_GET, std::bind(&WebLogService::getValues, this, _1));
// for setting a level
server->addHandler(&setValues_);
}
void WebLogService::forbidden(AsyncWebServerRequest * request) {
request->send(403);
server->addHandler(&events_);
}
// start the log service with INFO level
@@ -211,6 +202,7 @@ void WebLogService::transmit(const QueuedLogMessage & message) {
}
// send the complete log buffer to the API, not filtering on log level
// done by resetting the pointer
void WebLogService::fetchLog(AsyncWebServerRequest * request) {
log_message_id_tail_ = 0;
request->send(200);
@@ -224,6 +216,7 @@ void WebLogService::setValues(AsyncWebServerRequest * request, JsonVariant & jso
auto && body = json.as<JsonObject>();
// TODO refactor into one load and one save method
uuid::log::Level level = body["level"];
log_level(level);

View File

@@ -58,7 +58,6 @@ class WebLogService : public uuid::log::Handler {
const std::shared_ptr<const uuid::log::Message> content_; // Log message content
};
void forbidden(AsyncWebServerRequest * request);
void transmit(const QueuedLogMessage & message);
void fetchLog(AsyncWebServerRequest * request);
void getValues(AsyncWebServerRequest * request);

View File

@@ -50,7 +50,7 @@ void WebScheduler::read(WebScheduler & webScheduler, JsonObject & root) {
}
}
// call on initialization and also when the Scheduile web page is updated
// call on initialization and also when the Scheduile web page is saved
// this loads the data into the internal class
StateUpdateResult WebScheduler::update(JsonObject & root, WebScheduler & webScheduler) {
#ifdef EMSESP_STANDALONE
@@ -237,7 +237,7 @@ void WebSchedulerService::publish(const bool force) {
if (Mqtt::ha_enabled() && force) {
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/scheduler_data", Mqtt::base().c_str());
snprintf(stat_t, sizeof(stat_t), "%s/scheduler_data", Mqtt::basename().c_str());
config["stat_t"] = stat_t;
char val_obj[50];
@@ -272,7 +272,7 @@ void WebSchedulerService::publish(const bool force) {
JsonObject dev = config.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
ids.add(Mqtt::basename());
// add "availability" section
Mqtt::add_avty_to_doc(stat_t, config.as<JsonObject>(), val_cond);

View File

@@ -26,12 +26,11 @@ using namespace std::placeholders; // for `_1` etc
WebSettingsService::WebSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(WebSettings::read, WebSettings::update, this, server, EMSESP_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE)
, _boardProfileHandler(EMSESP_BOARD_PROFILE_SERVICE_PATH,
securityManager->wrapCallback(std::bind(&WebSettingsService::board_profile, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) {
_boardProfileHandler.setMethod(HTTP_POST);
_boardProfileHandler.setMaxContentLength(256);
server->addHandler(&_boardProfileHandler);
, _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE) {
// GET
server->on(EMSESP_BOARD_PROFILE_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&WebSettingsService::board_profile, this, _1), AuthenticationPredicates::IS_ADMIN));
addUpdateHandler([&](const String & originId) { onUpdate(); }, false);
}
@@ -342,15 +341,16 @@ void WebSettingsService::save() {
}
// build the json profile to send back
void WebSettingsService::board_profile(AsyncWebServerRequest * request, JsonVariant & json) {
if (json.is<JsonObject>()) {
void WebSettingsService::board_profile(AsyncWebServerRequest * request) {
if (request->hasParam("boardProfile")) {
std::string board_profile = request->getParam("boardProfile")->value().c_str();
auto * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_MEDIUM);
JsonObject root = response->getRoot();
if (json.containsKey("board_profile")) {
String board_profile = json["board_profile"];
std::vector<int8_t> data; // led, dallas, rx, tx, button, phy_type, eth_power, eth_phy_addr, eth_clock_mode
(void)System::load_board_profile(data, board_profile.c_str());
(void)System::load_board_profile(data, board_profile);
root["board_profile"] = board_profile;
root["led_gpio"] = data[0];
root["dallas_gpio"] = data[1];
root["rx_gpio"] = data[2];
@@ -365,7 +365,6 @@ void WebSettingsService::board_profile(AsyncWebServerRequest * request, JsonVari
request->send(response);
return;
}
}
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);

View File

@@ -124,9 +124,8 @@ class WebSettingsService : public StatefulService<WebSettings> {
private:
HttpEndpoint<WebSettings> _httpEndpoint;
FSPersistence<WebSettings> _fsPersistence;
AsyncCallbackJsonWebHandler _boardProfileHandler;
void board_profile(AsyncWebServerRequest * request, JsonVariant & json);
void board_profile(AsyncWebServerRequest * request);
void onUpdate();
};