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,57 +12,54 @@ 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]
);
<Layout>
<Routes>
<Route path="/dashboard/*" element={<Dashboard />} />
<Route
path="/settings/*"
element={
<RequireAdmin>
<Settings />
</RequireAdmin>
}
/>
<Route path="/help/*" element={<Help />} />
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 />} />
<Route
path="/settings/*"
element={
<RequireAdmin>
<Settings />
</RequireAdmin>
}
/>
<Route path="/help/*" element={<Help />} />
<Route path="/network/*" element={<NetworkConnection />} />
<Route path="/ap/*" element={<AccessPoint />} />
<Route path="/ntp/*" element={<NetworkTime />} />
<Route path="/mqtt/*" element={<Mqtt />} />
<Route
path="/security/*"
element={
<RequireAdmin>
<Security />
</RequireAdmin>
}
/>
<Route path="/system/*" element={<System />} />
<Route path="/*" element={<Navigate to="/" />} />
</Routes>
</Layout>
);
};
<Route path="/network/*" element={<NetworkConnection />} />
<Route path="/ap/*" element={<AccessPoint />} />
<Route path="/ntp/*" element={<NetworkTime />} />
<Route path="/mqtt/*" element={<Mqtt />} />
<Route
path="/security/*"
element={
<RequireAdmin>
<Security />
</RequireAdmin>
}
/>
<Route path="/system/*" element={<System />} />
<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) {
toast.warn(LL.INVALID_LOGIN());
}
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);
}
if (headers['Content-Type'] !== 'application/json') {
return data;
}
}
return JSON.stringify(data);
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);
}
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);
// }
}
});
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();
setMe(AuthenticationApi.decodeMeJWT(accessToken));
setInitialized(true);
} catch (error) {
setMe(undefined);
setInitialized(true);
}
await verifyAuthorization()
.then(() => {
setMe(AuthenticationApi.decodeMeJWT(accessToken));
setInitialized(true);
})
.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,17 +28,27 @@ 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>({
read: APApi.readAPSettings,
update: APApi.updateAPSettings
});
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<APSettings>({
read: APApi.readAPSettings,
update: APApi.updateAPSettings
});
const { LL } = useI18nContext();
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,17 +22,27 @@ 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>({
read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings
});
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<MqttSettings>({
read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings
});
const { LL } = useI18nContext();
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();
setRestarting(true);
} catch (error) {
toast.error(LL.PROBLEM_UPDATING());
}
await restartCommand().catch((error) => {
toast.error(error.message);
});
setRestarting(true);
};
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) {
const completedPollCount = pollCount.current + 1;
if (completedPollCount < NUM_POLLS) {
pollCount.current = completedPollCount;
setTimeout(pollNetworkList, POLLING_FREQUENCY);
} else {
finishedWithError(LL.PROBLEM_LOADING());
}
onSuccessNetworkList((event) => {
if (!event.data) {
const completedPollCount = pollCount.current + 1;
if (completedPollCount < NUM_POLLS) {
pollCount.current = completedPollCount;
setTimeout(getNetworkList, POLLING_FREQUENCY);
} 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());
setErrorMessage(LL.PROBLEM_LOADING());
pollCount.current = 0;
}
}
}, [finishedWithError, LL]);
});
const startNetworkScan = useCallback(async () => {
onCompleteScanNetworks(() => {
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]);
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>({
read: NTPApi.readNTPSettings,
update: NTPApi.updateNTPSettings
});
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(() => {
toast.error(LL.PROBLEM_UPDATING());
})
.finally(() => {
setProcessing(false);
});
toast.success(LL.TIME_SET());
setSettingTime(false);
await loadData();
} catch (error) {
toast.error(extractErrorMessage(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>({
read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings
});
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>({
read: SystemApi.readOTASettings,
update: SystemApi.updateOTASettings
});
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,17 +207,16 @@ const SystemLog: FC = () => {
p: 1
}}
>
{logEntries &&
logEntries.events.map((e) => (
<LogEntryLine key={e.i}>
<span>{e.t}</span>
{data.compact && <span>{paddedLevelLabel(e.l)} </span>}
{!data.compact && <span>{paddedLevelLabel(e.l)}&nbsp;</span>}
<span>{paddedIDLabel(e.i)} </span>
<span>{paddedNameLabel(e.n)} </span>
<span>{e.m}</span>
</LogEntryLine>
))}
{logEntries.map((e) => (
<LogEntryLine key={e.i}>
<span>{e.t}</span>
{data.compact && <span>{paddedLevelLabel(e.l)} </span>}
{!data.compact && <span>{paddedLevelLabel(e.l)}&nbsp;</span>}
<span>{paddedIDLabel(e.i)} </span>
<span>{paddedNameLabel(e.n)} </span>
<span>{e.m}</span>
</LogEntryLine>
))}
</Box>
</>
);

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
});
});
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: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
immediate: false
});
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 {
setConfirmRestart(false);
setProcessing(false);
}
})
.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();
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_LOADING()));
} finally {
setConfirmRestart(false);
setProcessing(false);
}
await partitionCommand()
.then(() => {
setRestarting(true);
})
.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>({
connected: true,
devices: []
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 {
setDeviceValueDialogOpen(false);
await fetchDeviceData(selectedDeviceID);
setSelectedDeviceValue(undefined);
}
})
.catch((error) => {
toast.error(error.message);
})
.finally(async () => {
setDeviceValueDialogOpen(false);
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">
{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>
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ mx: 1 }}>
{coreData.devices[deviceIndex].n}
</Typography>
<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 {
setTemperatureDialogOpen(false);
setSelectedTemperatureSensor(undefined);
await fetchSensorData();
}
})
.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,32 +273,27 @@ const DashboardSensors: FC = () => {
};
const onAnalogDialogSave = async (as: AnalogSensor) => {
try {
const response = await EMSESP.writeAnalogSensor({
id: as.id,
gpio: as.g,
name: as.n,
offset: as.o,
factor: as.f,
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 {
await writeAnalogSensor({
id: as.id,
gpio: as.g,
name: as.n,
offset: as.o,
factor: as.f,
uom: as.u,
type: as.t,
deleted: as.d
})
.then(() => {
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setAnalogDialogOpen(false);
setSelectedAnalogSensor(undefined);
await fetchSensorData();
}
})
.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();
toast.info(LL.SCANNING() + '...');
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setConfirmScan(false);
}
await scanDevices()
.then(() => {
toast.info(LL.SCANNING() + '...');
})
.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
});
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
} finally {
setProcessingBoard(false);
}
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);
});
};
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();
setRestarting(true);
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
await restartCommand().catch((error) => {
toast.error(error.message);
});
setRestarting(true);
};
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,72 +316,56 @@ 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) {
setRestartNeeded(true);
} else {
toast.error(LL.PROBLEM_UPDATING());
await writeCustomEntities({ id: devices?.devices[selectedDevice].i, entity_ids: masked_entities }).catch(
(error) => {
if (error.message === 'Reboot required') {
setRestartNeeded(true);
} else {
toast.error(error.message);
}
}
} catch (error) {
toast.error(extractErrorMessage(error, LL.PROBLEM_UPDATING()));
}
);
setOriginalSettings(deviceEntities);
}
};
const renderDeviceList = () => {
if (!devices) {
return <FormLoader errorMessage={errorMessage} />;
}
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
<Typography variant="body2" mt={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography>
</Box>
<TextField
name="device"
label={LL.EMS_DEVICE()}
variant="outlined"
fullWidth
value={selectedDevice}
disabled={numChanges !== 0}
onChange={changeSelectedDevice}
margin="normal"
select
>
<MenuItem disabled key={0} value={-1}>
{LL.SELECT_DEVICE()}...
const renderDeviceList = () => (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
<Typography variant="body2" mt={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography>
</Box>
<TextField
name="device"
label={LL.EMS_DEVICE()}
variant="outlined"
fullWidth
value={selectedDevice}
disabled={numChanges !== 0}
onChange={changeSelectedDevice}
margin="normal"
select
>
<MenuItem disabled key={0} value={-1}>
{LL.SELECT_DEVICE()}...
</MenuItem>
{devices.devices.map((device: DeviceShort, index) => (
<MenuItem key={index} value={index}>
{device.s}
</MenuItem>
{devices.devices.map((device: DeviceShort, index) => (
<MenuItem key={index} value={index}>
{device.s}
</MenuItem>
))}
</TextField>
</>
);
};
))}
</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,62 +109,32 @@ 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({
entities: entities
.filter((ei) => !ei.deleted)
.map((condensed_ei) => ({
id: condensed_ei.id,
name: condensed_ei.name,
device_id: condensed_ei.device_id,
type_id: condensed_ei.type_id,
offset: condensed_ei.offset,
factor: condensed_ei.factor,
uom: condensed_ei.uom,
writeable: condensed_ei.writeable,
value_type: condensed_ei.value_type
}))
});
if (response.status === 200) {
toast.success(LL.ENTITIES_UPDATED());
} else {
toast.error(LL.PROBLEM_UPDATING());
}
await writeEntities({
entities: entities
.filter((ei) => !ei.deleted)
.map((condensed_ei) => ({
id: condensed_ei.id,
name: condensed_ei.name,
device_id: condensed_ei.device_id,
type_id: condensed_ei.type_id,
offset: condensed_ei.offset,
factor: condensed_ei.factor,
uom: condensed_ei.uom,
writeable: condensed_ei.writeable,
value_type: condensed_ei.value_type
}))
})
.then(() => {
toast.success(LL.ENTITIES_UPDATED());
})
.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,63 +109,30 @@ 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({
schedule: schedule
.filter((si) => !si.deleted)
.map((condensed_si) => ({
id: condensed_si.id,
active: condensed_si.active,
flags: condensed_si.flags,
time: condensed_si.time,
cmd: condensed_si.cmd,
value: condensed_si.value,
name: condensed_si.name
}))
});
if (response.status === 200) {
toast.success(LL.SCHEDULE_UPDATED());
} else {
toast.error(LL.PROBLEM_UPDATING());
}
await writeSchedule({
schedule: schedule
.filter((si) => !si.deleted)
.map((condensed_si) => ({
id: condensed_si.id,
active: condensed_si.active,
flags: condensed_si.flags,
time: condensed_si.time,
cmd: condensed_si.cmd,
value: condensed_si.value,
name: condensed_si.name
}))
})
.then(() => {
toast.success(LL.SCHEDULE_UPDATED());
})
.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);
await readData().catch((error) => {
toast.error(error.message);
setErrorMessage(error.message);
});
};
const saveData = async () => {
if (!data) {
return;
}
}, [read, LL]);
const save = useCallback(
async (toSave: D) => {
if (!update) {
return;
setRestartNeeded(false);
setErrorMessage(undefined);
await writeData(data).catch((error) => {
if (error.message === 'Reboot required') {
setRestartNeeded(true);
} else {
toast.error(error.message);
setErrorMessage(error.message);
}
setSaving(true);
setRestartNeeded(false);
setErrorMessage(undefined);
try {
const response = await update(toSave);
setOrigData(response.data);
setData(response.data);
if (response.status === 202) {
setRestartNeeded(true);
} else {
toast.success(LL.UPDATED_OF(LL.SETTINGS()));
}
} 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

@@ -20,188 +20,198 @@ 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) {
int i = stringCompare(adaptString(rhs), adaptString(lhs, n));
if (i < 0)
return COMPARE_RESULT_GREATER;
else if (i > 0)
return COMPARE_RESULT_LESS;
else
return COMPARE_RESULT_EQUAL;
}
CompareResult visitString(const char * lhs, size_t n) {
int i = stringCompare(adaptString(rhs), adaptString(lhs, n));
if (i < 0)
return COMPARE_RESULT_GREATER;
else if (i > 0)
return COMPARE_RESULT_LESS;
else
return COMPARE_RESULT_EQUAL;
}
CompareResult visitNull() {
if (adaptString(rhs).isNull())
return COMPARE_RESULT_EQUAL;
else
return COMPARE_RESULT_DIFFER;
}
CompareResult visitNull() {
if (adaptString(rhs).isNull())
return COMPARE_RESULT_EQUAL;
else
return COMPARE_RESULT_DIFFER;
}
};
template <typename T>
struct Comparer<T, typename enable_if<is_integral<T>::value ||
is_floating_point<T>::value>::type>
: ComparerBase {
T rhs;
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);
}
CompareResult visitFloat(JsonFloat lhs) {
return arithmeticCompare(lhs, rhs);
}
CompareResult visitSignedInteger(JsonInteger lhs) {
return arithmeticCompare(lhs, rhs);
}
CompareResult visitSignedInteger(JsonInteger lhs) {
return arithmeticCompare(lhs, rhs);
}
CompareResult visitUnsignedInteger(JsonUInt lhs) {
return arithmeticCompare(lhs, rhs);
}
CompareResult visitUnsignedInteger(JsonUInt lhs) {
return arithmeticCompare(lhs, rhs);
}
CompareResult visitBoolean(bool lhs) {
return visitUnsignedInteger(static_cast<JsonUInt>(lhs));
}
CompareResult visitBoolean(bool lhs) {
return visitUnsignedInteger(static_cast<JsonUInt>(lhs));
}
};
struct NullComparer : ComparerBase {
CompareResult visitNull() {
return COMPARE_RESULT_EQUAL;
}
CompareResult visitNull() {
return COMPARE_RESULT_EQUAL;
}
};
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) {
if (JsonArrayConst(&lhs) == JsonArrayConst(_rhs))
return COMPARE_RESULT_EQUAL;
else
return COMPARE_RESULT_DIFFER;
}
CompareResult visitArray(const CollectionData & lhs) {
if (JsonArrayConst(&lhs) == JsonArrayConst(_rhs))
return COMPARE_RESULT_EQUAL;
else
return COMPARE_RESULT_DIFFER;
}
};
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) {
if (JsonObjectConst(&lhs) == JsonObjectConst(_rhs))
return COMPARE_RESULT_EQUAL;
else
return COMPARE_RESULT_DIFFER;
}
CompareResult visitObject(const CollectionData & lhs) {
if (JsonObjectConst(&lhs) == JsonObjectConst(_rhs))
return COMPARE_RESULT_EQUAL;
else
return COMPARE_RESULT_DIFFER;
}
};
struct RawComparer : ComparerBase {
const char* _rhsData;
size_t _rhsSize;
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) {
size_t size = _rhsSize < lhsSize ? _rhsSize : lhsSize;
int n = memcmp(lhsData, _rhsData, size);
if (n < 0)
return COMPARE_RESULT_LESS;
else if (n > 0)
return COMPARE_RESULT_GREATER;
else
return COMPARE_RESULT_EQUAL;
}
CompareResult visitRawJson(const char * lhsData, size_t lhsSize) {
size_t size = _rhsSize < lhsSize ? _rhsSize : lhsSize;
int n = memcmp(lhsData, _rhsData, size);
if (n < 0)
return COMPARE_RESULT_LESS;
else if (n > 0)
return COMPARE_RESULT_GREATER;
else
return COMPARE_RESULT_EQUAL;
}
};
struct VariantComparer : ComparerBase {
const VariantData* rhs;
const VariantData * rhs;
explicit VariantComparer(const VariantData* value) : rhs(value) {}
CompareResult visitArray(const CollectionData& lhs) {
ArrayComparer comparer(lhs);
return accept(comparer);
}
CompareResult visitObject(const CollectionData& lhs) {
ObjectComparer comparer(lhs);
return accept(comparer);
}
CompareResult visitFloat(JsonFloat lhs) {
Comparer<JsonFloat> comparer(lhs);
return accept(comparer);
}
CompareResult visitString(const char* lhs, size_t) {
Comparer<const char*> comparer(lhs);
return accept(comparer);
}
CompareResult visitRawJson(const char* lhsData, size_t lhsSize) {
RawComparer comparer(lhsData, lhsSize);
return accept(comparer);
}
CompareResult visitSignedInteger(JsonInteger lhs) {
Comparer<JsonInteger> comparer(lhs);
return accept(comparer);
}
CompareResult visitUnsignedInteger(JsonUInt lhs) {
Comparer<JsonUInt> comparer(lhs);
return accept(comparer);
}
CompareResult visitBoolean(bool lhs) {
Comparer<bool> comparer(lhs);
return accept(comparer);
}
CompareResult visitNull() {
NullComparer comparer;
return accept(comparer);
}
private:
template <typename TComparer>
CompareResult accept(TComparer& comparer) {
CompareResult reversedResult = variantAccept(rhs, comparer);
switch (reversedResult) {
case COMPARE_RESULT_GREATER:
return COMPARE_RESULT_LESS;
case COMPARE_RESULT_LESS:
return COMPARE_RESULT_GREATER;
default:
return reversedResult;
explicit VariantComparer(const VariantData * value)
: rhs(value) {
}
CompareResult visitArray(const CollectionData & lhs) {
ArrayComparer comparer(lhs);
return accept(comparer);
}
CompareResult visitObject(const CollectionData & lhs) {
ObjectComparer comparer(lhs);
return accept(comparer);
}
CompareResult visitFloat(JsonFloat lhs) {
Comparer<JsonFloat> comparer(lhs);
return accept(comparer);
}
CompareResult visitString(const char * lhs, size_t) {
Comparer<const char *> comparer(lhs);
return accept(comparer);
}
CompareResult visitRawJson(const char * lhsData, size_t lhsSize) {
RawComparer comparer(lhsData, lhsSize);
return accept(comparer);
}
CompareResult visitSignedInteger(JsonInteger lhs) {
Comparer<JsonInteger> comparer(lhs);
return accept(comparer);
}
CompareResult visitUnsignedInteger(JsonUInt lhs) {
Comparer<JsonUInt> comparer(lhs);
return accept(comparer);
}
CompareResult visitBoolean(bool lhs) {
Comparer<bool> comparer(lhs);
return accept(comparer);
}
CompareResult visitNull() {
NullComparer comparer;
return accept(comparer);
}
private:
template <typename TComparer>
CompareResult accept(TComparer & comparer) {
CompareResult reversedResult = variantAccept(rhs, comparer);
switch (reversedResult) {
case COMPARE_RESULT_GREATER:
return COMPARE_RESULT_LESS;
case COMPARE_RESULT_LESS:
return COMPARE_RESULT_GREATER;
default:
return reversedResult;
}
}
}
};
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) {
Comparer<T> comparer(rhs);
return variantAccept(VariantAttorney::getData(lhs), comparer);
CompareResult compare(ArduinoJson::JsonVariantConst lhs, const T & rhs) {
Comparer<T> comparer(rhs);
return variantAccept(VariantAttorney::getData(lhs), comparer);
}
ARDUINOJSON_END_PRIVATE_NAMESPACE

File diff suppressed because it is too large Load Diff

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,12 +42,12 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
return;
} else {
md5[0] = '\0';
return; // not support file type
return; // unsupported file type
}
if (is_firmware) {
// Check firmware header, 0xE9 magic offset 0 indicates esp bin, chip offset 12: esp32:0, S2:2, C3:5
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
#if CONFIG_IDF_TARGET_ESP32 // ESP32/PICO-D4
if (len > 12 && (data[0] != 0xE9 || data[12] != 0)) {
handleError(request, 503); // service unavailable
return;
@@ -76,7 +76,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri
}
request->onDisconnect(UploadFileService::handleEarlyDisconnect); // success, let's make sure we end the update if the client hangs up
} else {
handleError(request, 507); // failed to begin, send an error response Insufficient Storage
handleError(request, 507); // failed to begin, send an error response Insufficient Storage
return;
}
} else {
@@ -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

@@ -1448,9 +1448,10 @@ void EMSESP::start() {
bool factory_settings = false;
#endif
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
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,17 +1486,12 @@ 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
shower_.start(); // initialize shower timer and shower alert
temperaturesensor_.start(); // Temperature external sensors
analogsensor_.start(); // Analog external sensors
webLogService.start(); // apply settings to weblog service
mqtt_.start(); // mqtt init
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
webLogService.start(); // apply settings to weblog service
// Load our library of known devices into stack mem. Names are stored in Flash memory
device_library_ = {

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

@@ -1,7 +1,7 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2023 Paul Derbyshire
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -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,29 +341,29 @@ 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());
root["led_gpio"] = data[0];
root["dallas_gpio"] = data[1];
root["rx_gpio"] = data[2];
root["tx_gpio"] = data[3];
root["pbutton_gpio"] = data[4];
root["phy_type"] = data[5];
root["eth_power"] = data[6];
root["eth_phy_addr"] = data[7];
root["eth_clock_mode"] = data[8];
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);
root["board_profile"] = board_profile;
root["led_gpio"] = data[0];
root["dallas_gpio"] = data[1];
root["rx_gpio"] = data[2];
root["tx_gpio"] = data[3];
root["pbutton_gpio"] = data[4];
root["phy_type"] = data[5];
root["eth_power"] = data[6];
root["eth_phy_addr"] = data[7];
root["eth_clock_mode"] = data[8];
response->setLength();
request->send(response);
return;
}
response->setLength();
request->send(response);
return;
}
AsyncWebServerResponse * response = request->beginResponse(200);

View File

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