mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Merge branch 'dev2' of https://github.com/emsesp/EMS-ESP32 into dev2
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
2
interface/.env.development
Normal file
2
interface/.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_ALOVA_TIPS=0
|
||||
REACT_APP_ALOVA_TIPS=0
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || {})
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export { default as SingleUpload } from './SingleUpload';
|
||||
export { default as useFileUpload } from './useFileUpload';
|
||||
|
||||
@@ -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;
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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" />;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = () => (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()}…
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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)} </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)} </span>}
|
||||
<span>{paddedIDLabel(e.i)} </span>
|
||||
<span>{paddedNameLabel(e.n)} </span>
|
||||
<span>{e.m}</span>
|
||||
</LogEntryLine>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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é',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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|}}',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()}
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<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()}
|
||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}
|
||||
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -215,7 +215,7 @@ const SettingsSchedulerDialog = ({
|
||||
/>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.VALUE(0)}
|
||||
label={LL.VALUE(1)}
|
||||
multiline
|
||||
margin="normal"
|
||||
fullWidth
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -42,10 +42,6 @@ export interface LogEntry {
|
||||
m: string;
|
||||
}
|
||||
|
||||
export interface LogEntries {
|
||||
events: LogEntry[];
|
||||
}
|
||||
|
||||
export interface LogSettings {
|
||||
level: number;
|
||||
max_messages: number;
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './binding';
|
||||
export * from './endpoints';
|
||||
export * from './route';
|
||||
export * from './submit';
|
||||
export * from './time';
|
||||
|
||||
@@ -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,
|
||||
|
||||
1768
interface/yarn.lock
1768
interface/yarn.lock
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
1
mock-api/uploads/README.md
Normal file
1
mock-api/uploads/README.md
Normal file
@@ -0,0 +1 @@
|
||||
leave empty
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_ = {
|
||||
|
||||
17
src/mqtt.cpp
17
src/mqtt.cpp
@@ -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);
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define EMSESP_APP_VERSION "3.6.0-dev.12"
|
||||
#define EMSESP_APP_VERSION "3.6.0-dev.13b"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user