refactor web file structure and seperate settings from status

This commit is contained in:
proddy
2024-07-22 14:46:22 +02:00
parent d0976cd660
commit 53e9a062e8
60 changed files with 149 additions and 251 deletions

View File

@@ -0,0 +1,236 @@
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 APApi from 'api/ap';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
ValidatedPasswordField,
ValidatedTextField,
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';
import { createAPSettingsValidator, validate } from 'validators';
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
const APSettings: FC = () => {
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<APSettingsType>({
read: APApi.readAPSettings,
update: APApi.updateAPSettings
});
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF(LL.ACCESS_POINT(0)));
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(createAPSettingsValidator(data), data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
}
};
return (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="provision_mode"
label={LL.AP_PROVIDE() + '...'}
value={data.provision_mode}
fullWidth
select
variant="outlined"
onChange={updateFormValue}
margin="normal"
>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>
{LL.AP_PROVIDE_TEXT_1()}
</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
{LL.AP_PROVIDE_TEXT_2()}
</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>
{LL.AP_PROVIDE_TEXT_3()}
</MenuItem>
</ValidatedTextField>
{isAPEnabled(data) && (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="ssid"
label={LL.ACCESS_POINT(2) + ' SSID'}
fullWidth
variant="outlined"
value={data.ssid}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label={LL.ACCESS_POINT(2) + ' ' + LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="channel"
label={LL.AP_PREFERRED_CHANNEL()}
value={numberValue(data.channel)}
fullWidth
select
type="number"
variant="outlined"
onChange={updateFormValue}
margin="normal"
>
{range(1, 14).map((i) => (
<MenuItem key={i} value={i}>
{i}
</MenuItem>
))}
</ValidatedTextField>
<BlockFormControlLabel
control={
<Checkbox
name="ssid_hidden"
checked={data.ssid_hidden}
onChange={updateFormValue}
/>
}
label={LL.AP_HIDE_SSID()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="max_clients"
label={LL.AP_MAX_CLIENTS()}
value={numberValue(data.max_clients)}
fullWidth
select
type="number"
variant="outlined"
onChange={updateFormValue}
margin="normal"
>
{range(1, 9).map((i) => (
<MenuItem key={i} value={i}>
{i}
</MenuItem>
))}
</ValidatedTextField>
<ValidatedTextField
fieldErrors={fieldErrors}
name="local_ip"
label={LL.AP_LOCAL_IP()}
fullWidth
variant="outlined"
value={data.local_ip}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="gateway_ip"
label={LL.NETWORK_GATEWAY()}
fullWidth
variant="outlined"
value={data.gateway_ip}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="subnet_mask"
label={LL.NETWORK_SUBNET()}
fullWidth
variant="outlined"
value={data.subnet_mask}
onChange={updateFormValue}
margin="normal"
/>
</>
)}
{dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={loadData}
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving}
variant="contained"
color="info"
type="submit"
onClick={validateAndSubmit}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
</ButtonRow>
)}
</>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>
);
};
export default APSettings;

View File

@@ -0,0 +1,946 @@
import { useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
Button,
Checkbox,
Divider,
Grid,
InputAdornment,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
import { useRequest } from 'alova';
import RestartMonitor from 'app/status/RestartMonitor';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent,
ValidatedTextField,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import * as EMSESP from '../main/api';
import { BOARD_PROFILES } from '../main/types';
import type { Settings } from '../main/types';
import { createSettingsValidator } from '../main/validators';
export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}>
{BOARD_PROFILES[code]}
</MenuItem>
));
}
const ApplicationSettings: FC = () => {
const {
loadData,
saveData,
saving,
updateDataValue,
data,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
errorMessage,
restartNeeded
} = useRest<Settings>({
read: EMSESP.readSettings,
update: EMSESP.writeSettings
});
const [restarting, setRestarting] = useState<boolean>();
const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const {
loading: processingBoard,
send: readBoardProfile,
onSuccess: onSuccessBoardProfile
} = useRequest((boardProfile: string) => EMSESP.getBoardProfile(boardProfile), {
immediate: false
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
onSuccessBoardProfile((event) => {
const response = event.data as Settings;
updateDataValue({
...data,
board_profile: response.board_profile,
led_gpio: response.led_gpio,
dallas_gpio: response.dallas_gpio,
rx_gpio: response.rx_gpio,
tx_gpio: response.tx_gpio,
pbutton_gpio: response.pbutton_gpio,
phy_type: response.phy_type,
eth_power: response.eth_power,
eth_phy_addr: response.eth_phy_addr,
eth_clock_mode: response.eth_clock_mode
});
});
const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error: Error) => {
toast.error(error.message);
});
};
useLayoutTitle(LL.SETTINGS_OF(LL.APPLICATION()));
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(createSettingsValidator(data), data);
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
} finally {
await saveData();
}
};
const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
const boardProfile = event.target.value;
updateFormValue(event);
if (boardProfile === 'CUSTOM') {
updateDataValue({
...data,
board_profile: boardProfile
});
} else {
void updateBoardProfile(boardProfile);
}
};
const restart = async () => {
await validateAndSubmit();
await restartCommand().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
return (
<>
<Typography sx={{ pb: 1 }} variant="h6" color="primary">
{LL.INTERFACE_BOARD_PROFILE()}
</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}
fullWidth
variant="outlined"
onChange={changeBoardProfile}
margin="normal"
select
>
{boardProfileSelectItems()}
<Divider />
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
{LL.CUSTOM()}&hellip;
</MenuItem>
</TextField>
{data.board_profile === 'CUSTOM' && (
<>
<Grid
container
spacing={1}
sx={{ pt: 1 }}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="rx_gpio"
label={LL.GPIO_OF('Rx')}
fullWidth
variant="outlined"
value={numberValue(data.rx_gpio)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="tx_gpio"
label={LL.GPIO_OF('Tx')}
fullWidth
variant="outlined"
value={numberValue(data.tx_gpio)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="pbutton_gpio"
label={LL.GPIO_OF(LL.BUTTON())}
fullWidth
variant="outlined"
value={numberValue(data.pbutton_gpio)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="dallas_gpio"
label={
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
}
fullWidth
variant="outlined"
value={numberValue(data.dallas_gpio)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="led_gpio"
label={LL.GPIO_OF('LED') + ' (0=' + LL.DISABLED(1) + ')'}
fullWidth
variant="outlined"
value={numberValue(data.led_gpio)}
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"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
<MenuItem value={1}>LAN8720</MenuItem>
<MenuItem value={2}>TLK110</MenuItem>
</TextField>
</Grid>
</Grid>
{data.phy_type !== 0 && (
<Grid
container
spacing={1}
sx={{ pt: 1 }}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="eth_power"
label={LL.GPIO_OF('PHY Power') + ' (-1=' + LL.DISABLED(1) + ')'}
fullWidth
variant="outlined"
value={numberValue(data.eth_power)}
type="number"
onChange={updateFormValue}
margin="normal"
disabled={saving}
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="eth_phy_addr"
label={LL.ADDRESS_OF('PHY I²C')}
fullWidth
variant="outlined"
value={numberValue(data.eth_phy_addr)}
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"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>GPIO0_IN</MenuItem>
<MenuItem value={1}>GPIO0_OUT</MenuItem>
<MenuItem value={2}>GPIO16_OUT</MenuItem>
<MenuItem value={3}>GPIO17_OUT</MenuItem>
</TextField>
</Grid>
</Grid>
)}
</>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.SETTINGS_OF(LL.EMS_BUS(0))}
</Typography>
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<TextField
name="tx_mode"
label={LL.TX_MODE()}
disabled={saving}
value={data.tx_mode}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>EMS</MenuItem>
<MenuItem value={2}>EMS+</MenuItem>
<MenuItem value={3}>HT3</MenuItem>
<MenuItem value={4}>{LL.HARDWARE()}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="ems_bus_id"
label={LL.ID_OF(LL.EMS_BUS(1))}
disabled={saving}
value={data.ems_bus_id}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0x0a}>Terminal (0x0A)</MenuItem>
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
<MenuItem value={0x0d}>Modem (0x0D)</MenuItem>
<MenuItem value={0x0e}>Converter (0x0E)</MenuItem>
<MenuItem value={0x0f}>Time Module (0x0F)</MenuItem>
<MenuItem value={0x48}>Gateway 1 (0x48)</MenuItem>
<MenuItem value={0x49}>Gateway 2 (0x49)</MenuItem>
<MenuItem value={0x4a}>Gateway 3 (0x4A)</MenuItem>
<MenuItem value={0x4b}>Gateway 4 (0x4B)</MenuItem>
<MenuItem value={0x4c}>Gateway 5 (0x4C)</MenuItem>
<MenuItem value={0x4d}>Gateway 7 (0x4D)</MenuItem>
</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"
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>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={
<Checkbox
checked={data.hide_led}
onChange={updateFormValue}
name="hide_led"
/>
}
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
checked={data.low_clock}
onChange={updateFormValue}
name="low_clock"
/>
}
label={LL.UNDERCLOCK_CPU()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.boiler_heatingoff}
onChange={updateFormValue}
name="boiler_heatingoff"
/>
}
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
checked={data.remote_timeout_en}
onChange={updateFormValue}
name="remote_timeout_en"
/>
}
label={LL.REMOTE_TIMEOUT_EN()}
/>
</Grid>
{data.remote_timeout_en && (
<Grid item xs={12} sm={6} md={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="remote_timeout"
label={LL.REMOTE_TIMEOUT()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.HOURS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.remote_timeout)}
type="number"
onChange={updateFormValue}
/>
</Grid>
)}
</Grid>
<Grid
container
spacing={0}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<BlockFormControlLabel
control={
<Checkbox
checked={data.shower_timer}
onChange={updateFormValue}
name="shower_timer"
/>
}
label={LL.ENABLE_SHOWER_TIMER()}
disabled={saving}
/>
<BlockFormControlLabel
control={
<Checkbox
checked={data.shower_alert}
onChange={updateFormValue}
name="shower_alert"
/>
}
label={LL.ENABLE_SHOWER_ALERT()}
disabled={!data.shower_timer}
/>
</Grid>
<Grid
container
sx={{ pt: 2 }}
rowSpacing={3}
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
{data.shower_timer && (
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_min_duration"
label={LL.MIN_DURATION()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
variant="outlined"
value={numberValue(data.shower_min_duration)}
fullWidth
type="number"
onChange={updateFormValue}
/>
</Grid>
)}
{data.shower_alert && (
<>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_trigger"
label={LL.TRIGGER_TIME()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
)
}}
variant="outlined"
value={numberValue(data.shower_alert_trigger)}
fullWidth
type="number"
onChange={updateFormValue}
disabled={!data.shower_timer}
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_coldshot"
label={LL.COLD_SHOT_DURATION()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
variant="outlined"
value={numberValue(data.shower_alert_coldshot)}
fullWidth
type="number"
onChange={updateFormValue}
disabled={!data.shower_timer}
/>
</Grid>
</>
)}
</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={saving}
/>
}
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
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()}
</Button>
</MessageBox>
)}
{!restartNeeded && dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={loadData}
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving}
variant="contained"
color="info"
type="submit"
onClick={validateAndSubmit}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
</ButtonRow>
)}
</>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};
export default ApplicationSettings;

View File

@@ -0,0 +1,578 @@
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,
Grid,
InputAdornment,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as MqttApi from 'api/mqtt';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
ValidatedPasswordField,
ValidatedTextField,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { MqttSettingsType } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators';
const MqttSettings: FC = () => {
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<MqttSettingsType>({
read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings
});
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF('MQTT'));
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(createMqttSettingsValidator(data), data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
}
};
return (
<>
<BlockFormControlLabel
control={
<Checkbox
name="enabled"
checked={data.enabled}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_MQTT()}
/>
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="host"
label={LL.ADDRESS_OF(LL.BROKER())}
fullWidth
multiline
variant="outlined"
value={data.host}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="port"
label="Port"
fullWidth
variant="outlined"
value={numberValue(data.port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="base"
label={LL.BASE_TOPIC()}
fullWidth
variant="outlined"
value={data.base}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="client_id"
label={LL.ID_OF(LL.CLIENT()) + ' (' + LL.OPTIONAL() + ')'}
fullWidth
variant="outlined"
value={data.client_id}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="username"
label={LL.USERNAME(0)}
fullWidth
variant="outlined"
value={data.username}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedPasswordField
name="password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="keep_alive"
label="Keep Alive"
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.keep_alive)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
name="mqtt_qos"
label="QoS"
value={data.mqtt_qos}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
</TextField>
</Grid>
</Grid>
{data.enableTLS !== undefined && (
<BlockFormControlLabel
control={
<Checkbox
name="enableTLS"
checked={data.enableTLS}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_TLS()}
/>
)}
{data.enableTLS === true && (
<ValidatedPasswordField
name="rootCA"
label={LL.CERT()}
fullWidth
variant="outlined"
value={data.rootCA}
onChange={updateFormValue}
margin="normal"
/>
)}
<BlockFormControlLabel
control={
<Checkbox
name="clean_session"
checked={data.clean_session}
onChange={updateFormValue}
/>
}
label={LL.MQTT_CLEAN_SESSION()}
/>
<BlockFormControlLabel
control={
<Checkbox
name="mqtt_retain"
checked={data.mqtt_retain}
onChange={updateFormValue}
/>
}
label={LL.MQTT_RETAIN_FLAG()}
/>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.FORMATTING()}
</Typography>
<TextField
name="nested_format"
label={LL.MQTT_FORMAT()}
value={data.nested_format}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
</TextField>
<BlockFormControlLabel
control={
<Checkbox
name="send_response"
checked={data.send_response}
onChange={updateFormValue}
/>
}
label={LL.MQTT_RESPONSE()}
/>
{!data.ha_enabled && (
<Grid
container
rowSpacing={-1}
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item>
<BlockFormControlLabel
control={
<Checkbox
name="publish_single"
checked={data.publish_single}
onChange={updateFormValue}
/>
}
label={LL.MQTT_PUBLISH_TEXT_1()}
/>
</Grid>
{data.publish_single && (
<Grid item>
<BlockFormControlLabel
control={
<Checkbox
name="publish_single2cmd"
checked={data.publish_single2cmd}
onChange={updateFormValue}
/>
}
label={LL.MQTT_PUBLISH_TEXT_2()}
/>
</Grid>
)}
</Grid>
)}
{!data.publish_single && (
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item>
<BlockFormControlLabel
control={
<Checkbox
name="ha_enabled"
checked={data.ha_enabled}
onChange={updateFormValue}
/>
}
label={LL.MQTT_PUBLISH_TEXT_3()}
/>
</Grid>
{data.ha_enabled && (
<Grid
container
sx={{ pl: 1 }}
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="discovery_type"
label={LL.MQTT_PUBLISH_TEXT_5()}
value={data.discovery_type}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>Home Assistant</MenuItem>
<MenuItem value={1}>Domoticz</MenuItem>
<MenuItem value={2}>Domoticz (latest)</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="discovery_prefix"
label={LL.MQTT_PUBLISH_TEXT_4()}
fullWidth
variant="outlined"
value={data.discovery_prefix}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="entity_format"
label={LL.MQTT_ENTITY_FORMAT()}
value={data.entity_format}
fullWidth
variant="outlined"
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
<MenuItem value={3}>
{LL.MQTT_ENTITY_FORMAT_1()}&nbsp;(v3.6)
</MenuItem>
<MenuItem value={4}>
{LL.MQTT_ENTITY_FORMAT_2()}&nbsp;(v3.6)
</MenuItem>
<MenuItem value={1}>{LL.MQTT_ENTITY_FORMAT_1()}</MenuItem>
<MenuItem value={2}>{LL.MQTT_ENTITY_FORMAT_2()}</MenuItem>
</TextField>
</Grid>
</Grid>
)}
</Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto)
</Typography>
<Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_heartbeat"
label="Heartbeat"
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_heartbeat)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_boiler"
label={LL.MQTT_INT_BOILER()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_boiler)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_thermostat"
label={LL.MQTT_INT_THERMOSTATS()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_thermostat)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_solar"
label={LL.MQTT_INT_SOLAR()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_solar)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_mixer"
label={LL.MQTT_INT_MIXER()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_mixer)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_water"
label={LL.MQTT_INT_WATER()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_water)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_sensor"
label={LL.TEMP_SENSORS()}
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_sensor)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<TextField
name="publish_time_other"
InputProps={{
endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}}
label={LL.DEFAULT(0)}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_other)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
{dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={loadData}
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving}
variant="contained"
color="info"
type="submit"
onClick={validateAndSubmit}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
</ButtonRow>
)}
</>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>
);
};
export default MqttSettings;

View File

@@ -0,0 +1,155 @@
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 { updateState } from 'alova';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
ValidatedTextField,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { NTPSettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
import { TIME_ZONES, selectedTimeZone, timeZoneSelectItems } from './TZ';
const NTPSettings: FC = () => {
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<NTPSettingsType>({
read: NTPApi.readNTPSettings,
update: NTPApi.updateNTPSettings
});
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF('NTP'));
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(NTP_SETTINGS_VALIDATOR, data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
}
};
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFormValue(event);
updateState('ntpSettings', (settings: NTPSettingsType) => ({
...settings,
tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value]
}));
};
return (
<>
<BlockFormControlLabel
control={
<Checkbox
name="enabled"
checked={data.enabled}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_NTP()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="server"
label={LL.NTP_SERVER()}
fullWidth
variant="outlined"
value={data.server}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="tz_label"
label={LL.TIME_ZONE()}
fullWidth
variant="outlined"
value={selectedTimeZone(data.tz_label, data.tz_format)}
onChange={changeTimeZone}
margin="normal"
select
>
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
{timeZoneSelectItems()}
</ValidatedTextField>
{dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={loadData}
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving}
variant="contained"
color="info"
type="submit"
onClick={validateAndSubmit}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
</ButtonRow>
)}
</>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>
);
};
export default NTPSettings;

View File

@@ -0,0 +1,163 @@
import { type FC, useState } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import LockIcon from '@mui/icons-material/Lock';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TuneIcon from '@mui/icons-material/Tune';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
List
} from '@mui/material';
import * as SystemApi from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { useI18nContext } from 'i18n/i18n-react';
const Settings: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS(0));
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const { send: factoryResetCommand } = useRequest(SystemApi.factoryReset(), {
immediate: false
});
const factoryReset = async () => {
await factoryResetCommand();
setConfirmFactoryReset(false);
};
const renderFactoryResetDialog = () => (
<Dialog
sx={dialogStyle}
open={confirmFactoryReset}
onClose={() => setConfirmFactoryReset(false)}
>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmFactoryReset(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={factoryReset}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => (
<>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<ListMenuItem
icon={TuneIcon}
bgcolor="#134ba2"
label={LL.SETTINGS_OF(LL.APPLICATION())}
text={LL.APPLICATION_SETTINGS_1()}
to="application"
/>
<ListMenuItem
icon={SettingsEthernetIcon}
bgcolor="#40828f"
label={LL.SETTINGS_OF(LL.NETWORK(0))}
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
to="network"
/>
<ListMenuItem
icon={SettingsInputAntennaIcon}
bgcolor="#5f9a5f"
label={LL.SETTINGS_OF(LL.ACCESS_POINT(0))}
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
to="ap"
/>
<ListMenuItem
icon={AccessTimeIcon}
bgcolor="#c5572c"
label={LL.SETTINGS_OF('NTP')}
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
to="ntp"
/>
<ListMenuItem
icon={DeviceHubIcon}
bgcolor="#68374d"
label={LL.SETTINGS_OF('MQTT')}
text={LL.CONFIGURE('MQTT')}
to="mqtt"
/>
<ListMenuItem
icon={LockIcon}
label={LL.SETTINGS_OF(LL.SECURITY(0))}
text={LL.SECURITY_1()}
to="security"
/>
<ListMenuItem
icon={ViewModuleIcon}
bgcolor="#efc34b"
label="Modules"
text="Activate or deactivate external modules"
to="modules"
/>
<Divider />
<ListMenuItem
icon={ImportExportIcon}
bgcolor="#5d89f7"
label={LL.UPLOAD_DOWNLOAD()}
text={LL.UPLOAD_DOWNLOAD_1()}
to="upload"
/>
</List>
{renderFactoryResetDialog()}
<Box mt={2} display="flex" flexWrap="wrap">
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={() => setConfirmFactoryReset(true)}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</Box>
</>
);
return <SectionContent>{content()}</SectionContent>;
};
export default Settings;

View File

@@ -0,0 +1,478 @@
import { MenuItem } from '@mui/material';
type TimeZones = Record<string, string>;
export const TIME_ZONES: TimeZones = {
'Africa/Abidjan': 'GMT0',
'Africa/Accra': 'GMT0',
'Africa/Addis_Ababa': 'EAT-3',
'Africa/Algiers': 'CET-1',
'Africa/Asmara': 'EAT-3',
'Africa/Bamako': 'GMT0',
'Africa/Bangui': 'WAT-1',
'Africa/Banjul': 'GMT0',
'Africa/Bissau': 'GMT0',
'Africa/Blantyre': 'CAT-2',
'Africa/Brazzaville': 'WAT-1',
'Africa/Bujumbura': 'CAT-2',
'Africa/Cairo': 'EET-2',
'Africa/Casablanca': 'UNK-1',
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Africa/Conakry': 'GMT0',
'Africa/Dakar': 'GMT0',
'Africa/Dar_es_Salaam': 'EAT-3',
'Africa/Djibouti': 'EAT-3',
'Africa/Douala': 'WAT-1',
'Africa/El_Aaiun': 'UNK-1',
'Africa/Freetown': 'GMT0',
'Africa/Gaborone': 'CAT-2',
'Africa/Harare': 'CAT-2',
'Africa/Johannesburg': 'SAST-2',
'Africa/Juba': 'EAT-3',
'Africa/Kampala': 'EAT-3',
'Africa/Khartoum': 'CAT-2',
'Africa/Kigali': 'CAT-2',
'Africa/Kinshasa': 'WAT-1',
'Africa/Lagos': 'WAT-1',
'Africa/Libreville': 'WAT-1',
'Africa/Lome': 'GMT0',
'Africa/Luanda': 'WAT-1',
'Africa/Lubumbashi': 'CAT-2',
'Africa/Lusaka': 'CAT-2',
'Africa/Malabo': 'WAT-1',
'Africa/Maputo': 'CAT-2',
'Africa/Maseru': 'SAST-2',
'Africa/Mbabane': 'SAST-2',
'Africa/Mogadishu': 'EAT-3',
'Africa/Monrovia': 'GMT0',
'Africa/Nairobi': 'EAT-3',
'Africa/Ndjamena': 'WAT-1',
'Africa/Niamey': 'WAT-1',
'Africa/Nouakchott': 'GMT0',
'Africa/Ouagadougou': 'GMT0',
'Africa/Porto-Novo': 'WAT-1',
'Africa/Sao_Tome': 'GMT0',
'Africa/Tripoli': 'EET-2',
'Africa/Tunis': 'CET-1',
'Africa/Windhoek': 'CAT-2',
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Anguilla': 'AST4',
'America/Antigua': 'AST4',
'America/Araguaina': 'UNK3',
'America/Argentina/Buenos_Aires': 'UNK3',
'America/Argentina/Catamarca': 'UNK3',
'America/Argentina/Cordoba': 'UNK3',
'America/Argentina/Jujuy': 'UNK3',
'America/Argentina/La_Rioja': 'UNK3',
'America/Argentina/Mendoza': 'UNK3',
'America/Argentina/Rio_Gallegos': 'UNK3',
'America/Argentina/Salta': 'UNK3',
'America/Argentina/San_Juan': 'UNK3',
'America/Argentina/San_Luis': 'UNK3',
'America/Argentina/Tucuman': 'UNK3',
'America/Argentina/Ushuaia': 'UNK3',
'America/Aruba': 'AST4',
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
'America/Atikokan': 'EST5',
'America/Bahia': 'UNK3',
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
'America/Barbados': 'AST4',
'America/Belem': 'UNK3',
'America/Belize': 'CST6',
'America/Blanc-Sablon': 'AST4',
'America/Boa_Vista': 'UNK4',
'America/Bogota': 'UNK5',
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
'America/Campo_Grande': 'UNK4',
'America/Cancun': 'EST5',
'America/Caracas': 'UNK4',
'America/Cayenne': 'UNK3',
'America/Cayman': 'EST5',
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
'America/Costa_Rica': 'CST6',
'America/Creston': 'MST7',
'America/Cuiaba': 'UNK4',
'America/Curacao': 'AST4',
'America/Danmarkshavn': 'GMT0',
'America/Dawson': 'MST7',
'America/Dawson_Creek': 'MST7',
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
'America/Dominica': 'AST4',
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
'America/Eirunepe': 'UNK5',
'America/El_Salvador': 'CST6',
'America/Fort_Nelson': 'MST7',
'America/Fortaleza': 'UNK3',
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
'America/Grenada': 'AST4',
'America/Guadeloupe': 'AST4',
'America/Guatemala': 'CST6',
'America/Guayaquil': 'UNK5',
'America/Guyana': 'UNK4',
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
'America/Hermosillo': 'MST7',
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Knox': 'CST6CDT,M3.2.0,M11.1.0',
'America/Indiana/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Petersburg': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
'America/Indiana/Vevay': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Winamac': 'EST5EDT,M3.2.0,M11.1.0',
'America/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
'America/Jamaica': 'EST5',
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Kentucky/Louisville': 'EST5EDT,M3.2.0,M11.1.0',
'America/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
'America/Kralendijk': 'AST4',
'America/La_Paz': 'UNK4',
'America/Lima': 'UNK5',
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
'America/Lower_Princes': 'AST4',
'America/Maceio': 'UNK3',
'America/Managua': 'CST6',
'America/Manaus': 'UNK4',
'America/Marigot': 'AST4',
'America/Martinique': 'AST4',
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
'America/Moncton': 'AST4ADT,M3.2.0,M11.1.0',
'America/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
'America/Montevideo': 'UNK3',
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
'America/Montserrat': 'AST4',
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
'America/New_York': 'EST5EDT,M3.2.0,M11.1.0',
'America/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Noronha': 'UNK2',
'America/North_Dakota/Beulah': 'CST6CDT,M3.2.0,M11.1.0',
'America/North_Dakota/Center': 'CST6CDT,M3.2.0,M11.1.0',
'America/North_Dakota/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
'America/Panama': 'EST5',
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
'America/Paramaribo': 'UNK3',
'America/Phoenix': 'MST7',
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
'America/Port_of_Spain': 'AST4',
'America/Porto_Velho': 'UNK4',
'America/Puerto_Rico': 'AST4',
'America/Punta_Arenas': 'UNK3',
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
'America/Recife': 'UNK3',
'America/Regina': 'CST6',
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
'America/Rio_Branco': 'UNK5',
'America/Santarem': 'UNK3',
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
'America/Santo_Domingo': 'AST4',
'America/Sao_Paulo': 'UNK3',
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/St_Barthelemy': 'AST4',
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
'America/St_Kitts': 'AST4',
'America/St_Lucia': 'AST4',
'America/St_Thomas': 'AST4',
'America/St_Vincent': 'AST4',
'America/Swift_Current': 'CST6',
'America/Tegucigalpa': 'CST6',
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
'America/Thunder_Bay': 'EST5EDT,M3.2.0,M11.1.0',
'America/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
'America/Tortola': 'AST4',
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
'America/Whitehorse': 'MST7',
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
'Antarctica/Casey': 'UNK-8',
'Antarctica/Davis': 'UNK-7',
'Antarctica/DumontDUrville': 'UNK-10',
'Antarctica/Macquarie': 'UNK-11',
'Antarctica/Mawson': 'UNK-5',
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
'Antarctica/Palmer': 'UNK3',
'Antarctica/Rothera': 'UNK3',
'Antarctica/Syowa': 'UNK-3',
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
'Antarctica/Vostok': 'UNK-6',
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Asia/Aden': 'UNK-3',
'Asia/Almaty': 'UNK-6',
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
'Asia/Anadyr': 'UNK-12',
'Asia/Aqtau': 'UNK-5',
'Asia/Aqtobe': 'UNK-5',
'Asia/Ashgabat': 'UNK-5',
'Asia/Atyrau': 'UNK-5',
'Asia/Baghdad': 'UNK-3',
'Asia/Bahrain': 'UNK-3',
'Asia/Baku': 'UNK-4',
'Asia/Bangkok': 'UNK-7',
'Asia/Barnaul': 'UNK-7',
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
'Asia/Bishkek': 'UNK-6',
'Asia/Brunei': 'UNK-8',
'Asia/Chita': 'UNK-9',
'Asia/Choibalsan': 'UNK-8',
'Asia/Colombo': 'UNK-5:30',
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
'Asia/Dhaka': 'UNK-6',
'Asia/Dili': 'UNK-9',
'Asia/Dubai': 'UNK-4',
'Asia/Dushanbe': 'UNK-5',
'Asia/Famagusta': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Asia/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
'Asia/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
'Asia/Ho_Chi_Minh': 'UNK-7',
'Asia/Hong_Kong': 'HKT-8',
'Asia/Hovd': 'UNK-7',
'Asia/Irkutsk': 'UNK-8',
'Asia/Jakarta': 'WIB-7',
'Asia/Jayapura': 'WIT-9',
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
'Asia/Kabul': 'UNK-4:30',
'Asia/Kamchatka': 'UNK-12',
'Asia/Karachi': 'PKT-5',
'Asia/Kathmandu': 'UNK-5:45',
'Asia/Khandyga': 'UNK-9',
'Asia/Kolkata': 'IST-5:30',
'Asia/Krasnoyarsk': 'UNK-7',
'Asia/Kuala_Lumpur': 'UNK-8',
'Asia/Kuching': 'UNK-8',
'Asia/Kuwait': 'UNK-3',
'Asia/Macau': 'CST-8',
'Asia/Magadan': 'UNK-11',
'Asia/Makassar': 'WITA-8',
'Asia/Manila': 'PST-8',
'Asia/Muscat': 'UNK-4',
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Asia/Novokuznetsk': 'UNK-7',
'Asia/Novosibirsk': 'UNK-7',
'Asia/Omsk': 'UNK-6',
'Asia/Oral': 'UNK-5',
'Asia/Phnom_Penh': 'UNK-7',
'Asia/Pontianak': 'WIB-7',
'Asia/Pyongyang': 'KST-9',
'Asia/Qatar': 'UNK-3',
'Asia/Qyzylorda': 'UNK-5',
'Asia/Riyadh': 'UNK-3',
'Asia/Sakhalin': 'UNK-11',
'Asia/Samarkand': 'UNK-5',
'Asia/Seoul': 'KST-9',
'Asia/Shanghai': 'CST-8',
'Asia/Singapore': 'UNK-8',
'Asia/Srednekolymsk': 'UNK-11',
'Asia/Taipei': 'CST-8',
'Asia/Tashkent': 'UNK-5',
'Asia/Tbilisi': 'UNK-4',
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
'Asia/Thimphu': 'UNK-6',
'Asia/Tokyo': 'JST-9',
'Asia/Tomsk': 'UNK-7',
'Asia/Ulaanbaatar': 'UNK-8',
'Asia/Urumqi': 'UNK-6',
'Asia/Ust-Nera': 'UNK-10',
'Asia/Vientiane': 'UNK-7',
'Asia/Vladivostok': 'UNK-10',
'Asia/Yakutsk': 'UNK-9',
'Asia/Yangon': 'UNK-6:30',
'Asia/Yekaterinburg': 'UNK-5',
'Asia/Yerevan': 'UNK-4',
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Cape_Verde': 'UNK1',
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Reykjavik': 'GMT0',
'Atlantic/South_Georgia': 'UNK2',
'Atlantic/St_Helena': 'GMT0',
'Atlantic/Stanley': 'UNK3',
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
'Australia/Brisbane': 'AEST-10',
'Australia/Broken_Hill': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
'Australia/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Darwin': 'ACST-9:30',
'Australia/Eucla': 'UNK-8:45',
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Lindeman': 'AEST-10',
'Australia/Lord_Howe': 'UNK-10:30UNK-11,M10.1.0,M4.1.0',
'Australia/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Perth': 'AWST-8',
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Etc/GMT': 'GMT0',
'Etc/GMT+0': 'GMT0',
'Etc/GMT+1': 'UNK1',
'Etc/GMT+10': 'UNK10',
'Etc/GMT+11': 'UNK11',
'Etc/GMT+12': 'UNK12',
'Etc/GMT+2': 'UNK2',
'Etc/GMT+3': 'UNK3',
'Etc/GMT+4': 'UNK4',
'Etc/GMT+5': 'UNK5',
'Etc/GMT+6': 'UNK6',
'Etc/GMT+7': 'UNK7',
'Etc/GMT+8': 'UNK8',
'Etc/GMT+9': 'UNK9',
'Etc/GMT-0': 'GMT0',
'Etc/GMT-1': 'UNK-1',
'Etc/GMT-10': 'UNK-10',
'Etc/GMT-11': 'UNK-11',
'Etc/GMT-12': 'UNK-12',
'Etc/GMT-13': 'UNK-13',
'Etc/GMT-14': 'UNK-14',
'Etc/GMT-2': 'UNK-2',
'Etc/GMT-3': 'UNK-3',
'Etc/GMT-4': 'UNK-4',
'Etc/GMT-5': 'UNK-5',
'Etc/GMT-6': 'UNK-6',
'Etc/GMT-7': 'UNK-7',
'Etc/GMT-8': 'UNK-8',
'Etc/GMT-9': 'UNK-9',
'Etc/GMT0': 'GMT0',
'Etc/Greenwich': 'GMT0',
'Etc/UCT': 'UTC0',
'Etc/UTC': 'UTC0',
'Etc/Universal': 'UTC0',
'Etc/Zulu': 'UTC0',
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Andorra': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Astrakhan': 'UNK-4',
'Europe/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Berlin': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Busingen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
'Europe/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Helsinki': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Istanbul': 'UNK-3',
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Kaliningrad': 'EET-2',
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Kirov': 'UNK-3',
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Madrid': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Minsk': 'UNK-3',
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Moscow': 'MSK-3',
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Paris': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Samara': 'UNK-4',
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Sarajevo': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Saratov': 'UNK-4',
'Europe/Simferopol': 'MSK-3',
'Europe/Skopje': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Ulyanovsk': 'UNK-4',
'Europe/Uzhgorod': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vatican': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Volgograd': 'UNK-4',
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Zagreb': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Indian/Antananarivo': 'EAT-3',
'Indian/Chagos': 'UNK-6',
'Indian/Christmas': 'UNK-7',
'Indian/Cocos': 'UNK-6:30',
'Indian/Comoro': 'EAT-3',
'Indian/Kerguelen': 'UNK-5',
'Indian/Mahe': 'UNK-4',
'Indian/Maldives': 'UNK-5',
'Indian/Mauritius': 'UNK-4',
'Indian/Mayotte': 'EAT-3',
'Indian/Reunion': 'UNK-4',
'Pacific/Apia': 'UNK-13UNK,M9.5.0/3,M4.1.0/4',
'Pacific/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
'Pacific/Bougainville': 'UNK-11',
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
'Pacific/Chuuk': 'UNK-10',
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
'Pacific/Efate': 'UNK-11',
'Pacific/Enderbury': 'UNK-13',
'Pacific/Fakaofo': 'UNK-13',
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
'Pacific/Funafuti': 'UNK-12',
'Pacific/Galapagos': 'UNK6',
'Pacific/Gambier': 'UNK9',
'Pacific/Guadalcanal': 'UNK-11',
'Pacific/Guam': 'ChST-10',
'Pacific/Honolulu': 'HST10',
'Pacific/Kiritimati': 'UNK-14',
'Pacific/Kosrae': 'UNK-11',
'Pacific/Kwajalein': 'UNK-12',
'Pacific/Majuro': 'UNK-12',
'Pacific/Marquesas': 'UNK9:30',
'Pacific/Midway': 'SST11',
'Pacific/Nauru': 'UNK-12',
'Pacific/Niue': 'UNK11',
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
'Pacific/Noumea': 'UNK-11',
'Pacific/Pago_Pago': 'SST11',
'Pacific/Palau': 'UNK-9',
'Pacific/Pitcairn': 'UNK8',
'Pacific/Pohnpei': 'UNK-11',
'Pacific/Port_Moresby': 'UNK-10',
'Pacific/Rarotonga': 'UNK10',
'Pacific/Saipan': 'ChST-10',
'Pacific/Tahiti': 'UNK10',
'Pacific/Tarawa': 'UNK-12',
'Pacific/Tongatapu': 'UNK-13',
'Pacific/Wake': 'UNK-12',
'Pacific/Wallis': 'UNK-12'
};
export function selectedTimeZone(label: string, format: string) {
return TIME_ZONES[label] === format ? label : undefined;
}
export function timeZoneSelectItems() {
return Object.keys(TIME_ZONES).map((label) => (
<MenuItem key={label} value={label}>
{label}
</MenuItem>
));
}

View File

@@ -0,0 +1,367 @@
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.readESPSystemStatus, { 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

@@ -0,0 +1,58 @@
import { useCallback, useState } from 'react';
import type { FC } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom';
import { Tab } from '@mui/material';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { WiFiNetwork } from 'types';
import NetworkSettings from './NetworkSettings';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import WiFiNetworkScanner from './WiFiNetworkScanner';
const Network: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF(LL.NETWORK(0)));
const { routerTab } = useRouterTab();
const navigate = useNavigate();
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
const selectNetwork = useCallback(
(network: WiFiNetwork) => {
setSelectedNetwork(network);
navigate('settings');
},
[navigate]
);
const deselectNetwork = useCallback(() => {
setSelectedNetwork(undefined);
}, []);
return (
<WiFiConnectionContext.Provider
value={{
selectedNetwork,
selectNetwork,
deselectNetwork
}}
>
<RouterTabs value={routerTab}>
<Tab value="settings" label={LL.SETTINGS_OF(LL.NETWORK(1))} />
<Tab value="scan" label={LL.NETWORK_SCAN()} />
</RouterTabs>
<Routes>
<Route path="scan" element={<WiFiNetworkScanner />} />
<Route path="settings" element={<NetworkSettings />} />
<Route path="*" element={<Navigate replace to="settings" />} />
</Routes>
</WiFiConnectionContext.Provider>
);
};
export default Network;

View File

@@ -0,0 +1,406 @@
import { useContext, useEffect, useState } from 'react';
import type { FC } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import DeleteIcon from '@mui/icons-material/Delete';
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import WarningIcon from '@mui/icons-material/Warning';
import {
Avatar,
Button,
Checkbox,
IconButton,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as NetworkApi from 'api/network';
import * as SystemApi from 'api/system';
import { updateState, useRequest } from 'alova';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent,
ValidatedPasswordField,
ValidatedTextField
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { NetworkSettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network';
import RestartMonitor from '../../status/RestartMonitor';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiNetworkSelector';
const NetworkSettings: FC = () => {
const { LL } = useI18nContext();
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
const [initialized, setInitialized] = useState(false);
const [restarting, setRestarting] = useState(false);
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage,
restartNeeded
} = useRest<NetworkSettingsType>({
read: NetworkApi.readNetworkSettings,
update: NetworkApi.updateNetworkSettings
});
const { send: restartCommand } = useRequest(SystemApi.restart(), {
immediate: false
});
useEffect(() => {
if (!initialized && data) {
if (selectedNetwork) {
updateState('networkSettings', (current_data: NetworkSettingsType) => ({
ssid: selectedNetwork.ssid,
bssid: selectedNetwork.bssid,
password: current_data ? current_data.password : '',
hostname: current_data?.hostname,
static_ip_config: false,
bandwidth20: false,
tx_power: 0,
nosleep: false,
enableMDNS: true,
enableCORS: false,
CORSOrigin: '*'
}));
}
setInitialized(true);
}
}, [initialized, setInitialized, data, selectedNetwork]);
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
useEffect(() => deselectNetwork, [deselectNetwork]);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(createNetworkSettingsValidator(data), data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
}
deselectNetwork();
};
const setCancel = async () => {
deselectNetwork();
await loadData();
};
const restart = async () => {
await restartCommand().catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
return (
<>
<Typography variant="h6" color="primary">
WiFi
</Typography>
{selectedNetwork ? (
<List>
<ListItem>
<ListItemAvatar>
<Avatar>
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={selectedNetwork.ssid}
secondary={
'Security: ' +
networkSecurityMode(selectedNetwork) +
', Ch: ' +
selectedNetwork.channel +
', bssid: ' +
selectedNetwork.bssid
}
/>
<ListItemSecondaryAction>
<IconButton onClick={setCancel}>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</List>
) : (
<ValidatedTextField
fieldErrors={fieldErrors}
name="ssid"
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
fullWidth
variant="outlined"
value={data.ssid}
onChange={updateFormValue}
margin="normal"
/>
)}
<ValidatedTextField
fieldErrors={fieldErrors}
name="bssid"
label={'BSSID (' + LL.NETWORK_BLANK_BSSID() + ')'}
fullWidth
variant="outlined"
value={data.bssid}
onChange={updateFormValue}
margin="normal"
/>
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
onChange={updateFormValue}
margin="normal"
/>
)}
<TextField
name="tx_power"
label={LL.TX_POWER()}
fullWidth
variant="outlined"
value={data.tx_power}
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>Auto</MenuItem>
<MenuItem value={78}>19.5 dBm</MenuItem>
<MenuItem value={76}>19 dBm</MenuItem>
<MenuItem value={74}>18.5 dBm</MenuItem>
<MenuItem value={68}>17 dBm</MenuItem>
<MenuItem value={60}>15 dBm</MenuItem>
<MenuItem value={52}>13 dBm</MenuItem>
<MenuItem value={44}>11 dBm</MenuItem>
<MenuItem value={34}>8.5 dBm</MenuItem>
<MenuItem value={28}>7 dBm</MenuItem>
<MenuItem value={20}>5 dBm</MenuItem>
<MenuItem value={8}>2 dBm</MenuItem>
</TextField>
<BlockFormControlLabel
control={
<Checkbox
name="nosleep"
checked={data.nosleep}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_DISABLE_SLEEP()}
/>
<BlockFormControlLabel
control={
<Checkbox
name="bandwidth20"
checked={data.bandwidth20}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_LOW_BAND()}
/>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.GENERAL_OPTIONS()}
</Typography>
<ValidatedTextField
fieldErrors={fieldErrors}
name="hostname"
label={LL.HOSTNAME()}
fullWidth
variant="outlined"
value={data.hostname}
onChange={updateFormValue}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
name="enableMDNS"
checked={data.enableMDNS}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_USE_DNS()}
/>
<BlockFormControlLabel
control={
<Checkbox
name="enableCORS"
checked={data.enableCORS}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_ENABLE_CORS()}
/>
{data.enableCORS && (
<TextField
name="CORSOrigin"
label={LL.NETWORK_CORS_ORIGIN()}
fullWidth
variant="outlined"
value={data.CORSOrigin}
onChange={updateFormValue}
margin="normal"
/>
)}
<BlockFormControlLabel
control={
<Checkbox
name="static_ip_config"
checked={data.static_ip_config}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_FIXED_IP()}
/>
{data.static_ip_config && (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="local_ip"
label={LL.AP_LOCAL_IP()}
fullWidth
variant="outlined"
value={data.local_ip}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="gateway_ip"
label={LL.NETWORK_GATEWAY()}
fullWidth
variant="outlined"
value={data.gateway_ip}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="subnet_mask"
label={LL.NETWORK_SUBNET()}
fullWidth
variant="outlined"
value={data.subnet_mask}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="dns_ip_1"
label="DNS #1"
fullWidth
variant="outlined"
value={data.dns_ip_1}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="dns_ip_2"
label="DNS #2"
fullWidth
variant="outlined"
value={data.dns_ip_2}
onChange={updateFormValue}
margin="normal"
/>
</>
)}
{restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()}
</Button>
</MessageBox>
)}
{!restartNeeded &&
(selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={loadData}
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving}
variant="contained"
color="info"
type="submit"
onClick={validateAndSubmit}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
</ButtonRow>
)}
</>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{restarting ? <RestartMonitor /> : content()}
</SectionContent>
);
};
export default NetworkSettings;

View File

@@ -0,0 +1,14 @@
import { createContext } from 'react';
import type { WiFiNetwork } from 'types';
export interface WiFiConnectionContextValue {
selectedNetwork?: WiFiNetwork;
selectNetwork: (network: WiFiNetwork) => void;
deselectNetwork: () => void;
}
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
export const WiFiConnectionContext = createContext(
WiFiConnectionContextDefaultValue
);

View File

@@ -0,0 +1,81 @@
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 { ButtonRow, FormLoader, SectionContent } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import WiFiNetworkSelector from './WiFiNetworkSelector';
const NUM_POLLS = 10;
const POLLING_FREQUENCY = 1000;
const WiFiNetworkScanner: FC = () => {
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
});
onSuccessNetworkList((event) => {
if (!event.data) {
const completedPollCount = pollCount.current + 1;
if (completedPollCount < NUM_POLLS) {
pollCount.current = completedPollCount;
setTimeout(getNetworkList, POLLING_FREQUENCY);
} else {
setErrorMessage(LL.PROBLEM_LOADING());
pollCount.current = 0;
}
}
});
onCompleteScanNetworks(() => {
pollCount.current = 0;
setErrorMessage(undefined);
updateState('listNetworks', () => undefined);
void getNetworkList();
});
const renderNetworkScanner = () => {
if (!networkList) {
return (
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
);
}
return <WiFiNetworkSelector networkList={networkList} />;
};
return (
<SectionContent>
{renderNetworkScanner()}
<ButtonRow>
<Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
color="secondary"
onClick={scanNetworks}
disabled={!errorMessage && !networkList}
>
{LL.SCAN_AGAIN()}&hellip;
</Button>
</ButtonRow>
</SectionContent>
);
};
export default WiFiNetworkScanner;

View File

@@ -0,0 +1,105 @@
import { useContext } from 'react';
import type { FC } from 'react';
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import WifiIcon from '@mui/icons-material/Wifi';
import {
Avatar,
Badge,
List,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { WiFiNetwork, WiFiNetworkList } from 'types';
import { WiFiEncryptionType } from 'types';
import { WiFiConnectionContext } from './WiFiConnectionContext';
interface WiFiNetworkSelectorProps {
networkList: WiFiNetworkList;
}
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
switch (encryption_type) {
case WiFiEncryptionType.WIFI_AUTH_WEP:
return 'WEP';
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
return 'WPA';
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
return 'WPA2';
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
return 'WPA/WPA2';
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
return 'WPA2 Enterprise';
case WiFiEncryptionType.WIFI_AUTH_OPEN:
return 'None';
case WiFiEncryptionType.WIFI_AUTH_WPA3_PSK:
return 'WPA3';
case WiFiEncryptionType.WIFI_AUTH_WPA2_WPA3_PSK:
return 'WPA2/WPA3';
default:
return 'Unknown: ' + String(encryption_type);
}
};
const networkQualityHighlight = ({ rssi }: WiFiNetwork, theme: Theme) => {
if (rssi <= -85) {
return theme.palette.error.main;
} else if (rssi <= -75) {
return theme.palette.warning.main;
}
return theme.palette.success.main;
};
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
const { LL } = useI18nContext();
const theme = useTheme();
const wifiConnectionContext = useContext(WiFiConnectionContext);
const renderNetwork = (network: WiFiNetwork) => (
<ListItem
key={network.bssid}
onClick={() => wifiConnectionContext.selectNetwork(network)}
>
<ListItemAvatar>
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
</ListItemAvatar>
<ListItemText
primary={network.ssid}
secondary={
'Security: ' +
networkSecurityMode(network) +
', Ch: ' +
network.channel +
', bssid: ' +
network.bssid
}
/>
<ListItemIcon>
<Badge badgeContent={network.rssi + 'dBm'}>
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
</Badge>
</ListItemIcon>
</ListItem>
);
if (networkList.networks.length === 0) {
return <MessageBox mt={2} mb={1} message={LL.NETWORK_NO_WIFI()} level="info" />;
}
return <List>{networkList.networks.map(renderNetwork)}</List>;
};
export default WiFiNetworkSelector;

View File

@@ -0,0 +1,90 @@
import { useEffect } from 'react';
import type { FC } from 'react';
import CloseIcon from '@mui/icons-material/Close';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
LinearProgress,
TextField,
Typography
} from '@mui/material';
import * as SecurityApi from 'api/security';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova';
import { MessageBox } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
interface GenerateTokenProps {
username?: string;
onClose: () => void;
}
const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const { LL } = useI18nContext();
const open = !!username;
const { data: token, send: generateToken } = useRequest(
SecurityApi.generateToken(username),
{
immediate: false
}
);
useEffect(() => {
if (open) {
void generateToken();
}
}, [open]);
return (
<Dialog
sx={dialogStyle}
onClose={onClose}
open={!!username}
fullWidth
maxWidth="sm"
>
<DialogTitle>{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
<DialogContent dividers>
{token ? (
<>
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
<Box mt={2} mb={2}>
<TextField
label="Token"
multiline
value={token.token}
fullWidth
contentEditable={false}
/>
</Box>
</>
) : (
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">{LL.GENERATING_TOKEN()}&hellip;</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
<Button
startIcon={<CloseIcon />}
variant="outlined"
onClick={onClose}
color="secondary"
>
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
);
};
export default GenerateToken;

View File

@@ -0,0 +1,286 @@
import { useContext, useState } from 'react';
import type { FC } from 'react';
import { useBlocker } from 'react-router-dom';
import CancelIcon from '@mui/icons-material/Cancel';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import PersonAddIcon from '@mui/icons-material/PersonAdd';
import VpnKeyIcon from '@mui/icons-material/VpnKey';
import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, IconButton } from '@mui/material';
import * as SecurityApi from 'api/security';
import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import {
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType, UserType } from 'types';
import { useRest } from 'utils';
import { createUserValidator } from 'validators';
import GenerateToken from './GenerateToken';
import User from './User';
const ManageUsers: FC = () => {
const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
useRest<SecuritySettingsType>({
read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings
});
const [user, setUser] = useState<UserType>();
const [creating, setCreating] = useState<boolean>(false);
const [changed, setChanged] = useState<number>(0);
const [generatingToken, setGeneratingToken] = useState<string>();
const authenticatedContext = useContext(AuthenticatedContext);
const blocker = useBlocker(changed !== 0);
const { LL } = useI18nContext();
const table_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
`,
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
padding: 8px;
height: 36px;
border-bottom: 1px solid #565656;
}
`,
Row: `
.td {
padding: 8px;
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
&:nth-of-type(even) .td {
background-color: #1e1e1e;
}
`,
BaseCell: `
&:nth-of-type(2) {
text-align: center;
}
&:last-of-type {
text-align: right;
}
`
});
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const noAdminConfigured = () => !data.users.find((u) => u.admin);
const removeUser = (toRemove: UserType) => {
const users = data.users.filter((u) => u.username !== toRemove.username);
updateDataValue({ ...data, users });
setChanged(changed + 1);
};
const createUser = () => {
setCreating(true);
setUser({
username: '',
password: '',
admin: true
});
};
const editUser = (toEdit: UserType) => {
setCreating(false);
setUser({ ...toEdit });
};
const cancelEditingUser = () => {
setUser(undefined);
};
const doneEditingUser = () => {
if (user) {
const users = [
...data.users.filter(
(u: { username: string }) => u.username !== user.username
),
user
];
updateDataValue({ ...data, users });
setUser(undefined);
setChanged(changed + 1);
}
};
const closeGenerateToken = () => {
setGeneratingToken(undefined);
};
const generateToken = (username: string) => {
setGeneratingToken(username);
};
const onSubmit = async () => {
await saveData();
await authenticatedContext.refresh();
setChanged(0);
};
const onCancelSubmit = async () => {
await loadData();
setChanged(0);
};
interface UserType2 {
id: string;
username: string;
password: string;
admin: boolean;
}
// add id to the type, needed for the table
const user_table = data.users.map((u) => ({
...u,
id: u.username
})) as UserType2[];
return (
<>
<Table
data={{ nodes: user_table }}
theme={table_theme}
layout={{ custom: true }}
>
{(tableList: UserType2[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize>{LL.USERNAME(1)}</HeaderCell>
<HeaderCell stiff>{LL.IS_ADMIN(0)}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
<Body>
{tableList.map((u: UserType2) => (
<Row key={u.id} item={u}>
<Cell>{u.username}</Cell>
<Cell stiff>{u.admin ? <CheckIcon /> : <CloseIcon />}</Cell>
<Cell stiff>
<IconButton
size="small"
disabled={!authenticatedContext.me.admin}
onClick={() => generateToken(u.username)}
>
<VpnKeyIcon />
</IconButton>
<IconButton size="small" onClick={() => removeUser(u)}>
<DeleteIcon />
</IconButton>
<IconButton size="small" onClick={() => editUser(u)}>
<EditIcon />
</IconButton>
</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
{noAdminConfigured() && (
<MessageBox level="warning" message={LL.USER_WARNING()} my={2} />
)}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
{changed !== 0 && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={onCancelSubmit}
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving || noAdminConfigured()}
variant="contained"
color="info"
type="submit"
onClick={onSubmit}
>
{LL.APPLY_CHANGES(changed)}
</Button>
</ButtonRow>
)}
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button
startIcon={<PersonAddIcon />}
variant="outlined"
color="secondary"
onClick={createUser}
>
{LL.ADD(0)}
</Button>
</ButtonRow>
</Box>
</Box>
<GenerateToken username={generatingToken} onClose={closeGenerateToken} />
<User
user={user}
setUser={setUser}
creating={creating}
onDoneEditing={doneEditingUser}
onCancelEditing={cancelEditingUser}
validator={createUserValidator(data.users, creating)}
/>
</>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>
);
};
export default ManageUsers;

View File

@@ -0,0 +1,33 @@
import type { FC } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Tab } from '@mui/material';
import { RouterTabs, useLayoutTitle, useRouterTab } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import ManageUsers from './ManageUsers';
import SecuritySettings from './SecuritySettings';
const Security: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.SETTINGS_OF(LL.SECURITY(0)));
const { routerTab } = useRouterTab();
return (
<>
<RouterTabs value={routerTab}>
<Tab value="settings" label={LL.SETTINGS_OF(LL.SECURITY(1))} />
<Tab value="users" label={LL.MANAGE_USERS()} />
</RouterTabs>
<Routes>
<Route path="users" element={<ManageUsers />} />
<Route path="settings" element={<SecuritySettings />} />
<Route path="*" element={<Navigate replace to="settings" />} />
</Routes>
</>
);
};
export default Security;

View File

@@ -0,0 +1,119 @@
import { useContext, useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
import { Button } from '@mui/material';
import * as SecurityApi from 'api/security';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent,
ValidatedPasswordField
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils';
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
const SecuritySettings: FC = () => {
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const {
loadData,
saving,
data,
updateDataValue,
origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<SecuritySettingsType>({
read: SecurityApi.readSecuritySettings,
update: SecurityApi.updateSecuritySettings
});
const authenticatedContext = useContext(AuthenticatedContext);
const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(SECURITY_SETTINGS_VALIDATOR, data);
await saveData();
await authenticatedContext.refresh();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
}
};
return (
<>
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="jwt_secret"
label={LL.SU_PASSWORD()}
fullWidth
variant="outlined"
value={data.jwt_secret}
onChange={updateFormValue}
margin="normal"
/>
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
{dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow>
<Button
startIcon={<CancelIcon />}
disabled={saving}
variant="outlined"
color="primary"
type="submit"
onClick={loadData}
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
disabled={saving}
variant="contained"
color="info"
type="submit"
onClick={validateAndSubmit}
>
{LL.APPLY_CHANGES(dirtyFlags.length)}
</Button>
</ButtonRow>
)}
</>
);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content()}
</SectionContent>
);
};
export default SecuritySettings;

View File

@@ -0,0 +1,142 @@
import { useEffect, useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import PersonAddIcon from '@mui/icons-material/PersonAdd';
import SaveIcon from '@mui/icons-material/Save';
import {
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle
} from '@mui/material';
import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator';
import {
BlockFormControlLabel,
ValidatedPasswordField,
ValidatedTextField
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import type { UserType } from 'types';
import { updateValue } from 'utils';
import { validate } from 'validators';
interface UserFormProps {
creating: boolean;
validator: Schema;
user?: UserType;
setUser: React.Dispatch<React.SetStateAction<UserType | undefined>>;
onDoneEditing: () => void;
onCancelEditing: () => void;
}
const User: FC<UserFormProps> = ({
creating,
validator,
user,
setUser,
onDoneEditing,
onCancelEditing
}) => {
const { LL } = useI18nContext();
const updateFormValue = updateValue(setUser);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const open = !!user;
useEffect(() => {
if (open) {
setFieldErrors(undefined);
}
}, [open]);
const validateAndDone = async () => {
if (user) {
try {
setFieldErrors(undefined);
await validate(validator, user);
onDoneEditing();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
}
}
};
return (
<Dialog
sx={dialogStyle}
onClose={onCancelEditing}
open={!!user}
fullWidth
maxWidth="sm"
>
{user && (
<>
<DialogTitle id="user-form-dialog-title">
{creating ? LL.ADD(1) : LL.MODIFY()}&nbsp;{LL.USER(1)}
</DialogTitle>
<DialogContent dividers>
<ValidatedTextField
fieldErrors={fieldErrors}
name="username"
label={LL.USERNAME(1)}
fullWidth
variant="outlined"
value={user.username}
disabled={!creating}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={user.password}
onChange={updateFormValue}
margin="normal"
/>
<BlockFormControlLabel
control={
<Checkbox
name="admin"
checked={user.admin}
onChange={updateFormValue}
/>
}
label={LL.IS_ADMIN(1)}
/>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onCancelEditing}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={creating ? <PersonAddIcon /> : <SaveIcon />}
variant="outlined"
onClick={validateAndDone}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()}
</Button>
</DialogActions>
</>
)}
</Dialog>
);
};
export default User;