Merge pull request #1931 from proddy/dev

Web changes #1920
This commit is contained in:
Proddy
2024-08-16 13:49:32 +02:00
committed by GitHub
103 changed files with 5342 additions and 5640 deletions

View File

@@ -21,6 +21,7 @@ export default tseslint.config(
{
rules: {
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-misused-promises': [
'error',

View File

@@ -8,42 +8,33 @@
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"build-hosted": "typesafe-i18n --no-watch && vite build --mode hosted",
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"npm:mock-rest\" \"vite preview\"",
"mock-rest": "bun --watch ../mock-api/rest_server.ts",
"mock-es": "bun --watch ../mock-api/es_server.ts",
"mock-upload": "bun --watch ../mock-api/upload_server.ts",
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-rest\" \"npm:mock-es\" \"npm:mock-upload\" \"vite\"",
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"npm:mock-rest\" \"vite\"",
"typesafe-i18n": "typesafe-i18n --no-watch",
"webUI": "node progmem-generator.js",
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
"lint": "eslint . --fix"
},
"dependencies": {
"@alova/adapter-xhr": "^1.0.6",
"@alova/scene-react": "^1.6.2",
"@alova/adapter-xhr": "2.0.4",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@mui/icons-material": "^5.16.6",
"@mui/material": "^5.16.6",
"@mui/icons-material": "^5.16.7",
"@mui/material": "^5.16.7",
"@table-library/react-table-library": "4.1.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.0.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-router-dom": "^5.3.3",
"alova": "^2.21.4",
"alova": "3.0.9",
"async-validator": "^4.2.5",
"history": "^5.3.0",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
"mime-types": "^2.1.35",
"react": "latest",
"react-dom": "latest",
"react-dropzone": "^14.2.3",
"react-icons": "^5.2.1",
"preact": "^10.23.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-router-dom": "^6.26.0",
"react-toastify": "^10.0.5",
"typesafe-i18n": "^5.26.2",
@@ -51,22 +42,27 @@
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@eslint/js": "^9.8.0",
"@eslint/js": "^9.9.0",
"@preact/compat": "^17.1.2",
"@preact/preset-vite": "^2.9.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/babel__core": "^7",
"@types/formidable": "^3",
"@types/node": "^22.3.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-router-dom": "^5.3.3",
"concurrently": "^8.2.2",
"eslint": "^9.8.0",
"eslint": "^9.9.0",
"eslint-config-prettier": "^9.1.0",
"preact": "^10.23.1",
"formidable": "^3.5.1",
"prettier": "^3.3.3",
"rollup-plugin-visualizer": "^5.12.0",
"terser": "^5.31.3",
"typescript-eslint": "8.0.0",
"vite": "^5.3.5",
"terser": "^5.31.6",
"typescript-eslint": "8.1.0",
"vite": "^5.4.1",
"vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^4.3.2"
"vite-tsconfig-paths": "^5.0.1"
},
"packageManager": "yarn@4.4.0"
}

View File

@@ -1,5 +1,6 @@
/*
* Uses font-size 400 (normal) only and Latin (plus extra unicode chars) to keep flash memory to a minimum
* Uses font-weight 400 (normal) only, no bold, and Latin with a few extra unicode chars.
* This is to keep flash memory to a minimum
* View fonts on https://fonts.google.com/
* Download woff2 using e.g. https://fonts.googleapis.com/css2?family=Lato or https://fonts.googleapis.com/css2?family=Roboto
*/

View File

@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
import type { FC } from 'react';
import { Slide, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';
@@ -12,7 +11,7 @@ import { localStorageDetector } from 'typesafe-i18n/detectors';
const detectedLocale = detectLocale(localStorageDetector);
const App: FC = () => {
const App = () => {
const [wasLoaded, setWasLoaded] = useState(false);
useEffect(() => {

View File

@@ -1,6 +1,5 @@
import { useContext, useEffect } from 'react';
import type { FC } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import { toast } from 'react-toastify';
import AuthenticatedRouting from 'AuthenticatedRouting';
@@ -14,7 +13,7 @@ interface SecurityRedirectProps {
signOut?: boolean;
}
const RootRedirect: FC<SecurityRedirectProps> = ({ message, signOut }) => {
const RootRedirect = ({ message, signOut }: SecurityRedirectProps) => {
const authenticationContext = useContext(AuthenticationContext);
useEffect(() => {
signOut && authenticationContext.signOut(false);
@@ -23,26 +22,11 @@ const RootRedirect: FC<SecurityRedirectProps> = ({ message, signOut }) => {
return <Navigate to="/" />;
};
export const RemoveTrailingSlashes = () => {
const location = useLocation();
return (
location.pathname.match('/.*/$') && (
<Navigate
to={{
pathname: location.pathname.replace(/\/+$/, ''),
search: location.search
}}
/>
)
);
};
const AppRouting: FC = () => {
const AppRouting = () => {
const { LL } = useI18nContext();
return (
<Authentication>
<RemoveTrailingSlashes />
<Routes>
<Route
path="/unauthorized"

View File

@@ -1,18 +1,19 @@
import { type FC, useContext } from 'react';
import { useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import CustomEntities from 'app/main/CustomEntities';
import Customization from 'app/main/Customization';
import Customizations from 'app/main/Customizations';
import Devices from 'app/main/Devices';
import Help from 'app/main/Help';
import Modules from 'app/main/Modules';
import Scheduler from 'app/main/Scheduler';
import Sensors from 'app/main/Sensors';
import APSettings from 'app/settings/APSettings';
import ApplicationSettings from 'app/settings/ApplicationSettings';
import DownloadUpload from 'app/settings/DownloadUpload';
import MqttSettings from 'app/settings/MqttSettings';
import NTPSettings from 'app/settings/NTPSettings';
import Settings from 'app/settings/Settings';
import UploadDownload from 'app/settings/UploadDownload';
import Network from 'app/settings/network/Network';
import Security from 'app/settings/security/Security';
import APStatus from 'app/status/APStatus';
@@ -26,9 +27,7 @@ import SystemLog from 'app/status/SystemLog';
import { Layout } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import Help from './app/main/Help';
const AuthenticatedRouting: FC = () => {
const AuthenticatedRouting = () => {
const { me } = useContext(AuthenticatedContext);
return (
<Layout>
@@ -55,12 +54,12 @@ const AuthenticatedRouting: FC = () => {
<Route path="/settings/ntp" element={<NTPSettings />} />
<Route path="/settings/ap" element={<APSettings />} />
<Route path="/settings/modules" element={<Modules />} />
<Route path="/settings/upload" element={<UploadDownload />} />
<Route path="/settings/upload" element={<DownloadUpload />} />
<Route path="/settings/network/*" element={<Network />} />
<Route path="/settings/security/*" element={<Security />} />
<Route path="/customizations" element={<Customization />} />
<Route path="/customizations" element={<Customizations />} />
<Route path="/scheduler" element={<Scheduler />} />
<Route path="/customentities" element={<CustomEntities />} />
</>

View File

@@ -1,14 +1,11 @@
import { useContext, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import ForwardIcon from '@mui/icons-material/Forward';
import { Box, Button, Paper, Typography } from '@mui/material';
import * as AuthenticationApi from 'api/authentication';
import { PROJECT_NAME } from 'api/env';
import { useRequest } from 'alova';
import * as AuthenticationApi from 'components/routing/authentication';
import { useRequest } from 'alova/client';
import type { ValidateFieldsError } from 'async-validator';
import {
LanguageSelector,
@@ -16,12 +13,13 @@ import {
ValidatedTextField
} from 'components';
import { AuthenticationContext } from 'contexts/authentication';
import { PROJECT_NAME } from 'env';
import { useI18nContext } from 'i18n/i18n-react';
import type { SignInRequest } from 'types';
import { onEnterCallback, updateValue } from 'utils';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
const SignIn: FC = () => {
const SignIn = () => {
const authenticationContext = useContext(AuthenticationContext);
const { LL } = useI18nContext();
@@ -33,14 +31,12 @@ const SignIn: FC = () => {
const [processing, setProcessing] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { send: callSignIn, onSuccess } = useRequest(
const { send: callSignIn } = useRequest(
(request: SignInRequest) => AuthenticationApi.signIn(request),
{
immediate: false
}
);
onSuccess((response) => {
).onSuccess((response) => {
if (response.data) {
authenticationContext.signIn(response.data.access_token);
}
@@ -113,6 +109,10 @@ const SignIn: FC = () => {
onChange={updateLoginRequestValue}
margin="normal"
variant="outlined"
inputProps={{
autoCapitalize: 'none',
autoCorrect: 'off'
}}
/>
<ValidatedPasswordField
fieldErrors={fieldErrors}

View File

@@ -17,7 +17,7 @@ import type {
Settings,
WriteAnalogSensor,
WriteTemperatureSensor
} from './types';
} from '../app/main/types';
// DashboardDevices
export const readCoreData = () => alovaInstance.Get<CoreData>(`/rest/coreData`);
@@ -65,7 +65,7 @@ export const readDeviceEntities = (id: number) =>
alovaInstance.Get<DeviceEntity[]>(`/rest/deviceEntities`, {
params: { id },
responseType: 'arraybuffer',
transformData(data) {
transform(data) {
return (data as DeviceEntity[]).map((de: DeviceEntity) => ({
...de,
o_m: de.m,
@@ -88,8 +88,7 @@ export const writeDeviceName = (data: { id: number; name: string }) =>
// SettingsScheduler
export const readSchedule = () =>
alovaInstance.Get<ScheduleItem[]>('/rest/schedule', {
name: 'schedule',
transformData(data) {
transform(data) {
return (data as Schedule).schedule.map((si: ScheduleItem) => ({
...si,
o_id: si.id,
@@ -109,8 +108,7 @@ export const writeSchedule = (data: Schedule) =>
// Modules
export const readModules = () =>
alovaInstance.Get<ModuleItem[]>('/rest/modules', {
name: 'modules',
transformData(data) {
transform(data) {
return (data as Modules).modules.map((mi: ModuleItem) => ({
...mi,
o_enabled: mi.enabled,
@@ -127,8 +125,7 @@ export const writeModules = (data: {
// SettingsEntities
export const readCustomEntities = () =>
alovaInstance.Get<EntityItem[]>('/rest/customEntities', {
name: 'entities',
transformData(data) {
transform(data) {
return (data as Entities).entities.map((ei: EntityItem) => ({
...ei,
o_id: ei.id,

View File

@@ -1,19 +1,19 @@
import { xhrRequestAdapter } from '@alova/adapter-xhr';
import { type AlovaXHRResponse, xhrRequestAdapter } from '@alova/adapter-xhr';
import { createAlova } from 'alova';
import ReactHook from 'alova/react';
import { unpack } from '../api/unpack';
import { unpack } from './unpack';
export const ACCESS_TOKEN = 'access_token';
export const alovaInstance = createAlova({
statesHook: ReactHook,
timeout: 3000, // 3 seconds but throwing a timeout error
localCache: null,
// localCache: {
timeout: 3000, // 3 seconds before throwing a timeout error
cacheFor: null, // disable cache
// cacheFor: {
// GET: {
// mode: 'placeholder', // see https://alova.js.org/learning/response-cache/#cache-replaceholder-mode
// expire: 2000
// mode: 'memory',
// expire: 60 * 10 * 1000 // 60 seconds in cache
// }
// },
requestAdapter: xhrRequestAdapter(),
@@ -22,10 +22,15 @@ export const alovaInstance = createAlova({
method.config.headers.Authorization =
'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
}
// for simualting vrey slow networks
// return new Promise((resolve) => {
// const random = 3000 + Math.random() * 2000;
// setTimeout(resolve, Math.floor(random));
// });
},
responded: {
onSuccess: async (response) => {
onSuccess: async (response: AlovaXHRResponse) => {
// if (response.status === 202) {
// throw new Error('Wait'); // wifi scan in progress
// } else

View File

@@ -7,12 +7,9 @@ export const readNetworkStatus = () =>
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
export const listNetworks = () =>
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
name: 'listNetworks',
timeout: 20000 // timeout 20 seconds
});
export const readNetworkSettings = () =>
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings', {
name: 'networkSettings'
});
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings');
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);

View File

@@ -4,10 +4,9 @@ import { alovaInstance } from './endpoints';
export const readNTPStatus = () =>
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
export const readNTPSettings = () =>
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {
name: 'ntpSettings'
});
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {});
export const updateNTPSettings = (data: NTPSettingsType) =>
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);

View File

@@ -1,8 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import type { HardwareStatus, LogSettings, SystemStatus } from 'types';
import { alovaInstance, alovaInstanceGH } from './endpoints';
@@ -31,13 +26,13 @@ export const fetchLogES = () => alovaInstance.Get('/es/log');
// Get versions from github
export const getStableVersion = () =>
alovaInstanceGH.Get('latest', {
transformData(response) {
transform(response: { data: { name: string } }) {
return response.data.name.substring(1);
}
});
export const getDevVersion = () =>
alovaInstanceGH.Get('tags/latest', {
transformData(response) {
transform(response: { data: { name: string } }) {
return response.data.name.split(/\s+/).splice(-1)[0].substring(1);
}
});

View File

@@ -1,12 +1,10 @@
import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import RefreshIcon from '@mui/icons-material/Refresh';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Typography } from '@mui/material';
@@ -20,7 +18,7 @@ import {
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { updateState, useRequest } from 'alova';
import { updateState, useAutoRequest, useRequest } from 'alova/client';
import {
BlockNavigation,
ButtonRow,
@@ -30,13 +28,13 @@ import {
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import { readCustomEntities, writeCustomEntities } from '../../api/app';
import SettingsCustomEntitiesDialog from './CustomEntitiesDialog';
import { DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import type { Entities, EntityItem } from './types';
import { entityItemValidation } from './validators';
const CustomEntities: FC = () => {
const CustomEntities = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
@@ -50,13 +48,13 @@ const CustomEntities: FC = () => {
data: entities,
send: fetchEntities,
error
} = useRequest(EMSESP.readCustomEntities, {
} = useAutoRequest(readCustomEntities, {
initialData: [],
force: true
pollingTime: 2000
});
const { send: writeEntities } = useRequest(
(data: Entities) => EMSESP.writeCustomEntities(data),
(data: Entities) => writeCustomEntities(data),
{ immediate: false }
);
@@ -182,8 +180,7 @@ const CustomEntities: FC = () => {
const onDialogSave = (updatedItem: EntityItem) => {
setDialogOpen(false);
updateState('entities', (data: EntityItem[]) => {
void updateState(readCustomEntities(), (data: EntityItem[]) => {
const new_data = creating
? [
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
@@ -327,15 +324,6 @@ const CustomEntities: FC = () => {
)}
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={fetchEntities}
>
{LL.REFRESH()}
</Button>
<Button
startIcon={<AddIcon />}
variant="outlined"
@@ -344,7 +332,6 @@ const CustomEntities: FC = () => {
>
{LL.ADD(0)}
</Button>
</ButtonRow>
</Box>
</Box>
</SectionContent>

View File

@@ -291,7 +291,11 @@ const CustomEntitiesDialog = ({
fullWidth
margin="normal"
type="number"
inputProps={{ min: '1', max: String(256 - editItem.offset), step: '1' }}
inputProps={{
min: '1',
max: String(256 - editItem.offset),
step: '1'
}}
/>
</Grid>
)}

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { useBlocker, useLocation } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -27,7 +26,7 @@ import {
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
import { restart } from 'api/system';
import {
Body,
@@ -40,7 +39,7 @@ import {
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { useRequest } from 'alova/client';
import RestartMonitor from 'app/status/RestartMonitor';
import {
BlockNavigation,
@@ -51,8 +50,14 @@ import {
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import SettingsCustomizationDialog from './CustomizationDialog';
import {
readDeviceEntities,
readDevices,
resetCustomizations,
writeCustomizationEntities,
writeDeviceName
} from '../../api/app';
import SettingsCustomizationsDialog from './CustomizationsDialog';
import EntityMaskToggle from './EntityMaskToggle';
import OptionIcon from './OptionIcon';
import { DeviceEntityMask } from './types';
@@ -60,7 +65,7 @@ import type { DeviceEntity, DeviceShort } from './types';
export const APIURL = window.location.origin + '/api/';
const Customization: FC = () => {
const Customizations = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
@@ -78,7 +83,7 @@ const Customization: FC = () => {
useLayoutTitle(LL.CUSTOMIZATIONS());
// fetch devices first
const { data: devices, send: fetchDevices } = useRequest(EMSESP.readDevices);
const { data: devices, send: fetchDevices } = useRequest(readDevices);
const [selectedDevice, setSelectedDevice] = useState<number>(
Number(useLocation().state) || -1
@@ -87,32 +92,33 @@ const Customization: FC = () => {
useState<string>(''); // needed for API URL
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
immediate: false
});
const { send: writeDeviceName } = useRequest(
(data: { id: number; name: string }) => EMSESP.writeDeviceName(data),
const { send: sendDeviceName } = useRequest(
(data: { id: number; name: string }) => writeDeviceName(data),
{
immediate: false
}
);
const { send: writeCustomizationEntities } = useRequest(
(data: { id: number; entity_ids: string[] }) =>
EMSESP.writeCustomizationEntities(data),
const { send: sendCustomizationEntities } = useRequest(
(data: { id: number; entity_ids: string[] }) => writeCustomizationEntities(data),
{
immediate: false
}
);
const { send: readDeviceEntities, onSuccess: onSuccess } = useRequest(
(data: number) => EMSESP.readDeviceEntities(data),
const { send: sendDeviceEntities } = useRequest(
(data: number) => readDeviceEntities(data),
{
initialData: [],
immediate: false
}
);
).onSuccess((event) => {
setOriginalSettings(event.data);
});
const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities(
@@ -126,11 +132,7 @@ const Customization: FC = () => {
);
};
onSuccess((event) => {
setOriginalSettings(event.data);
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
const { send: sendRestart } = useRequest(restart(), {
immediate: false
});
@@ -231,7 +233,7 @@ const Customization: FC = () => {
useEffect(() => {
if (devices && selectedDevice !== -1) {
void readDeviceEntities(selectedDevice);
void sendDeviceEntities(selectedDevice);
const id = devices.devices.findIndex((d) => d.i === selectedDevice);
if (id === -1) {
setSelectedDevice(-1);
@@ -245,8 +247,8 @@ const Customization: FC = () => {
}
}, [devices, selectedDevice]);
const restart = async () => {
await restartCommand().catch((error: Error) => {
const doRestart = async () => {
await sendRestart().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
@@ -327,7 +329,7 @@ const Customization: FC = () => {
const resetCustomization = async () => {
try {
await resetCustomizations();
await sendResetCustomizations();
toast.info(LL.CUSTOMIZATIONS_RESTART());
} catch (error) {
toast.error((error as Error).message);
@@ -387,7 +389,7 @@ const Customization: FC = () => {
return;
}
await writeCustomizationEntities({
await sendCustomizationEntities({
id: selectedDevice,
entity_ids: masked_entities
}).catch((error: Error) => {
@@ -402,7 +404,7 @@ const Customization: FC = () => {
};
const renameDevice = async () => {
await writeDeviceName({ id: selectedDevice, name: selectedDeviceName })
await sendDeviceName({ id: selectedDevice, name: selectedDeviceName })
.then(() => {
toast.success(LL.UPDATED_OF(LL.NAME(1)));
})
@@ -676,7 +678,7 @@ const Customization: FC = () => {
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
onClick={doRestart}
>
{LL.RESTART()}
</Button>
@@ -691,7 +693,7 @@ const Customization: FC = () => {
startIcon={<CancelIcon />}
variant="outlined"
color="secondary"
onClick={() => devices && readDeviceEntities(selectedDevice)}
onClick={() => devices && sendDeviceEntities(selectedDevice)}
>
{LL.CANCEL()}
</Button>
@@ -727,7 +729,7 @@ const Customization: FC = () => {
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : renderContent()}
{selectedDeviceEntity && (
<SettingsCustomizationDialog
<SettingsCustomizationsDialog
open={dialogOpen}
onClose={onDialogClose}
onSave={onDialogSave}
@@ -738,4 +740,4 @@ const Customization: FC = () => {
);
};
export default Customization;
export default Customizations;

View File

@@ -23,19 +23,19 @@ import EntityMaskToggle from './EntityMaskToggle';
import { DeviceEntityMask } from './types';
import type { DeviceEntity } from './types';
interface SettingsCustomizationDialogProps {
interface SettingsCustomizationsDialogProps {
open: boolean;
onClose: () => void;
onSave: (di: DeviceEntity) => void;
selectedItem: DeviceEntity;
}
const CustomizationDialog = ({
const CustomizationsDialog = ({
open,
onClose,
onSave,
selectedItem
}: SettingsCustomizationDialogProps) => {
}: SettingsCustomizationsDialogProps) => {
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
const [error, setError] = useState<boolean>(false);
@@ -175,4 +175,4 @@ const CustomizationDialog = ({
);
};
export default CustomizationDialog;
export default CustomizationsDialog;

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa';
@@ -16,12 +15,8 @@ import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd';
import { DeviceType } from './types';
interface DeviceIconProps {
type_id: number;
}
const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
switch (type_id as DeviceType) {
const DeviceIcon = ({ type_id }: { type_id: DeviceType }) => {
switch (type_id) {
case DeviceType.TEMPERATURESENSOR:
case DeviceType.ANALOGSENSOR:
return <MdOutlineSensors />;

View File

@@ -5,7 +5,6 @@ import {
useLayoutEffect,
useState
} from 'react';
import type { FC } from 'react';
import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -20,13 +19,13 @@ import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import RefreshIcon from '@mui/icons-material/Refresh';
import StarIcon from '@mui/icons-material/Star';
import StarBorderOutlinedIcon from '@mui/icons-material/StarBorderOutlined';
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
@@ -57,12 +56,12 @@ import {
import { useTheme } from '@table-library/react-table-library/theme';
import type { Action, State } from '@table-library/react-table-library/types/common';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { ButtonRow, MessageBox, SectionContent, useLayoutTitle } from 'components';
import { useRequest } from 'alova/client';
import { MessageBox, SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import { readCoreData, readDeviceData, writeDeviceValue } from '../../api/app';
import DeviceIcon from './DeviceIcon';
import DashboardDevicesDialog from './DevicesDialog';
import { formatValue } from './deviceValue';
@@ -70,7 +69,7 @@ import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
import type { Device, DeviceValue } from './types';
import { deviceValueItemValidation } from './validators';
const Devices: FC = () => {
const Devices = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
@@ -85,18 +84,15 @@ const Devices: FC = () => {
useLayoutTitle(LL.DEVICES());
const { data: coreData, send: readCoreData } = useRequest(
() => EMSESP.readCoreData(),
{
const { data: coreData, send: sendCoreData } = useRequest(() => readCoreData(), {
initialData: {
connected: true,
devices: []
}
}
);
});
const { data: deviceData, send: readDeviceData } = useRequest(
(id: number) => EMSESP.readDeviceData(id),
const { data: deviceData, send: sendDeviceData } = useRequest(
(id: number) => readDeviceData(id),
{
initialData: {
data: []
@@ -105,8 +101,8 @@ const Devices: FC = () => {
}
);
const { loading: submitting, send: writeDeviceValue } = useRequest(
(data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data),
const { loading: submitting, send: sendDeviceValue } = useRequest(
(data: { id: number; c: string; v: unknown }) => writeDeviceValue(data),
{
immediate: false
}
@@ -288,7 +284,7 @@ const Devices: FC = () => {
async function onSelectChange(action: Action, state: State) {
setSelectedDevice(state.id as number);
if (action.type === 'ADD_BY_ID_EXCLUSIVELY') {
await readDeviceData(state.id as number);
await sendDeviceData(state.id as number);
}
}
@@ -321,12 +317,6 @@ const Devices: FC = () => {
};
}, [escFunction]);
const refreshData = () => {
if (!deviceValueDialogOpen) {
selectedDevice ? void readDeviceData(selectedDevice) : void readCoreData();
}
};
const customize = () => {
if (selectedDevice == 99) {
navigate('/customentities');
@@ -437,7 +427,12 @@ const Devices: FC = () => {
};
useEffect(() => {
const timer = setInterval(() => refreshData(), 60000);
const timer = setInterval(() => {
if (deviceValueDialogOpen) {
return;
}
selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData();
}, 2000);
return () => {
clearInterval(timer);
};
@@ -445,7 +440,7 @@ const Devices: FC = () => {
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
const id = Number(device_select.state.id);
await writeDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
})
@@ -454,7 +449,7 @@ const Devices: FC = () => {
})
.finally(async () => {
setDeviceValueDialogOpen(false);
await readDeviceData(id);
await sendDeviceData(id);
setSelectedDeviceValue(undefined);
});
};
@@ -568,6 +563,9 @@ const Devices: FC = () => {
</HeaderRow>
</Header>
<Body>
{tableList.length === 0 && (
<CircularProgress sx={{ margin: 1 }} size={24} />
)}
{tableList.map((device: Device) => (
<Row key={device.id} item={device}>
<Cell stiff>
@@ -592,6 +590,7 @@ const Devices: FC = () => {
const deviceValueDialogClose = () => {
setDeviceValueDialogOpen(false);
void sendDeviceData(selectedDevice);
};
const renderDeviceData = () => {
@@ -685,11 +684,6 @@ const Devices: FC = () => {
)}
</IconButton>
</ButtonTooltip>
<ButtonTooltip title={LL.REFRESH()}>
<IconButton onClick={refreshData}>
<RefreshIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
</ButtonTooltip>
</Typography>
<Grid item zeroMinWidth justifyContent="flex-end">
<ButtonTooltip title={LL.CANCEL()}>
@@ -784,16 +778,6 @@ const Devices: FC = () => {
progress={submitting}
/>
)}
<ButtonRow mt={1}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={refreshData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>
</SectionContent>
);
};

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { toast } from 'react-toastify';
import CommentIcon from '@mui/icons-material/CommentTwoTone';
@@ -18,25 +17,20 @@ import {
Typography
} from '@mui/material';
import * as EMSESP from 'app/main/api';
import { useRequest } from 'alova';
import { useRequest } from 'alova/client';
import { SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { API } from '../../api/app';
import type { APIcall } from './types';
const Help: FC = () => {
const Help = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.HELP_OF(''));
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
(data: APIcall) => EMSESP.API(data),
{
const { send: getAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
}
);
onGetAPI((event) => {
}).onSuccess((event) => {
const anchor = document.createElement('a');
anchor.href = URL.createObjectURL(
new Blob([JSON.stringify(event.data, null, 2)], {
@@ -45,7 +39,7 @@ const Help: FC = () => {
);
anchor.download =
'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
'emsesp_' + event.args[0].device + '_' + event.args[0].entity + '.txt';
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
@@ -58,6 +52,7 @@ const Help: FC = () => {
};
return (
<>
<SectionContent>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<ListItem>
@@ -139,6 +134,7 @@ const Help: FC = () => {
</Typography>
</Box>
</SectionContent>
</>
);
};

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -18,7 +17,7 @@ import {
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { updateState, useRequest } from 'alova';
import { updateState, useRequest } from 'alova/client';
import {
BlockNavigation,
ButtonRow,
@@ -28,11 +27,11 @@ import {
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import { readModules, writeModules } from '../../api/app';
import ModulesDialog from './ModulesDialog';
import type { ModuleItem, Modules } from './types';
const Modules: FC = () => {
const Modules = () => {
const { LL } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
@@ -44,13 +43,12 @@ const Modules: FC = () => {
data: modules,
send: fetchModules,
error
} = useRequest(EMSESP.readModules, {
} = useRequest(readModules, {
initialData: []
});
const { send: writeModules } = useRequest(
(data: { key: string; enabled: boolean; license: string }) =>
EMSESP.writeModules(data),
const { send: updateModules } = useRequest(
(data: { key: string; enabled: boolean; license: string }) => writeModules(data),
{
immediate: false
}
@@ -123,7 +121,7 @@ const Modules: FC = () => {
}
const updateModuleItem = (updatedItem: ModuleItem) => {
updateState('modules', (data: ModuleItem[]) => {
void updateState(readModules(), (data: ModuleItem[]) => {
const new_data = data.map((mi) =>
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
);
@@ -133,7 +131,7 @@ const Modules: FC = () => {
};
const saveModules = async () => {
await writeModules({
await updateModules({
modules: modules.map((condensed_mi) => ({
key: condensed_mi.key,
enabled: condensed_mi.enabled,

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
@@ -32,12 +30,7 @@ const OPTION_ICONS: {
favorite: [StarIcon, StarOutlineIcon]
};
interface OptionIconProps {
type: OptionType;
isSet: boolean;
}
const OptionIcon: FC<OptionIconProps> = ({ type, isSet }) => {
const OptionIcon = ({ type, isSet }: { type: OptionType; isSet: boolean }) => {
const Icon = OPTION_ICONS[type][isSet ? 0 : 1];
return isSet ? (
<Icon color="primary" sx={{ fontSize: 16, verticalAlign: 'middle' }} />

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -19,7 +18,7 @@ import {
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { updateState, useRequest } from 'alova';
import { updateState, useRequest } from 'alova/client';
import {
BlockNavigation,
ButtonRow,
@@ -29,13 +28,13 @@ import {
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import { readSchedule, writeSchedule } from '../../api/app';
import SettingsSchedulerDialog from './SchedulerDialog';
import { ScheduleFlag } from './types';
import type { Schedule, ScheduleItem } from './types';
import { schedulerItemValidation } from './validators';
const Scheduler: FC = () => {
const Scheduler = () => {
const { LL, locale } = useI18nContext();
const [numChanges, setNumChanges] = useState<number>(0);
const blocker = useBlocker(numChanges !== 0);
@@ -48,13 +47,12 @@ const Scheduler: FC = () => {
data: schedule,
send: fetchSchedule,
error
} = useRequest(EMSESP.readSchedule, {
initialData: [],
force: true
} = useRequest(readSchedule, {
initialData: []
});
const { send: writeSchedule } = useRequest(
(data: Schedule) => EMSESP.writeSchedule(data),
const { send: updateSchedule } = useRequest(
(data: Schedule) => writeSchedule(data),
{
immediate: false
}
@@ -131,7 +129,7 @@ const Scheduler: FC = () => {
});
const saveSchedule = async () => {
await writeSchedule({
await updateSchedule({
schedule: schedule
.filter((si) => !si.deleted)
.map((condensed_si) => ({
@@ -177,8 +175,7 @@ const Scheduler: FC = () => {
const onDialogSave = (updatedItem: ScheduleItem) => {
setDialogOpen(false);
updateState('schedule', (data: ScheduleItem[]) => {
void updateState(readSchedule(), (data: ScheduleItem[]) => {
const new_data = creating
? [
...data.filter((si) => creating || si.o_id !== updatedItem.o_id),

View File

@@ -208,7 +208,7 @@ const SchedulerDialog = ({
scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE ? 'primary' : 'grey'
}
>
{LL.ONCHANGE(0)}
{LL.ONCHANGE()}
</Typography>
</ToggleButton>
<ToggleButton value={ScheduleFlag.SCHEDULE_CONDITION}>
@@ -218,7 +218,7 @@ const SchedulerDialog = ({
scheduleType === ScheduleFlag.SCHEDULE_CONDITION ? 'primary' : 'grey'
}
>
{LL.CONDITION(0)}
{LL.CONDITION()}
</Typography>
</ToggleButton>
<ToggleButton value={ScheduleFlag.SCHEDULE_IMMEDIATE}>
@@ -228,7 +228,7 @@ const SchedulerDialog = ({
scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE ? 'primary' : 'grey'
}
>
{LL.IMMEDIATE(0)}
{LL.IMMEDIATE()}
</Typography>
</ToggleButton>
</ToggleButtonGroup>
@@ -311,10 +311,10 @@ const SchedulerDialog = ({
name="time"
label={
scheduleType === ScheduleFlag.SCHEDULE_CONDITION
? LL.CONDITION(1)
? LL.CONDITION()
: scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE
? LL.ONCHANGE(1)
: LL.IMMEDIATE(1)
? LL.ONCHANGE()
: LL.IMMEDIATE()
}
multiline
fullWidth

View File

@@ -1,11 +1,9 @@
import { useContext, useEffect, useState } from 'react';
import type { FC } from 'react';
import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
import RefreshIcon from '@mui/icons-material/Refresh';
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
import { Box, Button, Typography } from '@mui/material';
@@ -21,12 +19,16 @@ import {
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import type { State } from '@table-library/react-table-library/types/common';
import { useRequest } from 'alova';
import { ButtonRow, SectionContent, useLayoutTitle } from 'components';
import { useAutoRequest, useRequest } from 'alova/client';
import { SectionContent, useLayoutTitle } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api';
import {
readSensorData,
writeAnalogSensor,
writeTemperatureSensor
} from '../../api/app';
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
import {
@@ -46,7 +48,7 @@ import {
temperatureSensorItemValidation
} from './validators';
const Sensors: FC = () => {
const Sensors = () => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
@@ -57,27 +59,25 @@ const Sensors: FC = () => {
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
const [creating, setCreating] = useState<boolean>(false);
const { data: sensorData, send: fetchSensorData } = useRequest(
() => EMSESP.readSensorData(),
{
const { data: sensorData } = useAutoRequest(() => readSensorData(), {
initialData: {
ts: [],
as: [],
analog_enabled: false,
platform: 'ESP32'
}
}
);
},
pollingTime: 2000
});
const { send: writeTemperatureSensor } = useRequest(
(data: WriteTemperatureSensor) => EMSESP.writeTemperatureSensor(data),
const { send: sendTemperatureSensor } = useRequest(
(data: WriteTemperatureSensor) => writeTemperatureSensor(data),
{
immediate: false
}
);
const { send: writeAnalogSensor } = useRequest(
(data: WriteAnalogSensor) => EMSESP.writeAnalogSensor(data),
const { send: sendAnalogSensor } = useRequest(
(data: WriteAnalogSensor) => writeAnalogSensor(data),
{
immediate: false
}
@@ -195,13 +195,6 @@ const Sensors: FC = () => {
}
);
useEffect(() => {
const timer = setInterval(() => fetchSensorData(), 30000);
return () => {
clearInterval(timer);
};
});
useLayoutTitle(LL.SENSORS());
const formatDurationMin = (duration_min: number) => {
@@ -266,17 +259,16 @@ const Sensors: FC = () => {
};
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
await writeTemperatureSensor({ id: ts.id, name: ts.n, offset: ts.o })
await sendTemperatureSensor({ id: ts.id, name: ts.n, offset: ts.o })
.then(() => {
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
})
.catch(() => {
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
})
.finally(async () => {
.finally(() => {
setTemperatureDialogOpen(false);
setSelectedTemperatureSensor(undefined);
await fetchSensorData();
});
};
@@ -311,7 +303,7 @@ const Sensors: FC = () => {
};
const onAnalogDialogSave = async (as: AnalogSensor) => {
await writeAnalogSensor({
await sendAnalogSensor({
id: as.id,
gpio: as.g,
name: as.n,
@@ -327,10 +319,9 @@ const Sensors: FC = () => {
.catch(() => {
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
})
.finally(async () => {
.finally(() => {
setAnalogDialogOpen(false);
setSelectedAnalogSensor(undefined);
await fetchSensorData();
});
};
@@ -474,8 +465,6 @@ const Sensors: FC = () => {
)}
/>
)}
{sensorData?.analog_enabled === true && (
<>
<Typography sx={{ pt: 4, pb: 1 }} variant="h6" color="secondary">
{LL.ANALOG_SENSORS()}
</Typography>
@@ -495,32 +484,18 @@ const Sensors: FC = () => {
)}
/>
)}
</>
)}
<ButtonRow>
<Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={fetchSensorData}
>
{LL.REFRESH()}
</Button>
</Box>
{sensorData?.analog_enabled === true && me.admin && (
<Box mt={1} display="flex" flexWrap="wrap" justifyContent="flex-end">
<Button
variant="outlined"
color="primary"
startIcon={<AddCircleOutlineOutlinedIcon />}
onClick={addAnalogSensor}
>
{LL.ADD(0) + ' ' + LL.ANALOG_SENSOR(1)}
{LL.ADD(0)}
</Button>
)}
</Box>
</ButtonRow>
)}
</SectionContent>
);
};

View File

@@ -1,5 +1,4 @@
import { useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
@@ -19,7 +18,6 @@ import {
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { range } from 'lodash-es';
import type { APSettingsType } from 'types';
import { APProvisionMode } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils';
@@ -29,7 +27,7 @@ export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
const APSettings: FC = () => {
const APSettings = () => {
const {
loadData,
saving,
@@ -74,6 +72,11 @@ const APSettings: FC = () => {
}
};
// no lodash - https://asleepace.com/blog/typescript-range-without-a-loop/
function range(a: number, b: number): number[] {
return a < b ? [a, ...range(a + 1, b)] : [b];
}
return (
<>
<ValidatedTextField

View File

@@ -1,5 +1,4 @@
import { useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -17,9 +16,9 @@ import {
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
import { readHardwareStatus, restart } from 'api/system';
import { useRequest } from 'alova';
import { useRequest } from 'alova/client';
import RestartMonitor from 'app/status/RestartMonitor';
import type { ValidateFieldsError } from 'async-validator';
import {
@@ -36,7 +35,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import * as EMSESP from '../main/api';
import { getBoardProfile, readSettings, writeSettings } from '../../api/app';
import { BOARD_PROFILES } from '../main/types';
import type { Settings } from '../main/types';
import { createSettingsValidator } from '../main/validators';
@@ -49,15 +48,12 @@ export function boardProfileSelectItems() {
));
}
const ApplicationSettings: FC = () => {
const { data: hardwareData } = useRequest(SystemApi.readHardwareStatus, {
force: true
});
const ApplicationSettings = () => {
const { data: hardwareData } = useRequest(readHardwareStatus);
const {
loadData,
saveData,
saving,
updateDataValue,
data,
origData,
@@ -67,8 +63,8 @@ const ApplicationSettings: FC = () => {
errorMessage,
restartNeeded
} = useRest<Settings>({
read: EMSESP.readSettings,
update: EMSESP.writeSettings
read: readSettings,
update: writeSettings
});
const [restarting, setRestarting] = useState<boolean>();
@@ -84,19 +80,12 @@ const ApplicationSettings: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const {
loading: processingBoard,
send: readBoardProfile,
onSuccess: onSuccessBoardProfile
} = useRequest((boardProfile: string) => EMSESP.getBoardProfile(boardProfile), {
const { loading: processingBoard, send: readBoardProfile } = useRequest(
(boardProfile: string) => getBoardProfile(boardProfile),
{
immediate: false
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
onSuccessBoardProfile((event) => {
}
).onSuccess((event) => {
const response = event.data as Settings;
updateDataValue({
...data,
@@ -113,6 +102,10 @@ const ApplicationSettings: FC = () => {
});
});
const { send: restartCommand } = useRequest(restart(), {
immediate: false
});
const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error: Error) => {
toast.error(error.message);
@@ -122,7 +115,7 @@ const ApplicationSettings: FC = () => {
useLayoutTitle(LL.SETTINGS_OF(LL.APPLICATION()));
const content = () => {
if (!data) {
if (!data || !hardwareData) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
@@ -161,16 +154,321 @@ const ApplicationSettings: FC = () => {
return (
<>
<Typography sx={{ pb: 1 }} variant="h6" color="primary">
{LL.INTERFACE_BOARD_PROFILE()}
{LL.SERVICES()}
</Typography>
<Typography color="secondary">API</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.notoken_api}
onChange={updateFormValue}
name="notoken_api"
/>
}
label={LL.BYPASS_TOKEN()}
/>
<Typography color="secondary">Console</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.telnet_enabled}
onChange={updateFormValue}
name="telnet_enabled"
/>
}
label={LL.ENABLE_TELNET()}
/>
<Typography color="secondary">Modbus</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.modbus_enabled}
onChange={updateFormValue}
name="modbus_enabled"
disabled={!hardwareData.psram}
/>
}
label={LL.ENABLE_MODBUS()}
/>
{data.modbus_enabled && (
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_max_clients"
label={LL.AP_MAX_CLIENTS()}
fullWidth
variant="outlined"
value={numberValue(data.modbus_max_clients)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.modbus_port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_timeout"
label="Timeout"
InputProps={{
endAdornment: <InputAdornment position="end">ms</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.modbus_timeout)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
)}
<Typography color="secondary">Syslog</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.syslog_enabled}
onChange={updateFormValue}
name="syslog_enabled"
/>
}
label={LL.ENABLE_SYSLOG()}
/>
{data.syslog_enabled && (
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_host"
label="Host"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.syslog_port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="syslog_level"
label={LL.LOG_LEVEL()}
value={data.syslog_level}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={-1}>OFF</MenuItem>
<MenuItem value={3}>ERR</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={9}>ALL</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_mark_interval"
label={LL.MARK_INTERVAL()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.syslog_mark_interval)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
)}
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.SENSORS()}
</Typography>
<Typography color="secondary">Analog</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.analog_enabled}
onChange={updateFormValue}
name="analog_enabled"
/>
}
label={LL.ENABLE_ANALOG()}
/>
{data.dallas_gpio !== 0 && (
<>
<Typography color="secondary">{LL.TEMPERATURE()}</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.dallas_parasite}
onChange={updateFormValue}
name="dallas_parasite"
/>
}
label={LL.ENABLE_PARASITE()}
/>
</>
)}
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()}
</Typography>
<Grid item>
<TextField
name="locale"
label={LL.LANGUAGE_ENTITIES()}
value={data.locale}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value="de">Deutsch (DE)</MenuItem>
<MenuItem value="en">English (EN)</MenuItem>
<MenuItem value="fr">Français (FR)</MenuItem>
<MenuItem value="it">Italiano (IT)</MenuItem>
<MenuItem value="nl">Nederlands (NL)</MenuItem>
<MenuItem value="no">Norsk (NO)</MenuItem>
<MenuItem value="pl">Polski (PL)</MenuItem>
<MenuItem value="sk">Slovenčina (SK)</MenuItem>
<MenuItem value="sv">Svenska (SV)</MenuItem>
<MenuItem value="tr">Türk (TR)</MenuItem>
</TextField>
</Grid>
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="bool_dashboard"
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
value={data.bool_dashboard}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>true/false</MenuItem>
<MenuItem value={5}>1/0</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="bool_format"
label={LL.BOOLEAN_FORMAT_API()}
value={data.bool_format}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>&quot;true&quot;/&quot;false&quot;</MenuItem>
<MenuItem value={4}>true/false</MenuItem>
<MenuItem value={5}>&quot;1&quot;/&quot;0&quot;</MenuItem>
<MenuItem value={6}>1/0</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="enum_format"
label={LL.ENUM_FORMAT()}
value={data.enum_format}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>{LL.VALUE(5)}</MenuItem>
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
</TextField>
</Grid>
</Grid>
<BlockFormControlLabel
control={
<Checkbox
checked={data.fahrenheit}
onChange={updateFormValue}
name="fahrenheit"
/>
}
label={LL.CONVERT_FAHRENHEIT()}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.trace_raw}
onChange={updateFormValue}
name="trace_raw"
/>
}
label={LL.LOG_HEX()}
/>
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.SETTINGS_OF(LL.HARDWARE())}
</Typography>
<Box color="warning.main">
<Typography variant="body2">{LL.BOARD_PROFILE_TEXT()}</Typography>
</Box>
<TextField
name="board_profile"
label={LL.BOARD_PROFILE()}
value={data.board_profile}
disabled={processingBoard}
disabled={processingBoard || hardwareData.model.startsWith('BBQKees')}
fullWidth
variant="outlined"
onChange={changeBoardProfile}
@@ -204,7 +502,6 @@ const ApplicationSettings: FC = () => {
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
@@ -218,7 +515,6 @@ const ApplicationSettings: FC = () => {
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
@@ -232,7 +528,6 @@ const ApplicationSettings: FC = () => {
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
@@ -248,7 +543,6 @@ const ApplicationSettings: FC = () => {
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
@@ -262,14 +556,12 @@ const ApplicationSettings: FC = () => {
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="phy_type"
label={LL.PHY_TYPE()}
disabled={saving}
value={data.phy_type}
fullWidth
variant="outlined"
@@ -302,7 +594,6 @@ const ApplicationSettings: FC = () => {
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
@@ -315,14 +606,12 @@ const ApplicationSettings: FC = () => {
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="eth_clock_mode"
label="PHY Clk"
disabled={saving}
value={data.eth_clock_mode}
fullWidth
variant="outlined"
@@ -340,9 +629,6 @@ const ApplicationSettings: FC = () => {
)}
</>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
</Typography>
<Grid
container
spacing={1}
@@ -354,7 +640,6 @@ const ApplicationSettings: FC = () => {
<TextField
name="tx_mode"
label={LL.TX_MODE()}
disabled={saving}
value={data.tx_mode}
fullWidth
variant="outlined"
@@ -371,8 +656,7 @@ const ApplicationSettings: FC = () => {
<Grid item xs={12} sm={6}>
<TextField
name="ems_bus_id"
label={LL.ID_OF(LL.EMS_BUS(1))}
disabled={saving}
label={LL.ID_OF(LL.EMS_BUS(0))}
value={data.ems_bus_id}
fullWidth
variant="outlined"
@@ -394,33 +678,16 @@ const ApplicationSettings: FC = () => {
</TextField>
</Grid>
</Grid>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.GENERAL_OPTIONS()}
</Typography>
<Grid item>
<TextField
name="locale"
label={LL.LANGUAGE_ENTITIES()}
disabled={saving}
value={data.locale}
fullWidth
variant="outlined"
<BlockFormControlLabel
control={
<Checkbox
checked={data.readonly_mode}
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value="de">Deutsch (DE)</MenuItem>
<MenuItem value="en">English (EN)</MenuItem>
<MenuItem value="fr">Français (FR)</MenuItem>
<MenuItem value="it">Italiano (IT)</MenuItem>
<MenuItem value="nl">Nederlands (NL)</MenuItem>
<MenuItem value="no">Norsk (NO)</MenuItem>
<MenuItem value="pl">Polski (PL)</MenuItem>
<MenuItem value="sk">Slovenčina (SK)</MenuItem>
<MenuItem value="sv">Svenska (SV)</MenuItem>
<MenuItem value="tr">Türk (TR)</MenuItem>
</TextField>
</Grid>
name="readonly_mode"
/>
}
label={LL.READONLY()}
/>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={
@@ -431,64 +698,8 @@ const ApplicationSettings: FC = () => {
/>
}
label={LL.HIDE_LED()}
disabled={saving}
/>
)}
<BlockFormControlLabel
control={
<Checkbox
checked={data.telnet_enabled}
onChange={updateFormValue}
name="telnet_enabled"
/>
}
label={LL.ENABLE_TELNET()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.analog_enabled}
onChange={updateFormValue}
name="analog_enabled"
/>
}
label={LL.ENABLE_ANALOG()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.fahrenheit}
onChange={updateFormValue}
name="fahrenheit"
/>
}
label={LL.CONVERT_FAHRENHEIT()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.notoken_api}
onChange={updateFormValue}
name="notoken_api"
/>
}
label={LL.BYPASS_TOKEN()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.readonly_mode}
onChange={updateFormValue}
name="readonly_mode"
/>
}
label={LL.READONLY()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
@@ -498,8 +709,11 @@ const ApplicationSettings: FC = () => {
/>
}
label={LL.UNDERCLOCK_CPU()}
disabled={saving}
/>
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.GENERAL_OPTIONS()}
</Typography>
<BlockFormControlLabel
control={
<Checkbox
@@ -509,16 +723,8 @@ const ApplicationSettings: FC = () => {
/>
}
label={LL.HEATINGOFF()}
disabled={saving}
/>
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<BlockFormControlLabel
control={
<Checkbox
@@ -529,9 +735,8 @@ const ApplicationSettings: FC = () => {
}
label={LL.REMOTE_TIMEOUT_EN()}
/>
</Grid>
{data.remote_timeout_en && (
<Grid item xs={12} sm={6} md={4}>
<Box mt={2}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="remote_timeout"
@@ -541,15 +746,13 @@ const ApplicationSettings: FC = () => {
<InputAdornment position="end">{LL.HOURS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.remote_timeout)}
type="number"
onChange={updateFormValue}
/>
</Grid>
</Box>
)}
</Grid>
<Grid
container
spacing={0}
@@ -566,7 +769,6 @@ const ApplicationSettings: FC = () => {
/>
}
label={LL.ENABLE_SHOWER_TIMER()}
disabled={saving}
/>
<BlockFormControlLabel
control={
@@ -649,256 +851,7 @@ const ApplicationSettings: FC = () => {
</>
)}
</Grid>
<Typography sx={{ pt: 3 }} variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()}
</Typography>
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="bool_dashboard"
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
value={data.bool_dashboard}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>true/false</MenuItem>
<MenuItem value={5}>1/0</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="bool_format"
label={LL.BOOLEAN_FORMAT_API()}
value={data.bool_format}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>&quot;true&quot;/&quot;false&quot;</MenuItem>
<MenuItem value={4}>true/false</MenuItem>
<MenuItem value={5}>&quot;1&quot;/&quot;0&quot;</MenuItem>
<MenuItem value={6}>1/0</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="enum_format"
label={LL.ENUM_FORMAT()}
value={data.enum_format}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>{LL.VALUE(5)}</MenuItem>
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
</TextField>
</Grid>
</Grid>
{data.dallas_gpio !== 0 && (
<>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.TEMP_SENSORS()}
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.dallas_parasite}
onChange={updateFormValue}
name="dallas_parasite"
/>
}
label={LL.ENABLE_PARASITE()}
disabled={saving}
/>
</>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.LOGGING()}
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.trace_raw}
onChange={updateFormValue}
name="trace_raw"
/>
}
label={LL.LOG_HEX()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.syslog_enabled}
onChange={updateFormValue}
name="syslog_enabled"
disabled={saving}
/>
}
label={LL.ENABLE_SYSLOG()}
/>
{data.syslog_enabled && (
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_host"
label="Host"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.syslog_port)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="syslog_level"
label={LL.LOG_LEVEL()}
value={data.syslog_level}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
disabled={saving}
>
<MenuItem value={-1}>OFF</MenuItem>
<MenuItem value={3}>ERR</MenuItem>
<MenuItem value={5}>NOTICE</MenuItem>
<MenuItem value={6}>INFO</MenuItem>
<MenuItem value={7}>DEBUG</MenuItem>
<MenuItem value={9}>ALL</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_mark_interval"
label={LL.MARK_INTERVAL()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.syslog_mark_interval)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
</Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Modbus
</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.modbus_enabled}
onChange={updateFormValue}
name="modbus_enabled"
disabled={!hardwareData.psram}
/>
}
label={LL.ENABLE_MODBUS()}
/>
{data.modbus_enabled && (
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_max_clients"
label={LL.AP_MAX_CLIENTS()}
fullWidth
variant="outlined"
value={numberValue(data.modbus_max_clients)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.modbus_port)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="modbus_timeout"
label="Timeout"
InputProps={{
endAdornment: <InputAdornment position="end">ms</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.modbus_timeout)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
</Grid>
)}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button
@@ -915,7 +868,6 @@ const ApplicationSettings: FC = () => {
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
@@ -925,7 +877,6 @@ const ApplicationSettings: FC = () => {
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving}
variant="contained"
color="info"
type="submit"

View File

@@ -0,0 +1,295 @@
import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp';
import { Box, Button, Divider, Link, Typography } from '@mui/material';
import * as SystemApi from 'api/system';
import {
API,
getCustomizations,
getEntities,
getSchedule,
getSettings
} from 'api/app';
import { getDevVersion, getStableVersion } from 'api/system';
import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import {
FormLoader,
SectionContent,
SingleUpload,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const DownloadUpload = () => {
const { LL } = useI18nContext();
const { send: sendSettings } = useRequest(getSettings(), {
immediate: false
}).onSuccess((event) => {
saveFile(event.data, 'settings.json');
});
const { send: sendCustomizations } = useRequest(getCustomizations(), {
immediate: false
}).onSuccess((event) => {
saveFile(event.data, 'customizations.json');
});
const { send: sendEntities } = useRequest(getEntities(), {
immediate: false
}).onSuccess((event) => {
saveFile(event.data, 'entities.json');
});
const { send: sendSchedule } = useRequest(getSchedule(), {
immediate: false
}).onSuccess((event) => {
saveFile(event.data, 'schedule.json');
});
const { send: getAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
}).onSuccess((event) => {
saveFile(
event.data,
String(event.args[0].device) + '_' + String(event.args[0].entity) + '.txt'
);
});
const {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readHardwareStatus);
// called immediately to get the latest version, on page load
// set immediate to false to avoid calling the API on page load and GH blocking while testing!
const { data: latestVersion } = useRequest(getStableVersion, {
immediate: true
});
const { data: latestDevVersion } = useRequest(getDevVersion, {
immediate: true
});
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const getBinURL = (v: string) =>
'EMS-ESP-' +
v.replaceAll('.', '_') +
'-' +
getPlatform().replaceAll('-', '_') +
'.bin';
const getPlatform = () => {
if (
data.flash_chip_size >= 16384 &&
data.esp_platform === 'ESP32' &&
data.psram
) {
return data.esp_platform + '-16M';
}
return data.esp_platform;
};
const saveFile = (json: unknown, 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;
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
const downloadSettings = async () => {
await sendSettings().catch((error: Error) => {
toast.error(error.message);
});
};
const downloadCustomizations = async () => {
await sendCustomizations().catch((error: Error) => {
toast.error(error.message);
});
};
const downloadEntities = async () => {
await sendEntities().catch((error: Error) => {
toast.error(error.message);
});
};
const downloadSchedule = async () => {
await sendSchedule().catch((error: Error) => {
toast.error(error.message);
});
};
const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
toast.error(error.message);
});
};
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
{LL.HELP_INFORMATION_4()}
</Typography>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'info')}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'allvalues')}
>
{LL.ALLVALUES()}
</Button>
</Box>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadSettings}
>
{LL.SETTINGS_OF('')}
</Button>
</Box>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}
</Typography>
<Button
sx={{ ml: 2 }}
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
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadSchedule}
>
{LL.SCHEDULE(0)}
</Button>
</Box>
<Box color="warning.main">
<Typography mt={2} variant="body2">
{LL.EMS_ESP_VER()}
</Typography>
</Box>
<Box p={2} mt={2} border="1px solid grey" borderRadius={2}>
{LL.VERSION_ON() + ' '}
<b>{data.emsesp_version}</b>&nbsp;({getPlatform()})
<Divider />
{latestVersion && (
<Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.OFFICIAL()}&nbsp;{LL.RELEASE_IS()}
&nbsp;<b>{latestVersion}</b>
&nbsp;(
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link
target="_blank"
href={
STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion)
}
color="primary"
>
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
{latestDevVersion && (
<Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.DEVELOPMENT()}&nbsp;{LL.RELEASE_IS()}
&nbsp;
<b>{latestDevVersion}</b>
&nbsp;(
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link
target="_blank"
href={DEV_URL + getBinURL(latestDevVersion)}
color="primary"
>
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
</Box>
<SingleUpload />
</>
);
};
return <SectionContent>{content()}</SectionContent>;
};
export default DownloadUpload;

View File

@@ -1,5 +1,4 @@
import { useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
@@ -31,7 +30,7 @@ import type { MqttSettingsType } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators';
const MqttSettings: FC = () => {
const MqttSettings = () => {
const {
loadData,
saving,

View File

@@ -1,13 +1,13 @@
import { useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
import { Button, Checkbox, MenuItem } from '@mui/material';
import * as NTPApi from 'api/ntp';
import { readNTPSettings } from 'api/ntp';
import { updateState } from 'alova';
import { updateState } from 'alova/client';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
@@ -26,7 +26,7 @@ import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ';
const NTPSettings: FC = () => {
const NTPSettings = () => {
const {
loadData,
saving,
@@ -72,8 +72,7 @@ const NTPSettings: FC = () => {
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFormValue(event);
updateState('ntpSettings', (settings: NTPSettingsType) => ({
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
...settings,
tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value]

View File

@@ -1,4 +1,4 @@
import { type FC, useState } from 'react';
import { useState } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -24,12 +24,12 @@ import {
import * as SystemApi from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { useRequest } from 'alova/client';
import { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { useI18nContext } from 'i18n/i18n-react';
const Settings: FC = () => {
const Settings = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS(0));
@@ -136,8 +136,8 @@ const Settings: FC = () => {
<ListMenuItem
icon={ImportExportIcon}
bgcolor="#5d89f7"
label={LL.UPLOAD_DOWNLOAD()}
text={LL.UPLOAD_DOWNLOAD_1()}
label={LL.DOWNLOAD_UPLOAD()}
text={LL.DOWNLOAD_UPLOAD_1()}
to="upload"
/>
</List>

View File

@@ -1,367 +0,0 @@
import { type FC, useState } from 'react';
import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp';
import { Box, Button, Divider, Link, Typography } from '@mui/material';
import * as SystemApi from 'api/system';
import * as EMSESP from 'app/main/api';
import { useRequest } from 'alova';
import type { APIcall } from 'app/main/types';
import {
FormLoader,
SectionContent,
SingleUpload,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import RestartMonitor from '../status/RestartMonitor';
const UploadDownload: FC = () => {
const { LL } = useI18nContext();
const [restarting, setRestarting] = useState<boolean>();
const [md5, setMd5] = useState<string>();
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 { send: getAPI, onSuccess: onGetAPI } = useRequest(
(data: APIcall) => EMSESP.API(data),
{
immediate: false
}
);
const {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readHardwareStatus, { force: true });
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
immediate: true,
force: true
});
const { data: latestDevVersion } = useRequest(SystemApi.getDevVersion, {
immediate: true,
force: true
});
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const getBinURL = (v: string) =>
'EMS-ESP-' +
v.replaceAll('.', '_') +
'-' +
getPlatform().replaceAll('-', '_') +
'.bin';
const getPlatform = () => {
if (data.flash_chip_size === 16384) {
return data.esp_platform + '-16M';
}
return data.esp_platform;
};
const {
loading: isUploading,
uploading: progress,
send: sendUpload,
onSuccess: onSuccessUpload,
abort: cancelUpload
} = useRequest(SystemApi.uploadFile, {
immediate: false,
force: true
});
onSuccessUpload(({ data }) => {
if (data) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setMd5(data.md5);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
} else {
setRestarting(true);
}
});
const startUpload = async (files: File[]) => {
await sendUpload(files[0]).catch((error: Error) => {
if (error.message === 'The user aborted a request') {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else if (error.message === 'Network Error') {
toast.warning('Invalid file extension or incompatible bin file');
} else {
toast.error(error.message);
}
});
};
const saveFile = (json: unknown, 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;
anchor.click();
URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL());
};
onSuccessGetSettings((event) => {
saveFile(event.data, 'settings.json');
});
onSuccessGetCustomizations((event) => {
saveFile(event.data, 'customizations.json');
});
onSuccessGetEntities((event) => {
saveFile(event.data, 'entities.json');
});
onSuccessGetSchedule((event) => {
saveFile(event.data, 'schedule.json');
});
onGetAPI((event) => {
saveFile(
event.data,
event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'
);
});
const downloadSettings = async () => {
await getSettings().catch((error: Error) => {
toast.error(error.message);
});
};
const downloadCustomizations = async () => {
await getCustomizations().catch((error: Error) => {
toast.error(error.message);
});
};
const downloadEntities = async () => {
await getEntities().catch((error: Error) => {
toast.error(error.message);
});
};
const downloadSchedule = async () => {
await getSchedule().catch((error: Error) => {
toast.error(error.message);
});
};
const callAPI = async (device: string, entity: string) => {
await getAPI({ device, entity, id: 0 }).catch((error: Error) => {
toast.error(error.message);
});
};
useLayoutTitle(LL.UPLOAD_DOWNLOAD());
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message} />;
}
return (
<>
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
{LL.EMS_ESP_VER()}
</Typography>
<Box p={2} border="2px solid grey" borderRadius={2}>
{LL.VERSION_ON() + ' '}
<b>{data.emsesp_version}</b>&nbsp;({getPlatform()})
<Divider />
{latestVersion && (
<Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.OFFICIAL()}&nbsp;{LL.RELEASE_IS()}
&nbsp;<b>{latestVersion}</b>
&nbsp;(
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link
target="_blank"
href={
STABLE_URL +
'v' +
latestVersion +
'/' +
getBinURL(latestVersion as string)
}
color="primary"
>
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
{latestDevVersion && (
<Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.DEVELOPMENT()}&nbsp;{LL.RELEASE_IS()}
&nbsp;
<b>{latestDevVersion}</b>
&nbsp;(
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()}
</Link>
)&nbsp;(
<Link
target="_blank"
href={DEV_URL + getBinURL(latestDevVersion as string)}
color="primary"
>
{LL.DOWNLOAD(1)}
</Link>
)
</Box>
)}
</Box>
<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()}
<br />
<br />
{LL.RESTART_TEXT(1)}.
</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: 4, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
{LL.HELP_INFORMATION_4()}
</Typography>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'info')}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={() => callAPI('system', 'allvalues')}
>
All Values
</Button>
</Box>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
<Button
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadSettings}
>
{LL.SETTINGS_OF('')}
</Button>
</Box>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}
</Typography>
<Button
sx={{ ml: 2 }}
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
sx={{ ml: 2 }}
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={downloadSchedule}
>
{LL.SCHEDULE(0)}
</Button>
</Box>
</>
)}
</>
);
};
return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
};
export default UploadDownload;

View File

@@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
import { Tab } from '@mui/material';
@@ -12,7 +11,7 @@ import NetworkSettings from './NetworkSettings';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import WiFiNetworkScanner from './WiFiNetworkScanner';
const Network: FC = () => {
const Network = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF(LL.NETWORK(0)));

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -26,7 +25,7 @@ import {
import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system';
import { updateState, useRequest } from 'alova';
import { updateState, useRequest } from 'alova/client';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
@@ -48,7 +47,7 @@ import RestartMonitor from '../../status/RestartMonitor';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
const NetworkSettings: FC = () => {
const NetworkSettings = () => {
const { LL } = useI18nContext();
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
@@ -80,7 +79,9 @@ const NetworkSettings: FC = () => {
useEffect(() => {
if (!initialized && data) {
if (selectedNetwork) {
updateState('networkSettings', (current_data: NetworkSettingsType) => ({
void updateState(
NetworkApi.readNetworkSettings(),
(current_data: NetworkSettingsType) => ({
ssid: selectedNetwork.ssid,
bssid: selectedNetwork.bssid,
password: current_data ? current_data.password : '',
@@ -92,7 +93,8 @@ const NetworkSettings: FC = () => {
enableMDNS: true,
enableCORS: false,
CORSOrigin: '*'
}));
})
);
}
setInitialized(true);
}

View File

@@ -1,12 +1,11 @@
import { useRef, useState } from 'react';
import type { FC } from 'react';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import { Button } from '@mui/material';
import * as NetworkApi from 'api/network';
import { updateState, useRequest } from 'alova';
import { updateState, useRequest } from 'alova/client';
import { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
@@ -15,23 +14,28 @@ import WiFiNetworkSelector from './WiFiNetworkSelector';
const NUM_POLLS = 10;
const POLLING_FREQUENCY = 1000;
const WiFiNetworkScanner: FC = () => {
const WiFiNetworkScanner = () => {
const pollCount = useRef(0);
const { LL } = useI18nContext();
const [errorMessage, setErrorMessage] = useState<string>();
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
});
// is called on page load to start network scan
const { send: scanNetworks } = useRequest(NetworkApi.scanNetworks).onComplete(
() => {
pollCount.current = 0;
setErrorMessage(undefined);
void updateState(NetworkApi.listNetworks(), () => undefined);
void getNetworkList();
}
);
onSuccessNetworkList((event) => {
const { data: networkList, send: getNetworkList } = useRequest(
NetworkApi.listNetworks,
{
immediate: false
}
).onSuccess((event) => {
// is called when network scan is completed
if (!event.data) {
const completedPollCount = pollCount.current + 1;
if (completedPollCount < NUM_POLLS) {
@@ -44,13 +48,6 @@ const WiFiNetworkScanner: FC = () => {
}
});
onCompleteScanNetworks(() => {
pollCount.current = 0;
setErrorMessage(undefined);
updateState('listNetworks', () => undefined);
void getNetworkList();
});
const renderNetworkScanner = () => {
if (!networkList) {
return (

View File

@@ -1,5 +1,4 @@
import { useContext } from 'react';
import type { FC } from 'react';
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
@@ -23,10 +22,6 @@ import { WiFiEncryptionType } from 'types';
import { WiFiConnectionContext } from './WiFiConnectionContext';
interface WiFiNetworkSelectorProps {
networkList: WiFiNetworkList;
}
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
@@ -62,7 +57,7 @@ const networkQualityHighlight = ({ rssi }: WiFiNetwork, theme: Theme) => {
return theme.palette.success.main;
};
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList }) => {
const { LL } = useI18nContext();
const theme = useTheme();

View File

@@ -1,5 +1,4 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import CloseIcon from '@mui/icons-material/Close';
import {
@@ -17,7 +16,7 @@ import {
import * as SecurityApi from 'api/security';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { useRequest } from 'alova/client';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
@@ -26,7 +25,7 @@ interface GenerateTokenProps {
onClose: () => void;
}
const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const GenerateToken = ({ username, onClose }: GenerateTokenProps) => {
const { LL } = useI18nContext();
const open = !!username;

View File

@@ -1,5 +1,4 @@
import { useContext, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -40,7 +39,7 @@ import { createUserValidator } from 'validators';
import GenerateToken from './GenerateToken';
import User from './User';
const ManageUsers: FC = () => {
const ManageUsers = () => {
const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
useRest<SecuritySettingsType>({
read: SecurityApi.readSecuritySettings,
@@ -221,8 +220,8 @@ const ManageUsers: FC = () => {
)}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
{changed !== 0 && (
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button
startIcon={<CancelIcon />}
@@ -245,9 +244,8 @@ const ManageUsers: FC = () => {
{LL.APPLY_CHANGES(changed)}
</Button>
</ButtonRow>
)}
</Box>
)}
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material';
@@ -9,7 +8,7 @@ import { useI18nContext } from 'i18n/i18n-react';
import ManageUsers from './ManageUsers';
import SecuritySettings from './SecuritySettings';
const Security: FC = () => {
const Security = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF(LL.SECURITY(0)));

View File

@@ -1,5 +1,4 @@
import { useContext, useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
@@ -22,7 +21,7 @@ import type { SecuritySettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils';
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
const SecuritySettings: FC = () => {
const SecuritySettings = () => {
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();

View File

@@ -29,10 +29,8 @@ import { validate } from 'validators';
interface UserFormProps {
creating: boolean;
validator: Schema;
user?: UserType;
setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>;
onDoneEditing: () => void;
onCancelEditing: () => void;
}

View File

@@ -1,12 +1,8 @@
import type { FC } from 'react';
import ComputerIcon from '@mui/icons-material/Computer';
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,
@@ -18,8 +14,8 @@ import type { Theme } from '@mui/material';
import * as APApi from 'api/ap';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { APStatusType } from 'types';
import { APNetworkStatus } from 'types';
@@ -37,8 +33,12 @@ export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => {
}
};
const APStatus: FC = () => {
const { data: data, send: loadData, error } = useRequest(APApi.readAPStatus);
const APStatus = () => {
const {
data: data,
send: loadData,
error
} = useAutoRequest(APApi.readAPStatus, { pollingTime: 5000 });
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(LL.ACCESS_POINT(0)));
@@ -64,7 +64,6 @@ const APStatus: FC = () => {
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
@@ -79,10 +78,7 @@ const APStatus: FC = () => {
<ListItemAvatar>
<Avatar>IP</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.ADDRESS_OF('IP')}
secondary={data.ip_address}
/>
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={data.ip_address} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -107,17 +103,6 @@ const APStatus: FC = () => {
</ListItem>
<Divider variant="inset" component="li" />
</List>
<ButtonRow>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
);
};

View File

@@ -1,9 +1,3 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Button } from '@mui/material';
import {
Body,
Cell,
@@ -14,16 +8,20 @@ import {
Table
} from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { Translation } from 'i18n/i18n-types';
import * as EMSESP from '../main/api';
import { readActivity } from '../../api/app';
import type { Stat } from '../main/types';
const SystemActivity: FC = () => {
const { data: data, send: loadData, error } = useRequest(EMSESP.readActivity);
const SystemActivity = () => {
const {
data: data,
send: loadData,
error
} = useAutoRequest(readActivity, { pollingTime: 2000 });
const { LL } = useI18nContext();
@@ -67,13 +65,6 @@ const SystemActivity: FC = () => {
`
});
useEffect(() => {
const timer = setInterval(() => loadData(), 30000);
return () => {
clearInterval(timer);
};
});
const showName = (id: number) => {
const name: keyof Translation['STATUS_NAMES'] = id;
return LL.STATUS_NAMES[name]();
@@ -99,7 +90,6 @@ const SystemActivity: FC = () => {
}
return (
<>
<Table
data={{ nodes: data.stats }}
theme={stats_theme}
@@ -128,17 +118,6 @@ const SystemActivity: FC = () => {
</>
)}
</Table>
<ButtonRow mt={1}>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
);
};

View File

@@ -1,18 +1,13 @@
import type { FC } from 'react';
import AppsIcon from '@mui/icons-material/Apps';
import DeveloperBoardIcon from '@mui/icons-material/DeveloperBoard';
import DevicesIcon from '@mui/icons-material/Devices';
import FolderIcon from '@mui/icons-material/Folder';
import MemoryIcon from '@mui/icons-material/Memory';
import RefreshIcon from '@mui/icons-material/Refresh';
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
import SdStorageIcon from '@mui/icons-material/SdStorage';
import TapAndPlayIcon from '@mui/icons-material/TapAndPlay';
import {
Avatar,
Box,
Button,
Divider,
List,
ListItem,
@@ -22,8 +17,8 @@ import {
import * as SystemApi from 'api/system';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import BBQKeesIcon from './bbqkees.svg';
@@ -32,7 +27,7 @@ function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
const HardwareStatus: FC = () => {
const HardwareStatus = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(LL.HARDWARE()));
@@ -41,7 +36,7 @@ const HardwareStatus: FC = () => {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readHardwareStatus, { force: true });
} = useAutoRequest(SystemApi.readHardwareStatus, { pollingTime: 2000 });
const content = () => {
if (!data) {
@@ -49,7 +44,6 @@ const HardwareStatus: FC = () => {
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
@@ -199,21 +193,6 @@ const HardwareStatus: FC = () => {
</ListItem>
<Divider variant="inset" component="li" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
</Box>
</>
);
};

View File

@@ -1,13 +1,9 @@
import type { FC } from 'react';
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
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,
@@ -19,8 +15,8 @@ import type { Theme } from '@mui/material';
import * as MqttApi from 'api/mqtt';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { MqttStatusType } from 'types';
import { MqttDisconnectReason } from 'types';
@@ -57,8 +53,12 @@ export const mqttQueueHighlight = (
return theme.palette.warning.main;
};
const MqttStatus: FC = () => {
const { data: data, send: loadData, error } = useRequest(MqttApi.readMqttStatus);
const MqttStatus = () => {
const {
data: data,
send: loadData,
error
} = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 5000 });
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF('MQTT'));
@@ -148,7 +148,6 @@ const MqttStatus: FC = () => {
);
return (
<>
<List>
<ListItem>
<ListItemAvatar>
@@ -161,17 +160,6 @@ const MqttStatus: FC = () => {
<Divider variant="inset" component="li" />
{data.enabled && renderConnectionStatus()}
</List>
<ButtonRow>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
);
};

View File

@@ -1,11 +1,9 @@
import { useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel';
import DnsIcon from '@mui/icons-material/Dns';
import RefreshIcon from '@mui/icons-material/Refresh';
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
import UpdateIcon from '@mui/icons-material/Update';
import {
@@ -30,15 +28,19 @@ import type { Theme } from '@mui/material';
import * as NTPApi from 'api/ntp';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { useAutoRequest, useRequest } from 'alova/client';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { NTPStatusType, Time } from 'types';
import { NTPSyncStatus } from 'types';
import { formatDateTime, formatLocalDateTime } from 'utils';
const NTPStatus: FC = () => {
const { data: data, send: loadData, error } = useRequest(NTPApi.readNTPStatus);
const NTPStatus = () => {
const {
data: data,
send: loadData,
error
} = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 5000 });
const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false);
@@ -215,18 +217,6 @@ const NTPStatus: FC = () => {
<Divider variant="inset" component="li" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<ButtonRow>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
{data && !isNtpActive(data) && (
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>

View File

@@ -1,16 +1,12 @@
import type { FC } from 'react';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DnsIcon from '@mui/icons-material/Dns';
import GiteIcon from '@mui/icons-material/Gite';
import RefreshIcon from '@mui/icons-material/Refresh';
import RouterIcon from '@mui/icons-material/Router';
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,
@@ -22,8 +18,8 @@ import type { Theme } from '@mui/material';
import * as NetworkApi from 'api/network';
import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useAutoRequest } from 'alova/client';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { NetworkStatusType } from 'types';
import { NetworkConnectionStatus } from 'types';
@@ -84,12 +80,12 @@ const IPs = (status: NetworkStatusType) => {
return status.local_ip + ', ' + status.local_ipv6;
};
const NetworkStatus: FC = () => {
const NetworkStatus = () => {
const {
data: data,
send: loadData,
error
} = useRequest(NetworkApi.readNetworkStatus);
} = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 5000 });
const { LL } = useI18nContext();
useLayoutTitle(LL.STATUS_OF(LL.NETWORK(1)));
@@ -125,7 +121,6 @@ const NetworkStatus: FC = () => {
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
@@ -216,21 +211,9 @@ const NetworkStatus: FC = () => {
secondary={dnsServers(data)}
/>
</ListItem>
<Divider variant="inset" component="li" />
</>
)}
</List>
<ButtonRow>
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
);
};

View File

@@ -1,20 +1,19 @@
import { useEffect, useRef, useState } from 'react';
import type { FC } from 'react';
import * as SystemApi from 'api/system';
import { useRequest } from 'alova';
import { useRequest } from 'alova/client';
import { FormLoader } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000;
const POLL_INTERVAL = 3000;
const RestartMonitor: FC = () => {
const RestartMonitor = () => {
const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const { LL } = useI18nContext();
const { send } = useRequest(SystemApi.readSystemStatus, { force: true });
const { send } = useRequest(SystemApi.readSystemStatus);
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
const poll = useRef(async () => {

View File

@@ -1,4 +1,4 @@
import { type FC, useContext, useState } from 'react';
import { useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
@@ -10,7 +10,6 @@ import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import LogoDevIcon from '@mui/icons-material/LogoDev';
import MemoryIcon from '@mui/icons-material/Memory';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import RefreshIcon from '@mui/icons-material/Refresh';
import RouterIcon from '@mui/icons-material/Router';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TimerIcon from '@mui/icons-material/Timer';
@@ -18,7 +17,6 @@ import UpgradeIcon from '@mui/icons-material/Upgrade';
import WifiIcon from '@mui/icons-material/Wifi';
import {
Avatar,
Box,
Button,
Dialog,
DialogActions,
@@ -34,7 +32,7 @@ import {
import * as SystemApi from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { useAutoRequest, useRequest } from 'alova/client';
import { busConnectionStatus } from 'app/main/types';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
@@ -44,7 +42,7 @@ import { NTPSyncStatus, NetworkConnectionStatus } from 'types';
import RestartMonitor from './RestartMonitor';
const SystemStatus: FC = () => {
const SystemStatus = () => {
const { LL } = useI18nContext();
const navigate = useNavigate();
@@ -69,7 +67,10 @@ const SystemStatus: FC = () => {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readSystemStatus, { force: true });
} = useAutoRequest(SystemApi.readSystemStatus, {
initialData: [],
pollingTime: 5000
});
const theme = useTheme();
@@ -390,17 +391,6 @@ const SystemStatus: FC = () => {
</List>
{renderRestartDialog()}
<Box mt={2} display="flex" flexWrap="wrap">
<Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()}
</Button>
</Box>
</>
);
};

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp';
@@ -17,8 +16,7 @@ import {
import * as SystemApi from 'api/system';
import { fetchLogES } from 'api/system';
import { useSSE } from '@alova/scene-react';
import { useRequest } from 'alova';
import { useRequest, useSSE } from 'alova/client';
import {
BlockFormControlLabel,
BlockNavigation,
@@ -72,7 +70,7 @@ const levelLabel = (level: LogLevel) => {
}
};
const SystemLog: FC = () => {
const SystemLog = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.LOG_OF(LL.SYSTEM(0)));

View File

@@ -1,4 +1,4 @@
import { type ChangeEventHandler, type FC, useContext } from 'react';
import { type ChangeEventHandler, useContext } from 'react';
import { MenuItem, TextField } from '@mui/material';
@@ -16,7 +16,7 @@ import { I18nContext } from 'i18n/i18n-react';
import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
const LanguageSelector: FC = () => {
const LanguageSelector = () => {
const { setLocale, locale } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({

View File

@@ -4,8 +4,7 @@ import { useLocation } from 'react-router-dom';
import { Box, Toolbar } from '@mui/material';
import { PROJECT_NAME } from 'api/env';
import { PROJECT_NAME } from 'env';
import type { RequiredChildrenProps } from 'utils';
import LayoutAppBar from './LayoutAppBar';

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -12,7 +11,7 @@ interface LayoutAppBarProps {
onToggleDrawer: () => void;
}
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
const LayoutAppBar = ({ title, onToggleDrawer }: LayoutAppBarProps) => {
const pathnames = useLocation()
.pathname.split('/')
.filter((x) => x);

View File

@@ -1,8 +1,6 @@
import type { FC } from 'react';
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
import { PROJECT_NAME } from 'api/env';
import { PROJECT_NAME } from 'env';
import { DRAWER_WIDTH } from './Layout';
import LayoutMenu from './LayoutMenu';
@@ -23,7 +21,7 @@ interface LayoutDrawerProps {
onClose: () => void;
}
const LayoutDrawer: FC<LayoutDrawerProps> = ({ mobileOpen, onClose }) => {
const LayoutDrawerProps = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
const drawer = (
<>
<Toolbar disableGutters>
@@ -68,4 +66,4 @@ const LayoutDrawer: FC<LayoutDrawerProps> = ({ mobileOpen, onClose }) => {
);
};
export default LayoutDrawer;
export default LayoutDrawerProps;

View File

@@ -1,5 +1,4 @@
import { useContext, useState } from 'react';
import type { FC } from 'react';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import AssessmentIcon from '@mui/icons-material/Assessment';
@@ -30,7 +29,7 @@ import LayoutMenuItem from 'components/layout/LayoutMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
const LayoutMenu: FC = () => {
const LayoutMenu = () => {
const { me, signOut } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
@@ -13,12 +12,12 @@ interface LayoutMenuItemProps {
disabled?: boolean;
}
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({
const LayoutMenuItem = ({
icon: Icon,
label,
to,
disabled
}) => {
}: LayoutMenuItemProps) => {
const { pathname } = useLocation();
const selected = routeMatches(to, pathname);

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
@@ -34,14 +33,14 @@ function RenderIcon({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) {
);
}
const LayoutMenuItem: FC<ListMenuItemProps> = ({
const LayoutMenuItem = ({
icon,
bgcolor,
label,
text,
to,
disabled
}) => (
}: ListMenuItemProps) => (
<>
{to && !disabled ? (
<ListItem

View File

@@ -1,49 +0,0 @@
import type { FC } from 'react';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Paper, Typography } from '@mui/material';
interface ApplicationErrorProps {
message?: string;
}
const ApplicationError: FC<ApplicationErrorProps> = ({ message }) => (
<Box display="flex" height="100vh" justifyContent="center" flexDirection="column">
<Paper
elevation={10}
sx={{
textAlign: 'center',
padding: '280px 0 40px 0',
backgroundImage: 'url("/app/icon.png")',
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% 40px',
backgroundSize: '200px auto',
width: '100%',
borderRadius: 0
}}
>
<Box
display="flex"
flexDirection="row"
justifyContent="center"
alignItems="center"
mb={2}
>
<WarningIcon fontSize="large" color="error" />
<Box ml={2}>
<Typography variant="h4">Application Error</Typography>
</Box>
</Box>
<Typography variant="subtitle1" gutterBottom>
Failed to configure the application, please refresh to try again.
</Typography>
{message && (
<Typography variant="subtitle2" gutterBottom>
{message}
</Typography>
)}
</Paper>
</Box>
);
export default ApplicationError;

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Box, Button, CircularProgress, Typography } from '@mui/material';
@@ -12,11 +10,11 @@ interface FormLoaderProps {
onRetry?: () => void;
}
const FormLoader: FC<FormLoaderProps> = ({
const FormLoader = ({
errorMessage,
onRetry,
message = 'Loading…'
}) => {
}: FormLoaderProps) => {
const { LL } = useI18nContext();
if (errorMessage) {

View File

@@ -1,5 +1,3 @@
import type { FC } from 'react';
import { Box, CircularProgress, Typography } from '@mui/material';
import type { Theme } from '@mui/material';
@@ -9,7 +7,7 @@ interface LoadingSpinnerProps {
height?: number | string;
}
const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => {
const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => {
const { LL } = useI18nContext();
return (

View File

@@ -1,3 +1,2 @@
export { default as ApplicationError } from './ApplicationError';
export { default as LoadingSpinner } from './LoadingSpinner';
export { default as FormLoader } from './FormLoader';

View File

@@ -1,4 +1,3 @@
import type { FC } from 'react';
import type { Blocker } from 'react-router-dom';
import {
@@ -12,11 +11,7 @@ import {
import { dialogStyle } from 'CustomTheme';
import { useI18nContext } from 'i18n/i18n-react';
interface BlockNavigationProps {
blocker: Blocker;
}
const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
const { LL } = useI18nContext();
return (

View File

@@ -2,8 +2,7 @@ import { useContext, useEffect } from 'react';
import type { FC } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { storeLoginRedirect } from 'api/authentication';
import { storeLoginRedirect } from 'components/routing/authentication';
import type { AuthenticatedContextValue } from 'contexts/authentication/context';
import {
AuthenticatedContext,

View File

@@ -2,8 +2,7 @@ import { useContext } from 'react';
import type { FC } from 'react';
import { Navigate } from 'react-router-dom';
import * as AuthenticationApi from 'api/authentication';
import { fetchLoginRedirect } from 'components/routing/authentication';
import { AuthenticationContext } from 'contexts/authentication';
import type { RequiredChildrenProps } from 'utils';
@@ -11,7 +10,7 @@ const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
const authenticationContext = useContext(AuthenticationContext);
return authenticationContext.me ? (
<Navigate to={AuthenticationApi.fetchLoginRedirect()} />
<Navigate to={fetchLoginRedirect()} />
) : (
<>{children}</>
);

View File

@@ -4,7 +4,7 @@ import type * as H from 'history';
import { jwtDecode } from 'jwt-decode';
import type { Me, SignInRequest, SignInResponse } from 'types';
import { ACCESS_TOKEN, alovaInstance } from './endpoints';
import { ACCESS_TOKEN, alovaInstance } from '../../api/endpoints';
export const SIGN_IN_PATHNAME = 'loginPathname';
export const SIGN_IN_SEARCH = 'loginSearch';

View File

@@ -0,0 +1,112 @@
// Code inspired by https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
// (c) Prince Azubuike
import { type ChangeEvent, useRef, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import UploadIcon from '@mui/icons-material/Upload';
import { Box, Button } from '@mui/material';
import { useI18nContext } from 'i18n/i18n-react';
import './drag-drop.css';
const DragNdrop = ({ onFileSelected }) => {
const [file, setFile] = useState<File>();
const inputRef = useRef<HTMLInputElement | null>(null);
const { LL } = useI18nContext();
const checkFileExtension = (file: File) => {
const validExtensions = ['.json', '.txt', '.csv', '.bin', '.md5'];
const fileName = file.name;
const fileExtension = fileName.substring(fileName.lastIndexOf('.'));
if (validExtensions.includes(fileExtension)) {
setFile(file);
} else {
alert('Invalid file type');
}
};
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files) {
return;
}
checkFileExtension(e.target.files[0]);
e.target.value = ''; // this is to allow the same file to be selected again
};
const handleDrop = (event) => {
event.preventDefault();
const droppedFiles = event.dataTransfer.files;
if (droppedFiles.length > 0) {
checkFileExtension(droppedFiles[0]);
}
};
const handleRemoveFile = (event) => {
event.stopPropagation();
setFile(undefined);
};
const handleUploadClick = (event) => {
event.stopPropagation();
onFileSelected(file);
};
const handleBrowseClick = () => {
inputRef.current?.click();
};
return (
<div
className={`document-uploader ${file ? 'upload-box active' : 'upload-box'}`}
onDrop={handleDrop}
onDragOver={(event) => event.preventDefault()}
onClick={handleBrowseClick}
>
<div className="upload-info">
<CloudUploadIcon sx={{ marginRight: 4 }} color="primary" fontSize="large" />
<p>{LL.UPLOAD_DRAG()}</p>
</div>
<input
type="file"
hidden
onChange={handleFileChange}
ref={inputRef}
accept=".json,.txt,.csv,.bin,.md5"
multiple={false}
style={{ display: 'none' }}
/>
{file && (
<>
<div className="file-info">
<p>{file.name}</p>
</div>
<Box>
<Button
startIcon={<CancelIcon />}
variant="outlined"
color="error"
onClick={(e) => handleRemoveFile(e)}
>
{LL.CANCEL()}
</Button>
<Button
sx={{ ml: 2 }}
startIcon={<UploadIcon />}
variant="outlined"
color="primary"
onClick={handleUploadClick}
>
{LL.UPLOAD()}
</Button>
</Box>
</>
)}
</div>
);
};
export default DragNdrop;

View File

@@ -1,94 +1,82 @@
import { Fragment } from 'react';
import type { FC } from 'react';
import { useDropzone } from 'react-dropzone';
import type { DropzoneState } from 'react-dropzone';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import { Box, Button, LinearProgress, Typography, useTheme } from '@mui/material';
import type { Theme } from '@mui/material';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import { Box, Button, LinearProgress, Typography } from '@mui/material';
import type { Progress } from 'alova';
import * as SystemApi from 'api/system';
import { useRequest } from 'alova/client';
import RestartMonitor from 'app/status/RestartMonitor';
import MessageBox from 'components/MessageBox';
import { useI18nContext } from 'i18n/i18n-react';
const getBorderColor = (theme: Theme, props: DropzoneState) => {
if (props.isDragAccept) {
return theme.palette.success.main;
}
if (props.isDragReject) {
return theme.palette.error.main;
}
if (props.isDragActive) {
return theme.palette.info.main;
}
return theme.palette.grey[700];
};
import DragNdrop from './DragNdrop';
export interface SingleUploadProps {
onDrop: (acceptedFiles: File[]) => void;
onCancel: () => void;
isUploading: boolean;
progress: Progress;
}
const SingleUpload = () => {
const [md5, setMd5] = useState<string>();
const [restarting, setRestarting] = useState<boolean>(false);
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
const SingleUpload: FC<SingleUploadProps> = ({
onDrop,
onCancel,
isUploading,
progress
}) => {
const uploading = isUploading && progress.total > 0;
const dropzoneState = useDropzone({
onDrop,
accept: {
'application/octet-stream': ['.bin'],
'application/json': ['.json'],
'text/plain': ['.md5']
},
disabled: isUploading,
multiple: false
});
const { getRootProps, getInputProps } = dropzoneState;
const theme = useTheme();
const [file, setFile] = useState<File>();
const { LL } = useI18nContext();
const progressText = () => {
if (uploading) {
return (
LL.UPLOADING() +
': ' +
Math.round((progress.loaded * 100) / progress.total) +
'%'
);
const {
loading: isUploading,
uploading: progress,
send: sendUpload,
abort: cancelUpload
} = useRequest(SystemApi.uploadFile, {
immediate: false
}).onSuccess(({ data }) => {
if (data) {
setMd5(data.md5 as string);
toast.success(LL.UPLOAD() + ' MD5 ' + LL.SUCCESSFUL());
setFile(undefined);
} else {
setRestartNeeded(true);
}
return LL.UPLOAD_DROP_TEXT();
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
const restart = async () => {
await restartCommand().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
return (
<Box
{...getRootProps({
sx: {
py: 4,
px: 2,
borderWidth: 2,
borderRadius: 2,
borderStyle: 'dashed',
color: theme.palette.grey[400],
transition: 'border .24s ease-in-out',
width: '100%',
cursor: uploading ? 'default' : 'pointer',
borderColor: getBorderColor(theme, dropzoneState)
useEffect(async () => {
if (file) {
console.log('going to upload file ', file.name);
await sendUpload(file).catch((error: Error) => {
if (error.message === 'The user aborted a request') {
toast.warning(LL.UPLOAD() + ' ' + LL.ABORTED());
} else if (error.message === 'Network Error') {
toast.warning('Invalid file extension or incompatible bin file');
} else {
toast.error(error.message);
}
})}
>
<input {...getInputProps()} />
<Box flexDirection="column" display="flex" alignItems="center">
<CloudUploadIcon fontSize="large" />
<Typography variant="h6">{progressText()}</Typography>
{uploading && (
<Fragment>
});
}
}, [file]);
return (
<>
<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>
{isUploading || restartNeeded ? (
<>
<Box width="100%" p={2}>
<LinearProgress
variant="determinate"
@@ -101,18 +89,44 @@ const SingleUpload: FC<SingleUploadProps> = ({
}
/>
</Box>
{!restartNeeded && (
<Button
sx={{ ml: 2 }}
startIcon={<CancelIcon />}
variant="outlined"
color="secondary"
onClick={onCancel}
color="error"
onClick={cancelUpload}
>
{LL.CANCEL()}
</Button>
</Fragment>
)}
</>
) : (
<DragNdrop onFileSelected={setFile} />
)}
{md5 && (
<Box mt={2}>
<Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box>
</Box>
)}
{restartNeeded && (
<MessageBox mt={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()}
</Button>
</MessageBox>
)}
{restarting && <RestartMonitor />}
</>
);
};

View File

@@ -0,0 +1,33 @@
.document-uploader {
border: 2px dashed #4282fe;
background-color: #2e3339;
padding: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
border-radius: 8px;
cursor: pointer;
&.active {
border-color: #6dc24b;
}
.upload-info {
display: flex;
align-items: center;
}
.file-info {
display: flex;
flex-direction: column;
width: 100%;
justify-content: space-between;
align-items: center;
p {
font-size: 14px;
color: #6dc24b;
}
}
}

View File

@@ -3,11 +3,12 @@ import type { FC } from 'react';
import { redirect } from 'react-router-dom';
import { toast } from 'react-toastify';
import * as AuthenticationApi from 'api/authentication';
import { ACCESS_TOKEN } from 'api/endpoints';
import { useRequest } from 'alova';
import * as AuthenticationApi from 'components/routing/authentication';
import { useRequest } from 'alova/client';
import { LoadingSpinner } from 'components';
import { verifyAuthorization } from 'components/routing/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { Me } from 'types';
import type { RequiredChildrenProps } from 'utils';
@@ -20,12 +21,9 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const [initialized, setInitialized] = useState<boolean>(false);
const [me, setMe] = useState<Me>();
const { send: verifyAuthorization } = useRequest(
AuthenticationApi.verifyAuthorization(),
{
const { send: sendVerifyAuthorization } = useRequest(verifyAuthorization(), {
immediate: false
}
);
});
const signIn = (accessToken: string) => {
try {
@@ -43,7 +41,6 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
AuthenticationApi.clearAccessToken();
setMe(undefined);
if (doRedirect) {
// navigate('/');
redirect('/');
}
};
@@ -51,7 +48,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const refresh = useCallback(async () => {
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
await verifyAuthorization()
await sendVerifyAuthorization()
.then(() => {
setMe(AuthenticationApi.decodeMeJWT(accessToken));
setInitialized(true);

View File

@@ -94,8 +94,6 @@ const de: Translation = {
APPLICATION: 'Anwendung',
CUSTOMIZATIONS: 'Anpassungen',
APPLICATION_RESTARTING: 'EMS-ESP startet neu',
INTERFACE_BOARD_PROFILE: 'Interface Platinenprofil',
BOARD_PROFILE_TEXT: 'Wählen Sie ein vorkonfiguriertes Platinenprofil aus der Liste unten aus oder wählen Sie "Custom", um Ihre eigenen Hardwareeinstellungen zu konfigurieren',
BOARD_PROFILE: 'Platinenprofil',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
@@ -103,7 +101,7 @@ const de: Translation = {
TEMPERATURE: 'Temperatur',
PHY_TYPE: 'Eth PHY Typ',
DISABLED: 'deaktiviert',
TX_MODE: 'Tx Modus',
TX_MODE: 'EMS Tx Modus',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Allgemeine Optionen',
@@ -128,7 +126,7 @@ const de: Translation = {
BOOLEAN_FORMAT_API: 'Boolesches Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Parasitäre Stomversorgung',
ENABLE_PARASITE: '1-wire Parasitäre Stomversorgung',
LOGGING: 'Protokollierung',
LOG_HEX: 'EMS-Telegramme hexadezimal protokollieren',
ENABLE_SYSLOG: 'Syslog aktivieren',
@@ -169,7 +167,7 @@ const de: Translation = {
SYSTEM: 'System',
LOG_OF: '{0}protokoll',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Hoch-/Herunterladen',
DOWNLOAD_UPLOAD: 'Herunterladen/Hochladen',
VERSION_ON: 'Sie verwenden derzeit',
CLOSE: 'Schließen',
USE: 'Verwenden Sie',
@@ -194,7 +192,6 @@ const de: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Herunterladen geplanter Befehle',
DOWNLOAD_SETTINGS_TEXT: 'Herunterladen der Anwendungseinstellungen. Vorsicht beim Teilen der Einstellungen, da sie Passwörter und andere sensitive Einstellungen enthalten',
UPLOAD_TEXT: 'Hochladen von neuer Firmware (.bin), Geräte- oder Entitätseinstellungen (.json), zur optionalen Validitätsprüfung zuerst die (.md5) Datei hochladen',
UPLOADING: 'Hochladen',
UPLOAD_DROP_TEXT: 'Klicken Sie hier, oder ziehen eine Datei hierher',
ERROR: 'Unerwarteter Fehler, bitter versuchen Sie es erneut',
TIME_SET: 'Zeit gesetzt',
@@ -325,7 +322,7 @@ const de: Translation = {
SYSTEM_MEMORY: 'Systemspeicher',
APPLICATION_SETTINGS_1: 'Ändern Sie die EMS-ESP-Anwendungseinstellungen',
SECURITY_1: 'Benutzer hinzufügen oder entfernen',
UPLOAD_DOWNLOAD_1: 'Einstellungen und Firmware hochladen/herunterladen',
DOWNLOAD_UPLOAD_1: 'Einstellungen und Firmware herunterladen/hochladen',
MODULES: 'Module',
MODULES_1: 'Externe Module aktivieren oder deaktivieren',
MODULES_UPDATED: 'Module aktualisiert',
@@ -333,7 +330,10 @@ const de: Translation = {
MODULES_NONE: 'Keine externen Module erkannt',
RENAME: 'Umbenennen',
ENABLE_MODBUS: 'Modbus aktivieren',
VIEW_LOG: 'Sehen Sie sich das Protokoll an, um Probleme zu diagnostizieren'
VIEW_LOG: 'Sehen Sie sich das Protokoll an, um Probleme zu diagnostizieren',
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default de;

View File

@@ -94,8 +94,6 @@ const en: Translation = {
APPLICATION: 'Application',
CUSTOMIZATIONS: 'Customizations',
APPLICATION_RESTARTING: 'EMS-ESP is restarting',
INTERFACE_BOARD_PROFILE: 'Interface Board Profile',
BOARD_PROFILE_TEXT: 'Select a pre-configured interface board profile from the list below or choose Custom to configure your own hardware settings',
BOARD_PROFILE: 'Board Profile',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
@@ -103,7 +101,7 @@ const en: Translation = {
TEMPERATURE: 'Temperature',
PHY_TYPE: 'Eth PHY Type',
DISABLED: 'disabled',
TX_MODE: 'Tx Mode',
TX_MODE: 'EMS Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'General Options',
@@ -124,11 +122,11 @@ const en: Translation = {
TRIGGER_TIME: 'Trigger Time',
COLD_SHOT_DURATION: 'Cold Shot Duration',
FORMATTING_OPTIONS: 'Formatting Options',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean Format Dashboard',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean Format Web',
BOOLEAN_FORMAT_API: 'Boolean Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Enable parasite power',
ENABLE_PARASITE: 'Enable 1-Wire Parasite-Power',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrams in hexadecimal',
ENABLE_SYSLOG: 'Enable Syslog',
@@ -169,7 +167,7 @@ const en: Translation = {
SYSTEM: 'System',
LOG_OF: '{0} Log',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
DOWNLOAD_UPLOAD: 'Download/Upload',
VERSION_ON: 'You are currently on version',
CLOSE: 'Close',
USE: 'Use',
@@ -194,7 +192,6 @@ const en: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events',
DOWNLOAD_SETTINGS_TEXT: 'Download the application settings. Be careful when sharing your settings as this file contains passwords and other sensitive system information',
UPLOAD_TEXT: 'Upload a new firmware (.bin) file, settings or customizations (.json) file below, for optional validation upload (.md5) first',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Drop file or click here',
ERROR: 'Unexpected Error, please try again',
TIME_SET: 'Time set',
@@ -252,9 +249,9 @@ const en: Translation = {
TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'always',
AP_PROVIDE_TEXT_2: 'when WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'never',
AP_PROVIDE_TEXT_1: 'Always',
AP_PROVIDE_TEXT_2: 'When WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'Never',
AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID',
AP_CLIENTS: 'AP Clients',
@@ -325,7 +322,7 @@ const en: Translation = {
SYSTEM_MEMORY: 'System Memory',
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings',
SECURITY_1: 'Add or remove users',
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware',
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware',
MODULES: 'Modules',
MODULES_1: 'Activate or deactivate external modules',
MODULES_UPDATED: 'Modules updated',
@@ -333,7 +330,10 @@ const en: Translation = {
MODULES_NONE: 'No external modules detected',
RENAME: 'Rename',
ENABLE_MODBUS: 'Enable Modbus',
VIEW_LOG: 'View log to diagnose issues'
VIEW_LOG: 'View log to diagnose issues',
UPLOAD_DRAG: 'drag and drop a file here or click to select one',
SERVICES: 'Services',
ALLVALUES: 'All Values'
};
export default en;

View File

@@ -94,8 +94,6 @@ const fr: Translation = {
APPLICATION: "l'application",
CUSTOMIZATIONS: 'Personnalisation',
APPLICATION_RESTARTING: 'EMS-ESP redémarre',
INTERFACE_BOARD_PROFILE: "Profile de carte d'interface",
BOARD_PROFILE_TEXT: "Sélectionnez un profil de carte d'interface préconfiguré dans la liste ci-dessous ou choisissez Personnalisé pour configurer vos propres paramètres matériels",
BOARD_PROFILE: 'Profil de carte',
CUSTOM: 'Personnalisé',
GPIO_OF: 'GPIO {0}',
@@ -103,7 +101,7 @@ const fr: Translation = {
TEMPERATURE: 'Température',
PHY_TYPE: 'Eth PHY Type',
DISABLED: 'désactivé',
TX_MODE: 'Tx Mode',
TX_MODE: 'EMS Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Options générales',
@@ -128,7 +126,7 @@ const fr: Translation = {
BOOLEAN_FORMAT_API: 'Format booléen API/MQTT',
ENUM_FORMAT: 'Format enum API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Activer la puissance parasite',
ENABLE_PARASITE: 'Activer la puissance 1-wire parasite',
LOGGING: 'Journal',
LOG_HEX: 'Enregistrer les télégrammes EMS en hexadécimal',
ENABLE_SYSLOG: 'Activer les logs système',
@@ -169,7 +167,7 @@ const fr: Translation = {
SYSTEM: 'Système',
LOG_OF: '{0} Log',
STATUS_OF: 'Statut {0}',
UPLOAD_DOWNLOAD: 'Upload/Download',
DOWNLOAD_UPLOAD: 'Download/Upload', // TODO translate
VERSION_ON: 'You are currently on', // TODO translate
CLOSE: 'Fermer',
USE: 'Utiliser',
@@ -194,7 +192,6 @@ const fr: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', // TODO translate
DOWNLOAD_SETTINGS_TEXT: "Téléchargez les paramètres de l'application. Soyez prudent lorsque vous partagez vos paramètres car ce fichier contient des mots de passe et d'autres informations système sensibles.",
UPLOAD_TEXT: "Téléchargez un nouveau fichier de firmware (.bin), un fichier de paramètres ou de personnalisations (.json) ci-dessous, pour une validation optionnelle téléchargez d'abord un fichier (.md5)",
UPLOADING: 'Téléchargement',
UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici',
ERROR: 'Erreur inattendue, veuillez réessayer',
TIME_SET: 'Time set',
@@ -325,7 +322,7 @@ const fr: Translation = {
SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware', // TODO translate
MODULES: 'Module', // TODO translate
MODULES_1: 'Activer ou désactiver les modules externes', // TODO translate
MODULES_UPDATED: 'Modules updated', // TODO translate
@@ -333,7 +330,10 @@ const fr: Translation = {
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Activer Modbus',
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default fr;

View File

@@ -94,8 +94,6 @@ const it: Translation = {
APPLICATION: 'Applicazione',
CUSTOMIZATIONS: 'Personalizzazione',
APPLICATION_RESTARTING: 'EMS-ESP sta riavviando',
INTERFACE_BOARD_PROFILE: 'Profilo scheda di interfaccia',
BOARD_PROFILE_TEXT: 'Selezionare un profilo di interfaccia pre-configurato dalla lista sottostante o scegliere un profilo personalizzato per configurare le impostazioni del tuo hardware',
BOARD_PROFILE: 'Profilo Scheda',
CUSTOM: 'Personalizzazione',
GPIO_OF: 'GPIO {0}',
@@ -103,7 +101,7 @@ const it: Translation = {
TEMPERATURE: 'Temperatura',
PHY_TYPE: 'Eth PHY Type',
DISABLED: 'disattivato',
TX_MODE: 'Modo Tx ',
TX_MODE: 'EMS Modo Tx ',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Opzioni Generali',
@@ -128,7 +126,7 @@ const it: Translation = {
BOOLEAN_FORMAT_API: 'Formato booleano API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Indice',
ENABLE_PARASITE: 'Abilita potenza parassita',
ENABLE_PARASITE: 'Abilita potenza 1-wire parassita',
LOGGING: 'Registrazione',
LOG_HEX: 'Registra telegrammi EMS in esadecimale',
ENABLE_SYSLOG: 'Abilita Syslog',
@@ -169,7 +167,7 @@ const it: Translation = {
SYSTEM: 'Sistema',
LOG_OF: 'Registro {0}',
STATUS_OF: 'Stato {0}',
UPLOAD_DOWNLOAD: 'Caricamento/Scaricamento',
DOWNLOAD_UPLOAD: 'Scaricamento/Caricamento',
VERSION_ON: 'Attualmente stai eseguendo la versione',
CLOSE: 'Chiudere',
USE: 'Usa',
@@ -194,7 +192,6 @@ const it: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events',
DOWNLOAD_SETTINGS_TEXT: 'Scarica le impostazioni dell applicazione. Fai attenzione quando condividi le tue impostazioni poiché questo file contiene password e altre informazioni di sistema riservate',
UPLOAD_TEXT: 'Carica un nuovo file firmware (.bin) , file delle impostazioni o delle personalizzazioni (.json) di seguito, per un opzione di convalida scaricare dapprima un file "*.MD5" ',
UPLOADING: 'Caricamento',
UPLOAD_DROP_TEXT: 'Trascina il file o clicca qui',
ERROR: 'Errore Inaspettato, prego tenta ancora',
TIME_SET: 'Imposta Ora',
@@ -325,7 +322,7 @@ const it: Translation = {
SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware', // TODO translate
MODULES: 'Module', // TODO translate
MODULES_1: 'Attiva o disattiva i moduli esterni', // TODO translate
MODULES_UPDATED: 'Modules updated', // TODO translate
@@ -333,7 +330,10 @@ const it: Translation = {
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Abilita Modbus',
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default it;

View File

@@ -94,15 +94,13 @@ const nl: Translation = {
APPLICATION: 'Applicatie',
CUSTOMIZATIONS: 'Aanpassingen van entiteiten',
APPLICATION_RESTARTING: 'EMS-ESP herstarten',
INTERFACE_BOARD_PROFILE: 'Interface Apparaatprofiel',
BOARD_PROFILE_TEXT: 'Selecteer een vooraf ingesteld apparaat profiel uit de lijst of kies Eigen om zelf uw hardware te configureren',
BOARD_PROFILE: 'Apparaatprofiel',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
BUTTON: 'Toets',
TEMPERATURE: 'Temperatuur',
PHY_TYPE: 'Eth PHY Type',
TX_MODE: 'Tx Mode',
TX_MODE: 'EMS Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
DISABLED: 'Uitgeschakeld',
@@ -124,11 +122,11 @@ const nl: Translation = {
TRIGGER_TIME: 'Trigger tijd',
COLD_SHOT_DURATION: 'Tijd Shot koud water',
FORMATTING_OPTIONS: 'Formatteringsopties',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean formaat dashboard',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean formaat web',
BOOLEAN_FORMAT_API: 'Boolean formaat API/MQTT',
ENUM_FORMAT: 'Enum formaat API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Activeer parasitaire modus',
ENABLE_PARASITE: 'Activeer 1-wire parasitaire modus',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrammen in hexadecimaal',
ENABLE_SYSLOG: 'Activeer Syslog',
@@ -169,7 +167,7 @@ const nl: Translation = {
SYSTEM: 'Systeem',
LOG_OF: '{0} Log',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
DOWNLOAD_UPLOAD: 'Download/Upload',
VERSION_ON: 'U bevindt zich momenteel op versie',
CLOSE: 'Sluiten',
USE: 'Gebruik',
@@ -194,7 +192,6 @@ const nl: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events',
DOWNLOAD_SETTINGS_TEXT: 'Download de applicatie settings. Wees voorzichting met het delen van dit bestand want het bevat o.a. de wachtwoorden in plain text',
UPLOAD_TEXT: 'Upload een nieuwe firmware (.bin) file, instellingen of custom instellingen (.json) bestand hieronder',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Sleep bestand hierheen of klik hier',
ERROR: 'Onverwachte fout, probeer opnieuw',
TIME_SET: 'Tijd ingesteld',
@@ -325,7 +322,7 @@ const nl: Translation = {
SYSTEM_MEMORY: 'System Geheugen',
APPLICATION_SETTINGS_1: 'Applicatie-instellingen wijzigen',
SECURITY_1: 'Gebruikers toevoegen of verwijderen',
UPLOAD_DOWNLOAD_1: 'Upload-/downloadinstellingen en firmware',
DOWNLOAD_UPLOAD_1: 'Download en upload instellingen en firmware',
MODULES: 'Module',
MODULES_1: 'Externe modules activeren of deactiveren', // TODO translate
MODULES_UPDATED: 'Modules geüpdatet',
@@ -333,7 +330,10 @@ const nl: Translation = {
MODULES_NONE: 'Geen externe modules gedetecteerd',
RENAME: 'Hernoemen',
ENABLE_MODBUS: 'Activeer Modbus',
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default nl;

View File

@@ -94,8 +94,6 @@ const no: Translation = {
APPLICATION: 'Søknad',
CUSTOMIZATIONS: 'Tilpasninger',
APPLICATION_RESTARTING: 'EMS-ESP restarter',
INTERFACE_BOARD_PROFILE: 'Interface Prosessor Profil',
BOARD_PROFILE_TEXT: 'Velg en pre-konfigurert prosessor profil fra listen under eller velg Tilpasset for å konfigurere dine egne innstillinger',
BOARD_PROFILE: 'Prosessor Profil',
CUSTOM: 'Custom',
GPIO_OF: '{0} GPIO',
@@ -103,7 +101,7 @@ const no: Translation = {
TEMPERATURE: 'Temperatur',
PHY_TYPE: 'Eth PHY Type',
DISABLED: 'avslått',
TX_MODE: 'Tx Mode',
TX_MODE: 'EMS Tx Mode',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Generelle Innstillinger',
@@ -124,11 +122,11 @@ const no: Translation = {
TRIGGER_TIME: 'Aktiveringstid',
COLD_SHOT_DURATION: 'Tid på kaldt vann',
FORMATTING_OPTIONS: 'Formatteringsalternativs',
BOOLEAN_FORMAT_DASHBOARD: 'Bool Format Dashboard',
BOOLEAN_FORMAT_DASHBOARD: 'Bool Format Web',
BOOLEAN_FORMAT_API: 'Bool Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Indeks',
ENABLE_PARASITE: 'Aktiver parasitt strømforsyning',
ENABLE_PARASITE: 'Aktiver 1-wire parasitt strømforsyning',
LOGGING: 'Logging',
LOG_HEX: 'Logg EMS telegrammer i hexadesimal',
ENABLE_SYSLOG: 'Aktiver Syslog',
@@ -169,7 +167,7 @@ const no: Translation = {
SYSTEM: 'System',
LOG_OF: '{0} Logg',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Opp/Nedlasting',
DOWNLOAD_UPLOAD: 'Nedlasting/Opp',
VERSION_ON: 'You are currently on', // TODO translate
CLOSE: 'Steng',
USE: 'Bruk',
@@ -194,7 +192,6 @@ const no: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Last ned planlagte oppgaver',
DOWNLOAD_SETTINGS_TEXT: 'Last ned applikasjonskonfigurasjon. Vær varsom med å dele fila da den inneholder passord og annen sensitiv system informasjon',
UPLOAD_TEXT: 'Last opp en ny firmware (.bin) fil, innstillinger eller tilpassninger (.json) fil nedenfor',
UPLOADING: 'Opplasting',
UPLOAD_DROP_TEXT: 'Slipp fil eller klikk her',
ERROR: 'Ukjent feil, prøv igjen',
TIME_SET: 'Still in tid',
@@ -325,7 +322,7 @@ const no: Translation = {
SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware', // TODO translate
MODULES: 'Module', // TODO translate
MODULES_1: 'Aktiver eller deaktiver eksterne moduler', // TODO translate
MODULES_UPDATED: 'Modules updated', // TODO translate
@@ -333,7 +330,10 @@ const no: Translation = {
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Aktiver Modbus',
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default no;

View File

@@ -94,8 +94,6 @@ const pl: BaseTranslation = {
APPLICATION: 'Aplikacji',
CUSTOMIZATIONS: 'Personalizacja',
APPLICATION_RESTARTING: 'Trwa ponowne uruchamianie',
INTERFACE_BOARD_PROFILE: 'Profil płytki interfejsu',
BOARD_PROFILE_TEXT: 'Wybierz z listy gotowy profil płytki interfejsu lub "własny..." i samodzielnie skonfiguruj posiadany sprzęt.',
BOARD_PROFILE: 'Profil płytki',
CUSTOM: 'własny',
GPIO_OF: 'GPIO {0}',
@@ -103,7 +101,7 @@ const pl: BaseTranslation = {
TEMPERATURE: '1-Wire®',
PHY_TYPE: 'Typ układu ethernetowego (PHY)',
DISABLED: '{{wyłączono|brak|}}',
TX_MODE: 'Tryb transmisji (Tx)',
TX_MODE: 'EMS Tryb transmisji (Tx)',
EMS_BUS: '{{magistrali EMS|na magistrali|}}',
HARDWARE: 'sprzętowy',
GENERAL_OPTIONS: 'Opcje podstawowe',
@@ -128,7 +126,7 @@ const pl: BaseTranslation = {
BOOLEAN_FORMAT_API: 'Wartości dwustanowe w API/MQTT',
ENUM_FORMAT: 'Wartości z listy w API/MQTT',
INDEX: 'indeks',
ENABLE_PARASITE: 'Aktywuj zasilanie pasożytnicze',
ENABLE_PARASITE: 'Aktywuj zasilanie 1-wire pasożytnicze',
LOGGING: 'Logowanie',
LOG_HEX: 'Loguj telegramy EMS w systemie szesnastkowym (hex)',
ENABLE_SYSLOG: 'Aktywuj SysLog',
@@ -169,7 +167,7 @@ const pl: BaseTranslation = {
SYSTEM: '{{S|s||s}}yste{{m|mu||mowy}}',
LOG_OF: 'Log {0}',
STATUS_OF: 'Status {0}',
UPLOAD_DOWNLOAD: 'Przesyłanie plików',
DOWNLOAD_UPLOAD: 'Plików przesyłanie',
VERSION_ON: 'Aktualnie używasz',
CLOSE: 'Zamknij',
USE: 'Aby zaktualizować firmware skorzystaj z funkcji',
@@ -194,7 +192,6 @@ const pl: BaseTranslation = {
DOWNLOAD_SCHEDULE_TEXT: 'Pobierz harmonogram zdarzeń.',
DOWNLOAD_SETTINGS_TEXT: 'Pobierz ustawienia aplikacji. Uwaga! Plik z ustawieniami zawiera hasła oraz inne wrażliwe informacje systemowe! Nie udostepniaj go pochopnie!',
UPLOAD_TEXT: 'Wyślij firmware (.bin), ustawienia lub personalizacje (.json). Opcjonalnie, wyślij wcześniej plik walidacji z sumą kontrolną (.md5).',
UPLOADING: 'Wysłano',
UPLOAD_DROP_TEXT: 'Przeciągnij tutaj plik lub kliknij',
ERROR: 'Nieoczekiwany błąd, spróbuj ponownie!',
TIME_SET: 'Zegar został ustawiony.',
@@ -325,7 +322,7 @@ const pl: BaseTranslation = {
SYSTEM_MEMORY: 'Pamięć systemowa',
APPLICATION_SETTINGS_1: 'Modyfikacja ustawień aplikacji EMS-ESP',
SECURITY_1: 'Dodawanie i usuwanie użytkowników',
UPLOAD_DOWNLOAD_1: 'Wysyłanie/pobieranie ustawień i firmware',
DOWNLOAD_UPLOAD_1: 'Pobieranie/wysyłanie ustawień i firmware',
MODULES: 'Module', // TODO translate
MODULES_1: 'Aktywuj lub dezaktywuj moduły zewnętrzne', // TODO translate
MODULES_UPDATED: 'Modules updated', // TODO translate
@@ -333,7 +330,10 @@ const pl: BaseTranslation = {
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Aktywuj Modbus',
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default pl;

View File

@@ -94,8 +94,6 @@ const sk: Translation = {
APPLICATION: 'Aplikácie',
CUSTOMIZATIONS: 'Prispôsobenia',
APPLICATION_RESTARTING: 'EMS-ESP sa reštartuje',
INTERFACE_BOARD_PROFILE: 'Profil dosky rozhrania',
BOARD_PROFILE_TEXT: 'Vyberte vopred nakonfigurovaný profil dosky rozhrania zo zoznamu nižšie, alebo vyberte možnosť Vlastné a nakonfigurujte svoje vlastné hardvérové nastavenia',
BOARD_PROFILE: 'Profil dosky',
CUSTOM: 'Vlastné',
GPIO_OF: '{0} GPIO',
@@ -103,7 +101,7 @@ const sk: Translation = {
TEMPERATURE: 'Teplota',
PHY_TYPE: 'Eth PHY Typ',
DISABLED: 'zakázané',
TX_MODE: 'Tx režim',
TX_MODE: 'EMS Tx režim',
HARDWARE: 'Hardware',
EMS_BUS: '{{BUS|EMS BUS}}',
GENERAL_OPTIONS: 'Všeobecné možnosti',
@@ -124,11 +122,11 @@ const sk: Translation = {
TRIGGER_TIME: 'Čas spustenia',
COLD_SHOT_DURATION: 'Trvanie studeného záberu',
FORMATTING_OPTIONS: 'Možnosti formátovania',
BOOLEAN_FORMAT_DASHBOARD: 'Panel Boolean formát',
BOOLEAN_FORMAT_DASHBOARD: 'Web panel Boolean formát',
BOOLEAN_FORMAT_API: 'Boolean formát API/MQTT',
ENUM_FORMAT: 'Enum formát API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Povoliť parazité napájanie DS18B20',
ENABLE_PARASITE: 'Povoliť 1-wire parazité napájanie DS18B20',
LOGGING: 'Logovanie',
LOG_HEX: 'Záznam telegramov EMS v hexadecimálnej sústave',
ENABLE_SYSLOG: 'Povoliť Syslog',
@@ -169,7 +167,7 @@ const sk: Translation = {
SYSTEM: 'Systém',
LOG_OF: '{0} Log',
STATUS_OF: '{0} Stav',
UPLOAD_DOWNLOAD: 'Nahrať/Stiahnuť',
DOWNLOAD_UPLOAD: 'Stiahnuť/Nahrať',
VERSION_ON: 'Momentálne nainštalovaná verzia: ',
CLOSE: 'Zatvoriť',
USE: 'Použiť',
@@ -194,7 +192,6 @@ const sk: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Stiahnutie plánovača udalostí',
DOWNLOAD_SETTINGS_TEXT: 'Stiahnite si nastavenia aplikácie. Pri zdieľaní nastavení buďte opatrní, pretože tento súbor obsahuje heslá a iné citlivé systémové informácie.',
UPLOAD_TEXT: 'Najskôr nahrajte nový súbor firmvéru (.bin), nastavenia alebo prispôsobenia (.json), pre voliteľné overenie nahrajte súbor (.md5)',
UPLOADING: 'Nahrávanie',
UPLOAD_DROP_TEXT: 'Potiahnúť a pripnúť súbor alebo kliknúť sem',
ERROR: 'Neočakávaná chyba, prosím skúste to znova',
TIME_SET: 'Nastavený čas',
@@ -325,7 +322,7 @@ const sk: Translation = {
SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware', // TODO translate
MODULES: 'Module', // TODO translate
MODULES_1: 'Aktivujte alebo deaktivujte externé moduly', // TODO translate
MODULES_UPDATED: 'Modules updated', // TODO translate
@@ -333,7 +330,10 @@ const sk: Translation = {
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Povoliť Modbus',
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default sk;

View File

@@ -94,8 +94,6 @@ const sv: Translation = {
APPLICATION: 'Apliká',
CUSTOMIZATIONS: 'Anpassningr',
APPLICATION_RESTARTING: 'EMS-ESP startar om',
INTERFACE_BOARD_PROFILE: 'Interface Hårdvaruprofil',
BOARD_PROFILE_TEXT: 'Välj en förkonfigurerad hårdvaruprofil från listan nedan eller välj Anpassad för att konfigurera dina egna hårdvaruinställningar',
BOARD_PROFILE: 'Hårdvarutyp',
CUSTOM: 'Anpassa',
GPIO_OF: '{0} GPIO',
@@ -103,7 +101,7 @@ const sv: Translation = {
TEMPERATURE: 'Temperatur',
PHY_TYPE: 'Eth PHY-typ',
DISABLED: 'inaktiverad',
TX_MODE: 'Tx-läge',
TX_MODE: 'EMS Tx-läge',
HARDWARE: 'Hårdvara',
EMS_BUS: '{{BUSS|EMS-BUSS}}',
GENERAL_OPTIONS: 'Allmänna Inställningar',
@@ -128,7 +126,7 @@ const sv: Translation = {
BOOLEAN_FORMAT_API: 'Bool-format API/MQTT',
ENUM_FORMAT: 'Enum-format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Aktivera parasitström',
ENABLE_PARASITE: 'Aktivera 1-wire parasitström',
LOGGING: 'Loggning',
LOG_HEX: 'Logga EMS-telegram i hexadecimal',
ENABLE_SYSLOG: 'Aktivera Syslog',
@@ -169,7 +167,7 @@ const sv: Translation = {
SYSTEM: 'System',
LOG_OF: '{0} Logg',
STATUS_OF: '{0} Status',
UPLOAD_DOWNLOAD: 'Upp/Nedladdning',
DOWNLOAD_UPLOAD: 'Nedladdning/Upp',
VERSION_ON: 'You are currently on', // TODO translate
CLOSE: 'Stäng',
USE: 'Använd',
@@ -194,7 +192,6 @@ const sv: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', // TODO translate
DOWNLOAD_SETTINGS_TEXT: 'Ladda ner applikationsinställningar. Var försiktig om du delar dina iställlningar då de innehåller lösenord och annan känslig systeminformation',
UPLOAD_TEXT: 'Ladda upp ett nytt firmware (.bin), inställningar eller anpassningar (.json) nedan',
UPLOADING: 'Laddar upp',
UPLOAD_DROP_TEXT: 'Släpp fil eller klicka här',
ERROR: 'Okänt Fel, var god försök igen',
TIME_SET: 'Ställ in tid',
@@ -325,7 +322,7 @@ const sv: Translation = {
SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware', // TODO translate
MODULES: 'Module', // TODO translate
MODULES_1: 'Aktivera eller avaktivera externa moduler', // TODO translate
MODULES_UPDATED: 'Modules updated', // TODO translate
@@ -333,7 +330,10 @@ const sv: Translation = {
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Aktivera Modbus',
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default sv;

View File

@@ -94,8 +94,6 @@ const tr: Translation = {
APPLICATION: 'Uygulama',
CUSTOMIZATIONS: 'Özelleştirme',
APPLICATION_RESTARTING: 'EMS-ESP yeniden başlatılıyor',
INTERFACE_BOARD_PROFILE: 'Arabirim Kart Profili',
BOARD_PROFILE_TEXT: 'Aşağıdan hazır kart profillerinden birini seçin yada kendi donanımınızı ayarlamak için Özeli tercih edin',
BOARD_PROFILE: 'Kart Profili',
CUSTOM: 'Özel',
GPIO_OF: '{0} GPIO',
@@ -103,7 +101,7 @@ const tr: Translation = {
TEMPERATURE: 'Sıcaklık',
PHY_TYPE: 'Eth PHY Tipi',
DISABLED: 'devre dışı',
TX_MODE: 'Tx Modu',
TX_MODE: 'EMS Tx Modu',
HARDWARE: 'Donanım',
EMS_BUS: '{{HAT|EMS HATTI}}',
GENERAL_OPTIONS: 'Genel Seçenekler',
@@ -128,7 +126,7 @@ const tr: Translation = {
BOOLEAN_FORMAT_API: 'Boolean Biçimleme API/MQTT',
ENUM_FORMAT: 'Enum Biçimleme API/MQTT',
INDEX: 'İndeks',
ENABLE_PARASITE: 'Parazit gücü devreye al',
ENABLE_PARASITE: '1-wire parazit gücü devreye al',
LOGGING: 'Kayıt ediliyor',
LOG_HEX: 'EMS telegramlarını hexadecimal olarak kayıt et',
ENABLE_SYSLOG: 'Sistem Kaydını Devreye Al',
@@ -169,7 +167,7 @@ const tr: Translation = {
SYSTEM: 'Sistem',
LOG_OF: '{0} Kaydı',
STATUS_OF: '{0} Durumu',
UPLOAD_DOWNLOAD: 'Yükleme/İndirme',
DOWNLOAD_UPLOAD: 'İndirme/Yükleme',
VERSION_ON: 'You are currently on', // TODO translate
CLOSE: 'Kapat',
USE: 'KUllan',
@@ -194,7 +192,6 @@ const tr: Translation = {
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', // TODO translate
DOWNLOAD_SETTINGS_TEXT: 'Uygulama ayarlarını indir. Bu dosya hassas sistem bilgileri ve şifrelerinizi içerdiğinden ayarlarınızı paylaşırken dikkatli olun',
UPLOAD_TEXT: 'Yeni bir bellenim(.bin) dosyası yükleyin, ayarlar ve özelleştirmeler(.json) dosyası aşağıda, sçenekli denetim yüklemesi(.md5) için önce',
UPLOADING: 'Yüklüyor',
UPLOAD_DROP_TEXT: 'Buraya tıklayın yada dosyayı sürükleyip bırakın',
ERROR: 'Beklenemedik hata, lütfen tekrar deneyin.',
TIME_SET: 'Zaman ayarı',
@@ -325,7 +322,7 @@ const tr: Translation = {
SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate
SECURITY_1: 'Add or remove users', // TODO translate
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', // TODO translate
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware', // TODO translate
MODULES: 'Module', // TODO translate
MODULES_1: 'Harici modülleri etkinleştirin veya devre dışı bırakın', // TODO translate
MODULES_UPDATED: 'Modules updated', // TODO translate
@@ -333,7 +330,10 @@ const tr: Translation = {
MODULES_NONE: 'No external modules detected', // TODO translate
RENAME: 'Rename', // TODO translate
ENABLE_MODBUS: 'Enable Modbus', // TODO translate
VIEW_LOG: 'View log to diagnose issues' // TODO translate
VIEW_LOG: 'View log to diagnose issues', // TODO translate
UPLOAD_DRAG: 'drag and drop a file here or click to select one', // TODO translate
SERVICES: 'Services', // TODO translate
ALLVALUES: 'All Values' // TODO translate
};
export default tr;

View File

@@ -2,51 +2,42 @@ import { useState } from 'react';
import { useBlocker } from 'react-router-dom';
import { toast } from 'react-toastify';
import { type Method, useRequest } from 'alova';
import type { AlovaGenerics, Method } from 'alova';
import { useRequest } from 'alova/client';
import { useI18nContext } from 'i18n/i18n-react';
export interface RestRequestOptions<D> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
read: () => Method<any, any, any, any, any, any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
update: (value: D) => Method<any, any, any, any, any, any, any>;
read: () => Method<AlovaGenerics>;
update: (value: D) => Method<AlovaGenerics>;
}
export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
const { LL } = useI18nContext();
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 {
data,
send: readData,
update: updateData,
onComplete: onReadComplete
} = useRequest(read());
update: updateData
} = useRequest(read()).onComplete((event) => {
setOrigData(event.data as D);
});
const {
loading: saving,
send: writeData,
onSuccess: onWriteSuccess
} = useRequest((newData: D) => update(newData), { immediate: false });
const updateDataValue = (new_data: D) => {
updateData({ data: new_data });
};
onWriteSuccess(() => {
const { loading: saving, send: writeData } = useRequest(
(newData: D) => update(newData),
{ immediate: false }
).onSuccess(() => {
toast.success(LL.UPDATED_OF(LL.SETTINGS(1)));
setDirtyFlags([]);
});
onReadComplete((event) => {
setOrigData(event.data as D);
});
const updateDataValue = (new_data: D) => {
updateData({ data: new_data });
};
const loadData = async () => {
setDirtyFlags([]);
@@ -74,7 +65,6 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
}
});
};
return {
loadData,
saveData,

View File

@@ -12,7 +12,7 @@ export const validate = <T extends object>(
options ? options : {},
(errors, fieldErrors) => {
if (errors) {
reject(fieldErrors);
reject(fieldErrors as Error);
} else {
resolve(source as T);
}

View File

@@ -4,11 +4,13 @@ import { defineConfig } from 'vite';
import viteImagemin from 'vite-plugin-imagemin';
import viteTsconfigPaths from 'vite-tsconfig-paths';
import mockServer from '../mock-api/mockServer.js';
export default defineConfig(({ command, mode }) => {
if (command === 'serve') {
console.log('Preparing for standalone build with server, mode=' + mode);
return {
plugins: [preact(), viteTsconfigPaths()],
plugins: [preact(), viteTsconfigPaths(), mockServer()],
server: {
open: true,
port: mode == 'production' ? 4173 : 3000,
@@ -18,12 +20,6 @@ export default defineConfig(({ command, mode }) => {
changeOrigin: true,
secure: false
},
'/es': {
target: 'http://localhost:3081',
changeOrigin: true,
secure: false
},
'/rest/uploadFile': 'http://localhost:3082', // this must come first to work!
'/rest': 'http://localhost:3080'
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -75,6 +75,7 @@ void APSettingsService::manageAP() {
}
void APSettingsService::startAP() {
WiFi.softAPenableIpV6(); // force IPV6, same as for WiFi - fixes https://github.com/emsesp/EMS-ESP32/issues/1922
WiFi.softAPConfig(_state.localIP, _state.gatewayIP, _state.subnetMask);
esp_wifi_set_bandwidth(static_cast<wifi_interface_t>(ESP_IF_WIFI_AP), WIFI_BW_HT20);
WiFi.softAP(_state.ssid.c_str(), _state.password.c_str(), _state.channel, _state.ssidHidden, _state.maxClients);

View File

@@ -1,69 +0,0 @@
import express from 'express';
const rest_server = express();
const port = 3081;
const ES_ENDPOINT_ROOT = '/es/';
const ES_LOG_ENDPOINT = ES_ENDPOINT_ROOT + 'log';
const INTERVAL = 1000;
function pad(number) {
var r = String(number);
if (r.length === 1) {
r = '0' + r;
}
return r;
}
// e.g. 2024-03-29 07:02:37.856
Date.prototype.toISOString = function () {
return (
this.getUTCFullYear() +
'-' +
pad(this.getUTCMonth() + 1) +
'-' +
pad(this.getUTCDate()) +
' ' +
pad(this.getUTCHours()) +
':' +
pad(this.getUTCMinutes()) +
':' +
pad(this.getUTCSeconds()) +
'.' +
String((this.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5)
);
};
rest_server.get(ES_LOG_ENDPOINT, (_req, res) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
});
let count = 0;
const interval = setInterval(() => {
const data = {
t: new Date().toISOString(),
l: (3 + (count % 6)),
i: count,
n: 'system',
m: 'message #' + count++
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, INTERVAL);
// if client closes connection
res.on('close', () => {
console.log('Closing ES connection');
clearInterval(interval);
res.end();
});
});
// start eventsource server
rest_server.listen(port, () => console.log(`EMS-ESP EventSource mock server running on http://localhost:${port}/`));

123
mock-api/mockServer.js Normal file
View File

@@ -0,0 +1,123 @@
import formidable from 'formidable';
function pad(number) {
var r = String(number);
if (r.length === 1) {
r = '0' + r;
}
return r;
}
// e.g. 2024-03-29 07:02:37.856
Date.prototype.toISOString = function () {
return (
this.getUTCFullYear() +
'-' +
pad(this.getUTCMonth() + 1) +
'-' +
pad(this.getUTCDate()) +
' ' +
pad(this.getUTCHours()) +
':' +
pad(this.getUTCMinutes()) +
':' +
pad(this.getUTCSeconds()) +
'.' +
String((this.getUTCMilliseconds() / 1000).toFixed(3)).slice(2, 5)
);
};
export default () => {
return {
name: 'vite:mockserver',
configureServer: async (server) => {
server.middlewares.use(async (req, res, next) => {
// catch any file uploads
if (req.url.startsWith('/rest/uploadFile')) {
let progress = 0;
const file_size = req.headers['content-length'];
req.on('data', async (chunk) => {
progress += chunk.length;
const percentage = (progress / file_size) * 100;
console.log(`Progress: ${Math.round(percentage)}%`);
// await new Promise((resolve) => setTimeout(() => resolve(), 3000)); // slow it down
});
const form = formidable({});
let fields;
let files;
try {
[fields, files] = await form.parse(req);
} catch (err) {
console.error(err);
res.writeHead(err.httpCode || 400, {
'Content-Type': 'text/plain'
});
res.end(String(err));
return;
}
// only process when we have a file
if (Object.keys(files).length > 0) {
const uploaded_file = files.file[0];
const file_name = uploaded_file.originalFilename;
const file_extension = file_name.substring(
file_name.lastIndexOf('.') + 1
);
console.log('Filename: ' + file_name);
console.log('Extension: ' + file_extension);
console.log('File size: ' + file_size);
if (file_extension === 'bin' || file_extension === 'json') {
console.log('File uploaded successfully!');
} else if (file_extension === 'md5') {
console.log('MD5 hash generated successfully!');
res.end(
JSON.stringify({
md5: 'ef4304fc4d9025a58dcf25d71c882d2c'
})
);
} else {
res.statusCode = 400;
console.log('Invalid file extension!');
}
}
res.end();
}
// SSE Eventsource
else if (req.url.startsWith('/es/log')) {
res.writeHead(200, {
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
});
let count = 0;
const interval = setInterval(() => {
const data = {
t: new Date().toISOString(),
l: 3 + (count % 6),
i: count,
n: 'system',
m: 'message #' + count++
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 1000);
// if client closes connection
res.on('close', () => {
console.log('Closing ES connection');
clearInterval(interval);
res.end();
});
} else {
next(); // move on to the next middleware function in chain
}
});
}
};
};

View File

@@ -6,15 +6,16 @@
"license": "MIT",
"scripts": {
"mock-rest": "bun --watch rest_server.ts",
"mock-es": "bun --watch es_server.ts",
"mock-upload": "bun --watch upload_server.ts"
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'"
},
"dependencies": {
"@msgpack/msgpack": "^2.8.0",
"@types/multer": "^1.4.11",
"express": "^4.19.2",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"eslint": "^9.9.0",
"eslint-config-prettier": "^9.1.0",
"formidable": "^3.5.1",
"itty-router": "^5.0.17",
"multer": "^1.4.5-lts.1"
"prettier": "^3.3.3"
},
"packageManager": "yarn@4.2.1"
"packageManager": "yarn@4.4.0"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
import express from 'express';
import multer from 'multer';
const rest_server = express();
const port = 3082;
const upload = multer({ dest: './uploads' });
const UPLOAD_FILE_ENDPOINT = '/rest/uploadFile';
// delay functions, 2 different types
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
function delay_blocking(milliseconds) {
var start = new Date().getTime();
while (true) {
if (new Date().getTime() - start > milliseconds) {
break;
}
}
}
function progress_middleware(req, _res, next) {
let progress = 0;
const file_size = req.headers['content-length'];
console.log('Uploading file. Size ' + file_size + ' bytes');
// 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(1000); // slow it down
});
next(); // invoke next middleware which is multer
}
rest_server.post(UPLOAD_FILE_ENDPOINT, progress_middleware, upload.single('file'), (req, res) => {
if (req.file) {
const filename = req.file.originalname;
const ext = filename.substring(filename.lastIndexOf('.') + 1);
console.log(req.file);
if (ext === 'bin' || ext === 'json') {
console.log('Received firmware or json file, extension: ' + ext);
return res.sendStatus(200);
} else if (ext === 'md5') {
return res.json({ md5: 'ef4304fc4d9025a58dcf25d71c882d2c' });
}
}
console.log('Invalid file extension');
return res.sendStatus(400);
});
// start server
rest_server.listen(port, () => console.log(`EMS-ESP File Upload mock server running on http://localhost:${port}/`));

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@
; default_envs = esp32_4M
; default_envs = esp32_16M
default_envs = lolin_s3
; default_envs = lolin_c3_mini
; default_envs = native
; default_envs = debug
; default_envs = custom
@@ -26,11 +27,11 @@ default_envs = lolin_s3
[env]
; upload settings
; for USB
upload_protocol = esptool
upload_port = /dev/ttyUSB*
; upload_protocol = esptool
; upload_port = /dev/ttyUSB*
; for OTA add scripts/upload.py to extra_scripts
; upload_protocol = custom
custom_emsesp_ip = 10.10.10.173
upload_protocol = custom
custom_emsesp_ip = 10.10.10.175
; custom_emsesp_ip = ems-esp.local
custom_username = admin
custom_password = admin
@@ -42,59 +43,25 @@ custom_password = admin
[env:native]
extra_scripts =
; pre:scripts/refresh_module_library_native.py
; post:scripts/run_native.py
[env:esp32_4M]
extra_scripts =
; pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
scripts/rename_fw.py
; scripts/upload.py
post:scripts/run_native.py
[env:lolin_s3]
extra_scripts =
; pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
scripts/rename_fw.py
; scripts/upload.py
scripts/upload.py
[env:esp32_16M]
extra_scripts =
; pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
scripts/rename_fw.py
; scripts/upload.py
[env:custom]
; use for basic ESP boards with 4MB flash
; make sure -D TASMOTA_SDK is also enabled
platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.01.00/platform-espressif32.zip
; use for S3 boards:
; platform = espressif32
framework = arduino
board = esp32dev
board_build.filesystem = littlefs
board_build.f_cpu = 240000000L
board_upload.flash_size = 4MB
board_build.partitions = esp32_partition_4M.csv
board_upload.use_1200bps_touch = false
board_upload.wait_for_upload_port = true
upload_port = /dev/ttyUSB0
[env:lolin_c3_mini]
extra_scripts =
pre:scripts/build_interface.py
; pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
scripts/rename_fw.py
build_unflags = ${common.unbuild_flags}
build_flags =
${common.core_build_flags}
${factory_settings.build_flags}
${common.my_build_flags}
-D ONEWIRE_CRC16=0
-D NO_GLOBAL_ARDUINOOTA
-D ARDUINOJSON_ENABLE_STD_STRING=1
-D TASMOTA_SDK
; -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_WARN
-D EMSESP_TEST
-D EMSESP_DEBUG
-D CONFIG_ETH_ENABLED
-D CONFIG_ASYNC_TCP_STACK_SIZE=8192
'-DEMSESP_DEFAULT_BOARD_PROFILE="Test"'
scripts/upload.py
; pio run -e debug
; or from Visual Studio Code do PIO -> Project Tasks -> debug -> General -> Upload and Monitor

View File

@@ -215,6 +215,8 @@ build_unflags = ${common.unbuild_flags}
;
; It will generate an executbale which when run will show the EMS-ESP Console where you can run tests using the `test` command.
;
; See https://docs.platformio.org/en/latest/core/installation/shell-commands.html#piocore-install-shell-commands
;
; to build and run directly on linux: pio run -e native -t exec
;
; to build and run on Windows, it needs winsock for the console input so:
@@ -245,7 +247,6 @@ build_src_flags =
-I./lib/espMqttClient/src/Transport
build_src_filter =
+<*>
-<.git/>
+<../lib_standalone>
+<../lib/uuid-common>
+<../lib/uuid-console>

View File

@@ -1,11 +1,60 @@
import shutil
import re
import os
Import("env")
def run_native():
print("Running native...")
os.system("pio run -e native")
OUTPUT_DIR = "build{}".format(os.path.sep)
if not (env.IsCleanTarget()):
run_native()
def move_file(source, target, env):
# get the build info
bag = {}
exprs = [(re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"'), 'app_version')]
with open('./src/version.h', 'r') as f:
for l in f.readlines():
for expr, var in exprs:
m = expr.match(l)
if m and len(m.groups()) > 0:
bag[var] = m.group(1)
app_version = bag.get('app_version')
platform = "native"
# this breaks the CI so removed
# flash_size = env["PIOENV"].split('_')[1]
# print(env.Dump())
# my_flags = env.ParseFlags(env['BUILD_FLAGS'])
# defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")}
# print(my_flags)
# print(my_flags.get("CPPDEFINES")
# alternatively take platform from the pio target
# platform = str(target[0]).split(os.path.sep)[2]
print("app version: " + app_version)
print("platform: " + platform)
# TODO do I need .exe for windows?
variant = "native"
# check if output directories exist and create if necessary
if not os.path.isdir(OUTPUT_DIR):
os.mkdir(OUTPUT_DIR)
# create string with location and file names based on variant
bin_file = "{}firmware{}{}".format(OUTPUT_DIR, os.path.sep, variant)
# check if new target files exist and remove if necessary
for f in [bin_file]:
if os.path.isfile(f):
os.remove(f)
print("Renaming file to "+bin_file)
shutil.copy(str(target[0]), bin_file)
print("Executing file")
os.system(bin_file)
env.AddPostAction("$BUILD_DIR/${PROGNAME}", [move_file])

View File

@@ -418,7 +418,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha
LOG_WARNING(error);
} else {
if (single_command) {
// log as DEBUG (TRACE) regarless if compiled with EMSESP_DEBUG
// log as DEBUG (TRACE) regardless if compiled with EMSESP_DEBUG
logger_.debug(("%sCalled command %s"), ro.c_str(), info_s);
} else {
if (id > 0) {

View File

@@ -691,11 +691,16 @@ std::string EMSESPShell::context_text() {
// when in su (admin) show # as the prompt suffix
std::string EMSESPShell::prompt_suffix() {
#ifndef EMSESP_UNITY
if (has_flags(CommandFlags::ADMIN)) {
return std::string{'#'};
} else {
return std::string{'$'};
}
#else
// don't bother with prompt suffix if we're testing Unity output
return "";
#endif
}
void EMSESPShell::end_of_transmission() {

View File

@@ -1553,8 +1553,6 @@ void EMSESP::start() {
#if defined(EMSESP_STANDALONE)
shell_->add_flags(CommandFlags::ADMIN); // always start in su/admin mode when running tests
#endif
#else
#warning "Shell is disabled when running Unity tests."
#endif
// start the file system

View File

@@ -1398,7 +1398,12 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
// System
node = output["system"].to<JsonObject>();
// prevent false negataive in Unity tests everytime the version changes
#if defined(EMSESP_UNITY)
node["version"] = "dev";
#else
node["version"] = EMSESP_APP_VERSION;
#endif
node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
node["uptimeSec"] = uuid::get_uptime_sec();
#ifndef EMSESP_STANDALONE

View File

@@ -1109,6 +1109,8 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
Serial.println();
Serial.printf("%s**** Testing bad urls ****\n%s", COLOR_RED, COLOR_RESET);
request.method(HTTP_GET);
request.url("/api/boiler2");
EMSESP::webAPIService.webAPIService(&request);

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.7.0-dev.30"
#define EMSESP_APP_VERSION "3.7.0-dev.31"

View File

@@ -134,7 +134,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
const char * api_data = output["api_data"];
if (api_data) {
request->send(200, "text/plain; charset=utf-8", api_data);
#if defined(EMSESP_TEST)
#if defined(EMSESP_UNITY)
// store the result so we can test with Unity later
storeResponse(output);
#endif
@@ -161,7 +161,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
request->send(response);
api_count_++;
#if defined(EMSESP_TEST)
#if defined(EMSESP_UNITY)
// store the result so we can test with Unity later
storeResponse(output);
#endif
@@ -229,7 +229,7 @@ void WebAPIService::getEntities(AsyncWebServerRequest * request) {
request->send(response);
}
#if defined(EMSESP_TEST)
#if defined(EMSESP_UNITY)
// store the result so we can test with Unity later
static JsonDocument storeResponseDoc_;

Some files were not shown because too many files have changed in this diff Show More