sync with dev

This commit is contained in:
proddy
2026-04-15 09:25:38 +02:00
parent 16c0370443
commit 4d3b31e5a1
67 changed files with 1747 additions and 1125 deletions

View File

@@ -26,8 +26,8 @@
"@alova/adapter-xhr": "2.3.1", "@alova/adapter-xhr": "2.3.1",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.9", "@mui/icons-material": "^9.0.0",
"@mui/material": "^7.3.9", "@mui/material": "^9.0.0",
"@preact/compat": "^18.3.2", "@preact/compat": "^18.3.2",
"@table-library/react-table-library": "4.1.15", "@table-library/react-table-library": "4.1.15",
"alova": "^3.5.1", "alova": "^3.5.1",
@@ -37,11 +37,11 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
"mime-types": "^3.0.2", "mime-types": "^3.0.2",
"preact": "^10.29.0", "preact": "^10.29.1",
"react": "^19.2.4", "react": "^19.2.5",
"react-dom": "^19.2.4", "react-dom": "^19.2.5",
"react-icons": "^5.6.0", "react-icons": "^5.6.0",
"react-router": "^7.13.2", "react-router": "^7.14.1",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"typesafe-i18n": "^5.27.1", "typesafe-i18n": "^5.27.1",
"typescript": "^6.0.2" "typescript": "^6.0.2"
@@ -52,18 +52,18 @@
"@preact/compat": "^18.3.2", "@preact/compat": "^18.3.2",
"@preact/preset-vite": "^2.10.5", "@preact/preset-vite": "^2.10.5",
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.5.0", "@types/node": "^25.6.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"axe-core": "^4.11.1", "axe-core": "^4.11.3",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^10.1.0", "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1", "prettier": "^3.8.3",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"terser": "^5.46.1", "terser": "^5.46.1",
"typescript-eslint": "^8.58.0", "typescript-eslint": "^8.58.2",
"vite": "^8.0.3", "vite": "^8.0.8",
"vite-plugin-imagemin": "^0.6.1" "vite-plugin-imagemin": "^0.6.1"
}, },
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"

862
interface/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ import { toast } from 'react-toastify';
import ForwardIcon from '@mui/icons-material/Forward'; import ForwardIcon from '@mui/icons-material/Forward';
import { Box, Button, Paper, Typography } from '@mui/material'; import { Box, Button, Paper, Typography } from '@mui/material';
import type { Theme } from '@mui/material/styles';
import * as AuthenticationApi from 'components/routing/authentication'; import * as AuthenticationApi from 'components/routing/authentication';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
@@ -36,7 +37,7 @@ const SignIn = memo(() => {
{ {
immediate: false immediate: false
} }
).onSuccess((response) => { ).onSuccess((response: { data: { access_token: string } }) => {
if (response.data) { if (response.data) {
authenticationContext.signIn(response.data.access_token); authenticationContext.signIn(response.data.access_token);
} }
@@ -78,7 +79,6 @@ const SignIn = memo(() => {
} }
}, [signInRequest, signIn, LL]); }, [signInRequest, signIn, LL]);
// Memoize callback to prevent recreation on every render
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]); const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
// get rid of scrollbar // get rid of scrollbar
@@ -92,13 +92,15 @@ const SignIn = memo(() => {
return ( return (
<Box <Box
display="flex" sx={(theme: Theme) => ({
height="100vh" display: 'flex',
margin="auto" height: '100vh',
padding={2} margin: 'auto',
justifyContent="center" padding: 2,
flexDirection="column" justifyContent: 'center',
maxWidth={(theme) => theme.breakpoints.values.sm} flexDirection: 'column',
maxWidth: theme.breakpoints.values.sm
})}
> >
<Paper <Paper
sx={(theme) => ({ sx={(theme) => ({
@@ -111,16 +113,18 @@ const SignIn = memo(() => {
width: '100%' width: '100%'
})} })}
> >
<Typography mb={1} variant="h4"> <Typography sx={{ mb: 1 }} variant="h4">
{PROJECT_NAME} {PROJECT_NAME}
</Typography> </Typography>
<LanguageSelector /> <LanguageSelector />
<Box <Box
mt={1} sx={{
display="flex" mt: 1,
flexDirection="column" display: 'flex',
gap={1} flexDirection: 'column',
alignItems="center" gap: 1,
alignItems: 'center'
}}
> >
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors || {}}

View File

@@ -343,9 +343,9 @@ const CustomEntities = () => {
return ( return (
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main"> <Typography sx={{ mb: 2 }} color="warning" variant="body1">
<Typography variant="body1">{LL.ENTITIES_HELP_1()}.</Typography> {LL.ENTITIES_HELP_1()}.
</Box> </Typography>
{renderEntity()} {renderEntity()}
@@ -361,8 +361,8 @@ const CustomEntities = () => {
/> />
)} )}
<Box mt={2} display="flex" flexWrap="wrap"> <Box sx={{ mt: 2, display: 'flex', flexWrap: 'wrap' }}>
<Box flexGrow={1}> <Box sx={{ flexGrow: 1 }}>
{numChanges > 0 && ( {numChanges > 0 && (
<ButtonRow> <ButtonRow>
<Button <Button
@@ -384,7 +384,7 @@ const CustomEntities = () => {
</ButtonRow> </ButtonRow>
)} )}
</Box> </Box>
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
<Button <Button
startIcon={<AddIcon />} startIcon={<AddIcon />}
variant="outlined" variant="outlined"

View File

@@ -7,7 +7,7 @@ import DoneIcon from '@mui/icons-material/Done';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined'; import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
import { import {
Box, Box,
Button, Button,
@@ -178,7 +178,7 @@ const CustomEntitiesDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
/> />
</Grid> </Grid>
<Grid mt={3}> <Grid sx={{ mt: 3 }}>
<BlockFormControlLabel <BlockFormControlLabel
control={ control={
<Checkbox <Checkbox
@@ -238,7 +238,7 @@ const CustomEntitiesDialog = ({
)} )}
{editItem.ram === 0 && ( {editItem.ram === 0 && (
<> <>
<Grid mt={3}> <Grid sx={{ mt: 3 }}>
<BlockFormControlLabel <BlockFormControlLabel
control={ control={
<Checkbox <Checkbox
@@ -404,7 +404,7 @@ const CustomEntitiesDialog = ({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{!creating && ( {!creating && (
<Box flexGrow={1}> <Box sx={{ flexGrow: 1 }}>
<Button <Button
startIcon={<RemoveIcon />} startIcon={<RemoveIcon />}
variant="outlined" variant="outlined"

View File

@@ -470,10 +470,10 @@ const Customizations = () => {
const renderDeviceList = () => ( const renderDeviceList = () => (
<> <>
<Box mb={1} color="warning.main"> <Typography sx={{ mb: 1 }} color="warning" variant="body1">
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography> {LL.CUSTOMIZATIONS_HELP_1()}.
</Box> </Typography>
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}> <Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 2 }}>
{rename ? ( {rename ? (
<> <>
<TextField <TextField
@@ -570,27 +570,22 @@ const Customizations = () => {
const renderDeviceData = () => { const renderDeviceData = () => {
return ( return (
<> <>
<Box color="warning.main"> <Typography sx={{ mt: 1, mb: 1 }} color="warning" variant="body2">
<Typography variant="body2" mt={1} mb={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()} <OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
&nbsp;&nbsp; &nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()} <OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
&nbsp;&nbsp; &nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />= <OptionIcon type="api_mqtt_exclude" isSet={true} />=
{LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp; {LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />= <OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
{LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp; &nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()} <OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography> </Typography>
</Box>
<Grid <Grid
container container
mb={1}
mt={0}
spacing={2} spacing={2}
direction="row" direction="row"
justifyContent="flex-start" sx={{ mb: 1, mt: 0, justifyContent: 'flex-start', alignItems: 'center' }}
alignItems="center"
> >
<Grid> <Grid>
<TextField <TextField
@@ -779,8 +774,8 @@ const Customizations = () => {
</Button> </Button>
</MessageBox> </MessageBox>
) : ( ) : (
<Box display="flex" flexWrap="wrap"> <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
<Box flexGrow={1}> <Box sx={{ flexGrow: 1 }}>
{numChanges !== 0 && ( {numChanges !== 0 && (
<ButtonRow> <ButtonRow>
<Button <Button

View File

@@ -37,7 +37,7 @@ interface LabelValueProps {
const LabelValue = memo(({ label, value }: LabelValueProps) => ( const LabelValue = memo(({ label, value }: LabelValueProps) => (
<Grid container direction="row"> <Grid container direction="row">
<Typography variant="body2" color="warning.main"> <Typography variant="body2" color="warning">
{label}:&nbsp; {label}:&nbsp;
</Typography> </Typography>
<Typography variant="body2">{value}</Typography> <Typography variant="body2">{value}</Typography>
@@ -131,7 +131,7 @@ const CustomizationsDialog = ({
/> />
<LabelValue label={LL.WRITEABLE()} value={writeableIcon} /> <LabelValue label={LL.WRITEABLE()} value={writeableIcon} />
<Box mt={1} mb={2}> <Box sx={{ mt: 1, mb: 2 }}>
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} /> <EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
</Box> </Box>
@@ -172,7 +172,7 @@ const CustomizationsDialog = ({
</Grid> </Grid>
{error && ( {error && (
<Typography variant="body2" color="error" mt={2}> <Typography sx={{ mt: 2 }} variant="body2" color="error">
Error: Check min and max values Error: Check min and max values
</Typography> </Typography>
)} )}

View File

@@ -6,7 +6,7 @@ import { toast } from 'react-toastify';
import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; import HelpOutlineIcon from '@mui/icons-material/HelpOutlined';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'; import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import { import {
@@ -263,7 +263,7 @@ const Dashboard = memo(() => {
return ( return (
<> <>
{data.connected && data.nodes.length > 0 && !hasFavEntities && ( {data.connected && data.nodes.length > 0 && !hasFavEntities && (
<MessageBox mb={2} level="warning"> <MessageBox sx={{ mb: 2 }} level="warning">
<Typography> <Typography>
{LL.NO_DATA_1()}&nbsp; {LL.NO_DATA_1()}&nbsp;
<Link to="/customizations" style={{ color: 'white' }}> <Link to="/customizations" style={{ color: 'white' }}>
@@ -280,10 +280,12 @@ const Dashboard = memo(() => {
)} )}
<Box <Box
display="flex" sx={{
justifyContent="flex-end" display: 'flex',
flexWrap="nowrap" justifyContent: 'flex-end',
whiteSpace="nowrap" flexWrap: 'nowrap',
whiteSpace: 'nowrap'
}}
> >
<ToggleButtonGroup <ToggleButtonGroup
size="small" size="small"
@@ -306,7 +308,7 @@ const Dashboard = memo(() => {
</Box> </Box>
{data.nodes.length > 0 ? ( {data.nodes.length > 0 ? (
<Box mt={1} justifyContent="center" flexDirection="column"> <Box sx={{ mt: 1, justifyContent: 'center', flexDirection: 'column' }}>
<IconContext.Provider <IconContext.Provider
value={{ value={{
color: 'lightblue', color: 'lightblue',
@@ -373,13 +375,8 @@ const Dashboard = memo(() => {
</IconContext.Provider> </IconContext.Provider>
</Box> </Box>
) : ( ) : (
<Box <Box sx={{ display: 'flex' }}>
display="flex" <Typography sx={{ mt: 1 }} color="warning" variant="body1">
// justifyContent="flex-end"
// flexWrap="nowrap"
// whiteSpace="nowrap"
>
<Typography mt={1} color="warning.main" variant="body1">
no data no data
</Typography> </Typography>
<Tooltip title={LL.DASHBOARD_1()}> <Tooltip title={LL.DASHBOARD_1()}>

View File

@@ -1,4 +1,5 @@
import { memo } from 'react'; import { memo } from 'react';
import type { IconType } from 'react-icons';
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai'; import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
import { CgSmartHomeBoiler } from 'react-icons/cg'; import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa'; import { FaSolarPanel } from 'react-icons/fa';
@@ -15,14 +16,9 @@ import { PiFan, PiGauge } from 'react-icons/pi';
import { TiFlowSwitch, TiThermometer } from 'react-icons/ti'; import { TiFlowSwitch, TiThermometer } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc'; import { VscVmConnect } from 'react-icons/vsc';
import type { SvgIconProps } from '@mui/material';
import { DeviceType } from './types'; import { DeviceType } from './types';
const deviceIconLookup: Record< const deviceIconLookup: Record<DeviceType, IconType | null> = {
DeviceType,
React.ComponentType<SvgIconProps> | null
> = {
[DeviceType.TEMPERATURESENSOR]: TiThermometer, [DeviceType.TEMPERATURESENSOR]: TiThermometer,
[DeviceType.ANALOGSENSOR]: PiGauge, [DeviceType.ANALOGSENSOR]: PiGauge,
[DeviceType.BOILER]: CgSmartHomeBoiler, [DeviceType.BOILER]: CgSmartHomeBoiler,

View File

@@ -546,7 +546,7 @@ const Devices = memo(() => {
) )
</MessageBox> </MessageBox>
) : ( ) : (
<Box justifyContent="center" flexDirection="column"> <Box sx={{ justifyContent: 'center', flexDirection: 'column' }}>
<IconContext.Provider <IconContext.Provider
value={{ value={{
color: 'lightblue', color: 'lightblue',
@@ -670,12 +670,12 @@ const Devices = memo(() => {
}} }}
> >
<Box sx={{ p: 1 }}> <Box sx={{ p: 1 }}>
<Grid container justifyContent="space-between"> <Grid container sx={{ justifyContent: 'space-between' }}>
<Typography noWrap variant="subtitle1" color="warning.main"> <Typography noWrap variant="subtitle1" color="warning">
{deviceInfo.n}&nbsp;( {deviceInfo.n}&nbsp;(
{deviceInfo.tn}) {deviceInfo.tn})
</Typography> </Typography>
<Grid justifyContent="flex-end"> <Grid sx={{ justifyContent: 'flex-end' }}>
<ButtonTooltip title={LL.CLOSE()}> <ButtonTooltip title={LL.CLOSE()}>
<IconButton onClick={resetDeviceSelect} aria-label={LL.CLOSE()}> <IconButton onClick={resetDeviceSelect} aria-label={LL.CLOSE()}>
<HighlightOffIcon color="primary" sx={{ fontSize: 18 }} /> <HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />

View File

@@ -128,9 +128,9 @@ const DevicesDialog = ({
<Dialog sx={dialogStyle} open={open} onClose={onClose}> <Dialog sx={dialogStyle} open={open} onClose={onClose}>
<DialogTitle>{dialogTitle}</DialogTitle> <DialogTitle>{dialogTitle}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box color="warning.main" mb={2}> <Typography sx={{ mb: 2 }} color="warning" variant="body2">
<Typography variant="body2">{editItem.id.slice(2)}</Typography> {editItem.id.slice(2)}
</Box> </Typography>
<Grid container> <Grid container>
<Grid size={12}> <Grid size={12}>
{editItem.l ? ( {editItem.l ? (

View File

@@ -11,6 +11,7 @@ import {
Box, Box,
Button, Button,
Divider, Divider,
Grid,
Link, Link,
List, List,
ListItem, ListItem,
@@ -42,7 +43,7 @@ interface CustomSupport {
html: string | null; html: string | null;
} }
const DEFAULT_IMAGE_URL = 'https://emsesp.org/_media/images/installer.jpeg'; const DEFAULT_IMAGE_URL = 'https://emsesp.org/media/images/installer.jpeg';
const SUPPORT_BOX_STYLES: SxProps<Theme> = { const SUPPORT_BOX_STYLES: SxProps<Theme> = {
borderRadius: 3, borderRadius: 3,
@@ -71,7 +72,6 @@ const HelpComponent = () => {
}); });
const [imgError, setImgError] = useState<boolean>(false); const [imgError, setImgError] = useState<boolean>(false);
// Memoize the request method to prevent re-creation on every render
const getCustomSupportMethod = useMemo( const getCustomSupportMethod = useMemo(
() => callAction({ action: 'getCustomSupport' }), () => callAction({ action: 'getCustomSupport' }),
[] []
@@ -146,11 +146,9 @@ const HelpComponent = () => {
<SectionContent> <SectionContent>
{customSupport.html && ( {customSupport.html && (
<Stack <Stack
padding={1}
mb={2}
direction="row" direction="row"
divider={<Divider orientation="vertical" flexItem />} divider={<Divider orientation="vertical" flexItem />}
sx={SUPPORT_BOX_STYLES} sx={{ padding: 1, mb: 2, ...SUPPORT_BOX_STYLES }}
> >
<Typography variant="subtitle1"> <Typography variant="subtitle1">
<div dangerouslySetInnerHTML={{ __html: customSupport.html }} /> <div dangerouslySetInnerHTML={{ __html: customSupport.html }} />
@@ -185,9 +183,9 @@ const HelpComponent = () => {
</List> </List>
)} )}
<Box p={2} color="warning.main"> <Grid container spacing={2} sx={{ mt: 2, alignItems: 'center' }}>
<Typography mb={1} variant="body1"> <Typography sx={{ mb: 1 }} color="warning" variant="body1">
{LL.HELP_INFORMATION_4()}. {LL.HELP_INFORMATION_4()}:
</Typography> </Typography>
<Button <Button
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
@@ -197,11 +195,11 @@ const HelpComponent = () => {
> >
{LL.SUPPORT_INFORMATION(0)} {LL.SUPPORT_INFORMATION(0)}
</Button> </Button>
</Box> </Grid>
<Divider sx={{ mt: 4 }} /> <Divider sx={{ mt: 4 }} />
<Typography color="white" variant="subtitle1" align="center" mt={1}> <Typography color="white" variant="subtitle1" align="center" sx={{ mt: 1 }}>
&copy;&nbsp; &copy;&nbsp;
<Link <Link
target="_blank" target="_blank"

View File

@@ -186,9 +186,9 @@ const Modules = () => {
return ( return (
<> <>
<Box mb={2} color="warning.main"> <Typography sx={{ mb: 2 }} color="warning" variant="body1">
<Typography variant="body1">{LL.MODULES_DESCRIPTION()}.</Typography> {LL.MODULES_DESCRIPTION()}.
</Box> </Typography>
<Table <Table
data={{ nodes: modules }} data={{ nodes: modules }}
theme={modules_theme} theme={modules_theme}
@@ -236,8 +236,8 @@ const Modules = () => {
)} )}
</Table> </Table>
<Box mt={1} display="flex" flexWrap="wrap"> <Box sx={{ mt: 1, display: 'flex', flexWrap: 'wrap' }}>
<Box flexGrow={1}> <Box sx={{ flexGrow: 1 }}>
{numChanges !== 0 && ( {numChanges !== 0 && (
<ButtonRow> <ButtonRow>
<Button <Button

View File

@@ -79,7 +79,7 @@ const ModulesDialog = ({
label="Enabled" label="Enabled"
/> />
</Grid> </Grid>
<Box mt={2} mb={1}> <Box sx={{ mt: 2, mb: 1 }}>
<TextField <TextField
name="license" name="license"
label="License Key" label="License Key"

View File

@@ -2,12 +2,12 @@ import { memo } from 'react';
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined'; import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlined';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined'; import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined'; import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
import StarIcon from '@mui/icons-material/Star'; import StarIcon from '@mui/icons-material/Star';
import StarOutlineIcon from '@mui/icons-material/StarOutline'; import StarOutlineIcon from '@mui/icons-material/StarOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import type { SvgIconProps } from '@mui/material'; import type { SvgIconProps } from '@mui/material';

View File

@@ -358,9 +358,9 @@ const Scheduler = () => {
return ( return (
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
<Box mb={2} color="warning.main"> <Typography sx={{ mb: 2 }} color="warning" variant="body1">
<Typography variant="body1">{LL.SCHEDULER_HELP_1()}.</Typography> {LL.SCHEDULER_HELP_1()}.
</Box> </Typography>
{renderSchedule()} {renderSchedule()}
{selectedScheduleItem && ( {selectedScheduleItem && (
@@ -375,8 +375,8 @@ const Scheduler = () => {
/> />
)} )}
<Box display="flex" flexWrap="wrap"> <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
<Box flexGrow={1}> <Box sx={{ flexGrow: 1 }}>
{numChanges !== 0 && ( {numChanges !== 0 && (
<ButtonRow> <ButtonRow>
<Button <Button
@@ -398,7 +398,7 @@ const Scheduler = () => {
</ButtonRow> </ButtonRow>
)} )}
</Box> </Box>
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
<ButtonRow> <ButtonRow>
<Button <Button
startIcon={<AddIcon />} startIcon={<AddIcon />}

View File

@@ -4,7 +4,7 @@ import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
import { import {
Box, Box,
Button, Button,
@@ -338,11 +338,13 @@ const SchedulerDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
/> />
{isTimerSchedule && ( {isTimerSchedule && (
<Box color="warning.main" ml={2} mt={4}> <Typography
<Typography variant="body2"> sx={{ ml: 2, mt: 4 }}
color="warning"
variant="body2"
>
{LL.SCHEDULER_HELP_2()} {LL.SCHEDULER_HELP_2()}
</Typography> </Typography>
</Box>
)} )}
</> </>
) : ( ) : (
@@ -391,7 +393,7 @@ const SchedulerDialog = ({
<DialogActions> <DialogActions>
{!creating && ( {!creating && (
<Box flexGrow={1}> <Box sx={{ flexGrow: 1 }}>
<Button <Button
startIcon={<RemoveIcon />} startIcon={<RemoveIcon />}
variant="outlined" variant="outlined"

View File

@@ -591,7 +591,14 @@ const Sensors = () => {
/> />
)} )}
{sensorData?.analog_enabled === true && me.admin && ( {sensorData?.analog_enabled === true && me.admin && (
<Box mt={2} display="flex" flexWrap="wrap" justifyContent="flex-end"> <Box
sx={{
mt: 2,
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'flex-end'
}}
>
<Button <Button
variant="outlined" variant="outlined"
color="primary" color="primary"

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { import {
Box, Box,
@@ -479,7 +479,7 @@ const SensorsAnalogDialog = ({
)} )}
</Grid> </Grid>
{fieldErrors && Object.keys(fieldErrors).length > 0 && ( {fieldErrors && Object.keys(fieldErrors).length > 0 && (
<Box mt={1}> <Box sx={{ mt: 1 }}>
{Object.values(fieldErrors).map((errArr, idx) => {Object.values(fieldErrors).map((errArr, idx) =>
Array.isArray(errArr) Array.isArray(errArr)
? errArr.map((err, j) => ( ? errArr.map((err, j) => (
@@ -487,7 +487,7 @@ const SensorsAnalogDialog = ({
key={`${idx}-${j}`} key={`${idx}-${j}`}
color="error" color="error"
variant="caption" variant="caption"
display="block" sx={{ display: 'block' }}
> >
{err.message} {err.message}
</Typography> </Typography>
@@ -498,7 +498,7 @@ const SensorsAnalogDialog = ({
)} )}
{editItem.s && ( {editItem.s && (
<Grid> <Grid>
<Typography mt={1} color="warning.main" variant="body2"> <Typography sx={{ mt: 1 }} color="warning" variant="body2">
<WarningIcon <WarningIcon
fontSize="small" fontSize="small"
sx={{ mr: 1, verticalAlign: 'middle' }} sx={{ mr: 1, verticalAlign: 'middle' }}
@@ -511,7 +511,7 @@ const SensorsAnalogDialog = ({
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{!creating && ( {!creating && (
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}> <Box sx={{ flexGrow: 1, '& button': { mt: 0 } }}>
<Button <Button
startIcon={<RemoveIcon />} startIcon={<RemoveIcon />}
disabled={editItem.s} disabled={editItem.s}

View File

@@ -4,7 +4,6 @@ import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { import {
Box,
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
@@ -111,11 +110,9 @@ const SensorsTemperatureDialog = ({
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <Dialog sx={dialogStyle} open={open} onClose={handleClose}>
<DialogTitle>{dialogTitle}</DialogTitle> <DialogTitle>{dialogTitle}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box color="warning.main" mb={2}> <Typography sx={{ mb: 2 }} color="warning" variant="body2">
<Typography variant="body2">
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id} {LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
</Typography> </Typography>
</Box>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
@@ -142,7 +139,7 @@ const SensorsTemperatureDialog = ({
</Grid> </Grid>
{editItem.s && ( {editItem.s && (
<Grid> <Grid>
<Typography mt={1} color="warning.main" variant="body2"> <Typography sx={{ mt: 1 }} color="warning" variant="body2">
<WarningIcon <WarningIcon
fontSize="small" fontSize="small"
sx={{ mr: 1, verticalAlign: 'middle' }} sx={{ mr: 1, verticalAlign: 'middle' }}

View File

@@ -41,8 +41,12 @@ const UserProfileComponent = () => {
/> />
</ListItem> </ListItem>
</List> </List>
<Box mt={2} mb={2} display="flex" alignItems="center"> <Box sx={{ mt: 2, mb: 2, display: 'flex', alignItems: 'center' }}>
<Typography mr={2} variant="body1" align="center"> <Typography
sx={{ mr: 2, textAlign: 'center' }}
color="warning"
variant="body1"
>
{LL.LANGUAGE()}: {LL.LANGUAGE()}:
</Typography> </Typography>
<LanguageSelector /> <LanguageSelector />

View File

@@ -43,16 +43,6 @@ export interface Settings {
modbus_port: number; modbus_port: number;
modbus_max_clients: number; modbus_max_clients: number;
modbus_timeout: number; modbus_timeout: number;
email_enabled: boolean;
email_ssl?: boolean;
email_starttls?: boolean;
email_server: string;
email_port: number;
email_login: string;
email_pass: string;
email_sender: string;
email_recp: string;
email_subject: string;
developer_mode: boolean; developer_mode: boolean;
} }

View File

@@ -28,7 +28,6 @@ import {
FormLoader, FormLoader,
MessageBox, MessageBox,
SectionContent, SectionContent,
ValidatedPasswordField,
ValidatedTextField, ValidatedTextField,
useLayoutTitle useLayoutTitle
} from 'components'; } from 'components';
@@ -352,156 +351,6 @@ const ApplicationSettings = () => {
</Grid> </Grid>
</Grid> </Grid>
)} )}
<Typography color="secondary">eMail</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.email_enabled}
onChange={updateFormValue}
name="email_enabled"
disabled={!hardwareData.psram}
/>
}
label={
<Typography color={!hardwareData.psram ? 'grey' : 'default'}>
Enable eMail notification
{!hardwareData.psram && (
<Typography variant="caption">
&nbsp; &#40;{LL.IS_REQUIRED('PSRAM')}&#41;
</Typography>
)}
</Typography>
}
/>
{data.email_enabled && (
<>
<Grid
container
spacing={2}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_server"
label="SMTP Server"
variant="outlined"
value={data.email_server}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
sx={{ width: '12ch' }}
name="email_port"
variant="outlined"
label="Port"
value={numberValue(data.email_port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid size={4} mt={!data.email_ssl && !data.email_starttls ? 0 : 3}>
{!data.email_starttls && (
<BlockFormControlLabel
sx={{ width: '12ch' }}
control={
<Checkbox
checked={data.email_ssl}
onChange={updateFormValue}
name="email_ssl"
disabled={
data.email_starttls || data.email_ssl === undefined
}
/>
}
label="SSL/TLS"
/>
)}
{!data.email_ssl && (
<BlockFormControlLabel
sx={{ width: '12ch' }}
control={
<Checkbox
checked={data.email_starttls}
onChange={updateFormValue}
name="email_starttls"
disabled={
data.email_ssl || data.email_starttls === undefined
}
/>
}
label="STARTTLS"
/>
)}
</Grid>
</Grid>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_login"
label="Login"
variant="outlined"
value={data.email_login}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedPasswordField
fieldErrors={fieldErrors || {}}
name="email_pass"
label="Password"
variant="outlined"
value={data.email_pass}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_sender"
label="From"
variant="outlined"
value={data.email_sender}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_recp"
label="To"
variant="outlined"
value={data.email_recp}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_subject"
label="Subject"
variant="outlined"
value={data.email_subject}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
</>
)}
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary"> <Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.SENSORS()} {LL.SENSORS()}
</Typography> </Typography>
@@ -922,7 +771,7 @@ const ApplicationSettings = () => {
label={LL.REMOTE_TIMEOUT_EN()} label={LL.REMOTE_TIMEOUT_EN()}
/> />
{data.remote_timeout_en && ( {data.remote_timeout_en && (
<Box mt={2}> <Box sx={{ mt: 2 }}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors || {}}
name="remote_timeout" name="remote_timeout"

View File

@@ -1,12 +1,23 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
import { Box, Button, Grid, Typography } from '@mui/material'; import WarningIcon from '@mui/icons-material/Warning';
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { API, callAction } from 'api/app'; import { API, callAction } from 'api/app';
import { dialogStyle } from '@/CustomTheme';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types'; import type { APIcall } from 'app/main/types';
import SystemMonitor from 'app/status/SystemMonitor'; import SystemMonitor from 'app/status/SystemMonitor';
@@ -19,15 +30,11 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { saveFile } from 'utils'; import { saveFile } from 'utils';
interface DownloadButton {
type: string;
label: string | number;
isGridButton: boolean;
}
const DownloadUpload = () => { const DownloadUpload = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [confirmBackup, setConfirmBackup] = useState<boolean>(false);
const [restarting, setRestarting] = useState<boolean>(false); const [restarting, setRestarting] = useState<boolean>(false);
const { send: sendExportData } = useRequest( const { send: sendExportData } = useRequest(
@@ -62,40 +69,44 @@ const DownloadUpload = () => {
useLayoutTitle(LL.DOWNLOAD_UPLOAD()); useLayoutTitle(LL.DOWNLOAD_UPLOAD());
const downloadButtons: DownloadButton[] = useMemo( const handleCloseBackupDialog = useCallback(() => {
() => [ setConfirmBackup(false);
{ }, []);
type: 'settings',
label: LL.SETTINGS_OF(LL.APPLICATION()), const renderBackupDialog = useMemo(
isGridButton: true () => (
}, <Dialog
{ sx={dialogStyle}
type: 'customizations', open={confirmBackup}
label: LL.CUSTOMIZATIONS(), onClose={handleCloseBackupDialog}
isGridButton: true >
}, <DialogTitle>{LL.DOWNLOAD_SYSTEM_BACKUP()}</DialogTitle>
{ <DialogContent dividers>
type: 'entities', <WarningIcon color="warning" sx={{ fontSize: 18 }} />
label: LL.CUSTOM_ENTITIES(0), &nbsp;
isGridButton: true {LL.WARNING_SYSTEM_BACKUP()}
}, </DialogContent>
{ <DialogActions>
type: 'schedule', <Button
label: LL.SCHEDULE(0), startIcon={<CancelIcon />}
isGridButton: true variant="outlined"
}, onClick={handleCloseBackupDialog}
{ color="secondary"
type: 'systembackup', >
label: LL.DOWNLOAD_SYSTEM_BACKUP(), {LL.CANCEL()}
isGridButton: true </Button>
}, <Button
{ startIcon={<DownloadIcon />}
type: 'allvalues', variant="outlined"
label: LL.ALLVALUES(), onClick={() => handleDownload('systembackup')()}
isGridButton: false color="primary"
} >
], {LL.DOWNLOAD(0)}
[LL] </Button>
</DialogActions>
</Dialog>
),
[confirmBackup, handleCloseBackupDialog, LL]
); );
const handleDownload = useCallback( const handleDownload = useCallback(
@@ -117,58 +128,57 @@ const DownloadUpload = () => {
); );
} }
const gridButtons = downloadButtons.filter((btn) => btn.isGridButton);
const standaloneButton = downloadButtons.find((btn) => !btn.isGridButton);
return ( return (
<SectionContent> <SectionContent>
{renderBackupDialog}
<Typography sx={{ pb: 2 }} variant="h6" color="primary"> <Typography sx={{ pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)} {LL.DOWNLOAD(0)}
</Typography> </Typography>
<Typography mb={1} variant="body1" color="warning"> <Grid
{LL.DOWNLOAD_SETTINGS_TEXT()}. container
spacing={2}
sx={{
alignItems: 'center'
}}
>
<Typography variant="body1" color="warning">
{LL.DOWNLOAD_SETTINGS_TEXT()}:
</Typography> </Typography>
<Grid container spacing={2}>
{gridButtons.map((button) => (
<Grid key={button.type}>
<Button <Button
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={handleDownload(button.type)} onClick={() => setConfirmBackup(true)}
> >
{button.label} {LL.DOWNLOAD_SYSTEM_BACKUP()}
</Button> </Button>
</Grid> </Grid>
))}
</Grid>
<Typography mt={2} mb={1} variant="body1" color="warning"> <Grid container spacing={2} sx={{ mt: 2, alignItems: 'center' }}>
{LL.DOWNLOAD_SETTINGS_TEXT2()}. <Typography variant="body1" color="warning">
{LL.DOWNLOAD_SETTINGS_TEXT2()}:
</Typography> </Typography>
{standaloneButton && (
<Button <Button
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={handleDownload(standaloneButton.type)} onClick={handleDownload('allvalues')}
> >
{standaloneButton.label} {LL.ALLVALUES()}
</Button> </Button>
)} </Grid>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()} {LL.UPLOAD()}
</Typography> </Typography>
<Box color="warning.main" sx={{ pb: 2 }}> <Typography sx={{ pb: 2 }} color="warning" variant="body1">
<Typography variant="body1">{LL.UPLOAD_TEXT()}.</Typography> {LL.UPLOAD_TEXT()}:
</Box> </Typography>
<SingleUpload text={LL.UPLOAD_DRAG()} doRestart={doRestart} /> <SingleUpload doRestart={doRestart} />
</SectionContent> </SectionContent>
); );
}; };

View File

@@ -129,7 +129,7 @@ const MqttSettings = () => {
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
<> <>
<Box display="flex" gap={2} mb={1}> <Box sx={{ display: 'flex', gap: 2, mb: 1 }}>
<BlockFormControlLabel <BlockFormControlLabel
control={ control={
<Checkbox <Checkbox

View File

@@ -193,9 +193,9 @@ const NTPSettings = () => {
{timeZoneItems} {timeZoneItems}
</ValidatedTextField> </ValidatedTextField>
<Box display="flex" flexWrap="wrap"> <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
{!data.enabled && !dirtyFlags.length && ( {!data.enabled && !dirtyFlags.length && (
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
<ButtonRow> <ButtonRow>
<Button <Button
onClick={openSetTime} onClick={openSetTime}
@@ -259,9 +259,9 @@ const NTPSettings = () => {
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}> <Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle> <DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}> <Typography color="warning" variant="body2">
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography> {LL.SET_TIME_TEXT()}
</Box> </Typography>
<TextField <TextField
label={LL.LOCAL_TIME(0)} label={LL.LOCAL_TIME(0)}
type="datetime-local" type="datetime-local"

View File

@@ -156,11 +156,13 @@ const Settings = () => {
<Divider /> <Divider />
<Box <Box
mt={2} sx={{
display="flex" mt: 2,
justifyContent="flex-end" display: 'flex',
flexWrap="nowrap" justifyContent: 'flex-end',
whiteSpace="nowrap" flexWrap: 'nowrap',
whiteSpace: 'nowrap'
}}
> >
<Button <Button
startIcon={<SettingsBackupRestoreIcon />} startIcon={<SettingsBackupRestoreIcon />}

View File

@@ -54,19 +54,27 @@ const GenerateToken = ({ username, onClose }: GenerateTokenProps) => {
<DialogContent dividers> <DialogContent dividers>
{token ? ( {token ? (
<> <>
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} /> <MessageBox
<Box mt={2} mb={2}> message={LL.ACCESS_TOKEN_TEXT()}
level="info"
sx={{ mt: 2, mb: 2 }}
/>
<Box sx={{ mt: 2, mb: 2 }}>
<TextField <TextField
label="Token" label="Token"
multiline multiline
value={token.token} value={token.token}
fullWidth fullWidth
contentEditable={false} slotProps={{
input: {
readOnly: true
}
}}
/> />
</Box> </Box>
</> </>
) : ( ) : (
<Box m={4} textAlign="center"> <Box sx={{ m: 4, textAlign: 'center' }}>
<LinearProgress /> <LinearProgress />
<Typography variant="h6">{LL.GENERATING_TOKEN()}&hellip;</Typography> <Typography variant="h6">{LL.GENERATING_TOKEN()}&hellip;</Typography>
</Box> </Box>

View File

@@ -240,12 +240,16 @@ const ManageUsers = () => {
</Table> </Table>
{noAdminConfigured() && ( {noAdminConfigured() && (
<MessageBox level="warning" message={LL.USER_WARNING()} my={2} /> <MessageBox
level="warning"
message={LL.USER_WARNING()}
sx={{ mt: 2, mb: 2 }}
/>
)} )}
<Box display="flex" flexWrap="wrap"> <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
{changed !== 0 && ( {changed !== 0 && (
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}> <Box sx={{ flexGrow: 1, '& button': { mt: 2 } }}>
<ButtonRow> <ButtonRow>
<Button <Button
startIcon={<CancelIcon />} startIcon={<CancelIcon />}
@@ -270,7 +274,7 @@ const ManageUsers = () => {
</ButtonRow> </ButtonRow>
</Box> </Box>
)} )}
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
<ButtonRow> <ButtonRow>
<Button <Button
startIcon={<PersonAddIcon />} startIcon={<PersonAddIcon />}

View File

@@ -266,7 +266,7 @@ const SystemLog = () => {
return ( return (
<> <>
<Grid container spacing={2} alignItems="center"> <Grid container spacing={2} sx={{ alignItems: 'center' }}>
<Grid> <Grid>
<TextField <TextField
name="level" name="level"

View File

@@ -118,17 +118,15 @@ const SystemMonitor = () => {
p: 3 p: 3
}} }}
> >
<Box display="flex" alignItems="center" flexDirection="column"> <Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
<img <img
src="/app/icon.png" src="/app/icon.png"
alt="EMS-ESP" alt="EMS-ESP"
style={{ width: '40px', height: '40px', marginBottom: '16px' }} style={{ width: '40px', height: '40px', marginBottom: '16px' }}
/> />
<Typography <Typography
color="secondary" sx={{ color: 'secondary', fontWeight: 400, textAlign: 'center' }}
variant="h6" variant="h6"
fontWeight={400}
textAlign="center"
> >
{statusMessage} {statusMessage}
</Typography> </Typography>
@@ -148,11 +146,14 @@ const SystemMonitor = () => {
</MessageBox> </MessageBox>
) : ( ) : (
<> <>
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center"> <Typography
sx={{ mt: 2, fontWeight: 400, textAlign: 'center' }}
variant="h6"
>
{LL.PLEASE_WAIT()}&hellip; {LL.PLEASE_WAIT()}&hellip;
</Typography> </Typography>
{isUploading && ( {isUploading && (
<Box width="100%" pl={2} pr={2} py={2}> <Box sx={{ width: '100%', pl: 2, pr: 2, py: 2 }}>
<LinearProgressWithLabel value={progressValue} /> <LinearProgressWithLabel value={progressValue} />
</Box> </Box>
)} )}

View File

@@ -274,6 +274,8 @@ const InstallDialog = memo(
fetchDevVersion, fetchDevVersion,
latestVersion, latestVersion,
latestDevVersion, latestDevVersion,
upgradeImportantMessageType,
downloadOnly,
platform, platform,
LL, LL,
onClose, onClose,
@@ -283,6 +285,8 @@ const InstallDialog = memo(
fetchDevVersion: boolean; fetchDevVersion: boolean;
latestVersion?: VersionInfo; latestVersion?: VersionInfo;
latestDevVersion?: VersionInfo; latestDevVersion?: VersionInfo;
upgradeImportantMessageType: number;
downloadOnly: boolean;
platform: string; platform: string;
LL: TranslationFunctions; LL: TranslationFunctions;
onClose: () => void; onClose: () => void;
@@ -305,12 +309,24 @@ const InstallDialog = memo(
{`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`} {`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Typography mb={2}> <Typography sx={{ mb: 2 }}>
{LL.INSTALL_VERSION( {LL.INSTALL_VERSION(
LL.INSTALL(), downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
fetchDevVersion ? latestDevVersion?.name : latestVersion?.name fetchDevVersion ? latestDevVersion?.name : latestVersion?.name
)} )}
</Typography> </Typography>
{upgradeImportantMessageType === 1 && LL.UPGRADE_IMPORTANT_MESSAGES_1()}
{upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()}
<Typography sx={{ mt: 2 }}>
<Link
target="_blank"
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
style={{ color: 'lightblue' }}
>
{LL.ONLINE_HELP()}
</Link>
</Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
@@ -331,6 +347,7 @@ const InstallDialog = memo(
{LL.DOWNLOAD(0)} {LL.DOWNLOAD(0)}
</Link> </Link>
</Button> </Button>
{!downloadOnly && (
<Button <Button
startIcon={<WarningIcon color="warning" />} startIcon={<WarningIcon color="warning" />}
variant="outlined" variant="outlined"
@@ -339,6 +356,7 @@ const InstallDialog = memo(
> >
{LL.INSTALL()} {LL.INSTALL()}
</Button> </Button>
)}
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
@@ -367,7 +385,9 @@ const InstallPartitionDialog = memo(
{LL.INSTALL()} {LL.STORED_VERSIONS()} {LL.INSTALL()} {LL.STORED_VERSIONS()}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Typography mb={2}>{LL.INSTALL_VERSION(LL.INSTALL(), version)}</Typography> <Typography sx={{ mb: 2 }}>
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
</Typography>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
@@ -419,6 +439,7 @@ const Version = () => {
const [stableUpgradeAvailable, setStableUpgradeAvailable] = const [stableUpgradeAvailable, setStableUpgradeAvailable] =
useState<boolean>(false); useState<boolean>(false);
const [internetLive, setInternetLive] = useState<boolean>(false); const [internetLive, setInternetLive] = useState<boolean>(false);
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
const [firmwareSize, setFirmwareSize] = useState<number>(0); const [firmwareSize, setFirmwareSize] = useState<number>(0);
@@ -444,6 +465,9 @@ const Version = () => {
error error
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => { } = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
const systemData = event.data as VersionData; const systemData = event.data as VersionData;
if (systemData.arduino_version.startsWith('Tasmota')) {
setDownloadOnly(true);
}
setUsingDevVersion(systemData.emsesp_version.includes('dev')); setUsingDevVersion(systemData.emsesp_version.includes('dev'));
}); });
@@ -459,6 +483,26 @@ const Version = () => {
immediate: false immediate: false
}); });
const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
useState<number>(0);
const { send: checkUpgradeImportantMessages } = useRequest(
(version: string) =>
callAction({ action: 'upgradeImportantMessages', param: version }),
{
immediate: false
}
)
.onSuccess((event) => {
const upgradeImportantMessageType_n = (
event.data as { upgradeImportantMessageType: number }
).upgradeImportantMessageType;
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
})
.onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
// Memoized values // Memoized values
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]); const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
@@ -524,10 +568,16 @@ const Version = () => {
[] []
); );
const showFirmwareDialog = useCallback((useDevVersion: boolean) => { const showFirmwareDialog = useCallback(
(useDevVersion: boolean) => {
setFetchDevVersion(useDevVersion); setFetchDevVersion(useDevVersion);
void checkUpgradeImportantMessages(
useDevVersion ? latestDevVersion?.name : latestVersion?.name
);
setOpenInstallDialog(true); setOpenInstallDialog(true);
}, []); },
[latestDevVersion, latestVersion, fetchDevVersion]
);
const closeInstallDialog = useCallback(() => { const closeInstallDialog = useCallback(() => {
setOpenInstallDialog(false); setOpenInstallDialog(false);
@@ -629,8 +679,8 @@ const Version = () => {
return ( return (
<> <>
<Box p={2} border="1px solid grey" borderRadius={2}> <Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
<Typography mb={1} variant="h6" color="primary"> <Typography sx={{ mb: 1 }} variant="h6" color="primary">
{LL.THIS_VERSION()} {LL.THIS_VERSION()}
</Typography> </Typography>
@@ -695,7 +745,7 @@ const Version = () => {
{internetLive ? ( {internetLive ? (
<> <>
<Typography mt={4} mb={1} variant="h6" color="primary"> <Typography sx={{ mt: 4, mb: 1 }} variant="h6" color="primary">
{LL.AVAILABLE_VERSION()} {LL.AVAILABLE_VERSION()}
</Typography> </Typography>
@@ -717,7 +767,7 @@ const Version = () => {
</Grid> </Grid>
<Grid size={{ xs: 8, md: 10 }}> <Grid size={{ xs: 8, md: 10 }}>
{otherPartitions.map((partition) => ( {otherPartitions.map((partition) => (
<Typography key={partition.partition} mb={1}> <Typography key={partition.partition} sx={{ mb: 1 }}>
{partition.version} {partition.version}
<IconButton <IconButton
onClick={() => onClick={() =>
@@ -783,7 +833,7 @@ const Version = () => {
</Grid> </Grid>
</> </>
) : ( ) : (
<Typography mt={2} color="warning"> <Typography sx={{ mt: 2 }} color="warning">
<WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} /> <WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} />
{LL.INTERNET_CONNECTION_REQUIRED()} {LL.INTERNET_CONNECTION_REQUIRED()}
</Typography> </Typography>
@@ -807,6 +857,8 @@ const Version = () => {
fetchDevVersion={fetchDevVersion} fetchDevVersion={fetchDevVersion}
latestVersion={latestVersion} latestVersion={latestVersion}
latestDevVersion={latestDevVersion} latestDevVersion={latestDevVersion}
upgradeImportantMessageType={upgradeImportantMessageType}
downloadOnly={downloadOnly}
platform={platform} platform={platform}
LL={LL} LL={LL}
onClose={closeInstallDialog} onClose={closeInstallDialog}
@@ -823,7 +875,7 @@ const Version = () => {
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()} {LL.UPLOAD()}
</Typography> </Typography>
<SingleUpload text={LL.UPLOAD_DROP_TEXT()} doRestart={doRestart} /> <SingleUpload doRestart={doRestart} />
</> </>
)} )}
</Box> </Box>
@@ -842,6 +894,7 @@ const Version = () => {
locale, locale,
openInstallDialog, openInstallDialog,
fetchDevVersion, fetchDevVersion,
downloadOnly,
me.admin, me.admin,
showButtons, showButtons,
handleVersionInfoClose, handleVersionInfoClose,

View File

@@ -53,12 +53,16 @@ const MessageBox: FC<PropsWithChildren<MessageBoxProps>> = ({
return ( return (
<Box <Box
p={2}
display="flex"
alignItems="center"
borderRadius={1}
sx={{ backgroundColor, color: 'white', ...sx }}
{...rest} {...rest}
sx={{
display: 'flex',
alignItems: 'center',
borderRadius: 1,
backgroundColor,
color: 'white',
p: 2,
...sx
}}
> >
<Icon /> <Icon />
{(message || children) && ( {(message || children) && (

View File

@@ -29,7 +29,7 @@ const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
() => ( () => (
<> <>
<Toolbar disableGutters> <Toolbar disableGutters>
<Box display="flex" alignItems="center" px={2}> <Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} /> <LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
<Typography variant="h6">{PROJECT_NAME}</Typography> <Typography variant="h6">{PROJECT_NAME}</Typography>
</Box> </Box>

View File

@@ -51,9 +51,7 @@ const LayoutMenuComponent = () => {
sx={{ my: 0 }} sx={{ my: 0 }}
slotProps={{ slotProps={{
primary: { primary: {
fontWeight: '600', sx: { fontWeight: 600, mb: '2px', color: 'lightblue' }
mb: '2px',
color: 'lightblue'
} }
}} }}
/> />

View File

@@ -32,8 +32,16 @@ const FormLoaderComponent = ({ errorMessage, onRetry }: FormLoaderProps) => {
); );
} }
return ( return (
<Box m={2} py={2} display="flex" alignItems="center" flexDirection="column"> <Box
<Box py={2}> sx={{
m: 2,
py: 2,
display: 'flex',
alignItems: 'center',
flexDirection: 'column'
}}
>
<Box sx={{ p: 2 }}>
<CircularProgress size={100} /> <CircularProgress size={100} />
</Box> </Box>
</Box> </Box>

View File

@@ -15,12 +15,14 @@ const circularProgressStyles: SxProps<Theme> = (theme: Theme) => ({
const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => { const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => {
return ( return (
<Box <Box
display="flex" sx={{
alignItems="center" display: 'flex',
justifyContent="center" alignItems: 'center',
flexDirection="column" justifyContent: 'center',
padding={2} flexDirection: 'column',
height={height} padding: 2,
height
}}
> >
<CircularProgress sx={circularProgressStyles} size={100} /> <CircularProgress sx={circularProgressStyles} size={100} />
</Box> </Box>

View File

@@ -1,4 +1,4 @@
// Code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0 // drag/drop code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
import { import {
type ChangeEvent, type ChangeEvent,
type DragEvent, type DragEvent,
@@ -6,12 +6,28 @@ import {
useRef, useRef,
useState useState
} from 'react'; } from 'react';
import { Link } from 'react-router';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CloudUploadIcon from '@mui/icons-material/CloudUpload'; import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import UploadIcon from '@mui/icons-material/Upload'; import UploadIcon from '@mui/icons-material/Upload';
import { Box, Button, Typography, styled } from '@mui/material'; import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography,
styled
} from '@mui/material';
import { callAction } from 'api/app';
import { dialogStyle } from '@/CustomTheme';
import { useRequest } from 'alova/client';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const DocumentUploader = styled(Box)<{ active?: boolean }>(({ theme, active }) => ({ const DocumentUploader = styled(Box)<{ active?: boolean }>(({ theme, active }) => ({
@@ -58,6 +74,29 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
const [dragged, setDragged] = useState(false); const [dragged, setDragged] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
useState<number>(0);
const { send: checkUpgradeImportantMessages } = useRequest(
(version: string) =>
callAction({ action: 'upgradeImportantMessages', param: version }),
{
immediate: false
}
)
.onSuccess((event) => {
const upgradeImportantMessageType_n = (
event.data as { upgradeImportantMessageType: number }
).upgradeImportantMessageType;
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
if (upgradeImportantMessageType_n === 0) {
onFileSelected(file);
}
})
.onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
const checkFileExtension = (file: File) => { const checkFileExtension = (file: File) => {
const validExtensions = ['.json', '.bin', '.md5']; const validExtensions = ['.json', '.bin', '.md5'];
@@ -97,9 +136,8 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
const handleUploadClick = (event: MouseEvent<HTMLButtonElement>) => { const handleUploadClick = (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
if (file) { void checkUpgradeImportantMessages(file?.name || '');
onFileSelected(file); setShowUpgradeDialog(true);
}
}; };
const handleBrowseClick = () => { const handleBrowseClick = () => {
@@ -158,6 +196,55 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
{LL.UPLOAD()} {LL.UPLOAD()}
</Button> </Button>
</Box> </Box>
{showUpgradeDialog && upgradeImportantMessageType > 0 && (
<Dialog
sx={dialogStyle}
open={showUpgradeDialog}
onClose={() => setShowUpgradeDialog(false)}
>
<DialogTitle>
<WarningIcon
color="warning"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
&nbsp;&nbsp;
{LL.UPGRADE_IMPORTANT_MESSAGES()}
</DialogTitle>
<DialogContent dividers>
{upgradeImportantMessageType === 1 &&
LL.UPGRADE_IMPORTANT_MESSAGES_1()}
{upgradeImportantMessageType === 2 &&
LL.UPGRADE_IMPORTANT_MESSAGES_2()}
<Typography sx={{ mt: 2 }}>
<Link
target="_blank"
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
style={{ color: 'lightblue' }}
>
{LL.ONLINE_HELP()}
</Link>
</Typography>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setShowUpgradeDialog(false)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<UploadIcon />}
variant="outlined"
onClick={() => onFileSelected(file)}
color="primary"
>
{LL.UPLOAD()}
</Button>
</DialogActions>
</Dialog>
)}
</> </>
)} )}
</DocumentUploader> </DocumentUploader>

View File

@@ -13,11 +13,10 @@ import DragNdrop from './DragNdrop';
import { LinearProgressWithLabel } from './LinearProgressWithLabel'; import { LinearProgressWithLabel } from './LinearProgressWithLabel';
interface SingleUploadProps { interface SingleUploadProps {
text: string;
doRestart: () => void; doRestart: () => void;
} }
const SingleUpload = ({ text, doRestart }: SingleUploadProps) => { const SingleUpload = ({ doRestart }: SingleUploadProps) => {
const [md5, setMd5] = useState<string>(); const [md5, setMd5] = useState<string>();
const [file, setFile] = useState<File>(); const [file, setFile] = useState<File>();
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -58,7 +57,7 @@ const SingleUpload = ({ text, doRestart }: SingleUploadProps) => {
<> <>
{isUploading ? ( {isUploading ? (
<> <>
<Box width="100%" pl={2} pr={2}> <Box sx={{ width: '100%', pl: 2, pr: 2 }}>
<LinearProgressWithLabel <LinearProgressWithLabel
value={ value={
progress.total === 0 || progress.loaded === 0 progress.total === 0 || progress.loaded === 0
@@ -81,11 +80,11 @@ const SingleUpload = ({ text, doRestart }: SingleUploadProps) => {
</Button> </Button>
</> </>
) : ( ) : (
<DragNdrop text={text} onFileSelected={setFile} /> <DragNdrop text={LL.UPLOAD_DROP_TEXT()} onFileSelected={setFile} />
)} )}
{md5 && ( {md5 && (
<Box mt={2}> <Box sx={{ mt: 2 }}>
<Typography variant="body2">{'MD5: ' + md5}</Typography> <Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box> </Box>
)} )}

View File

@@ -359,7 +359,12 @@ const cz: Translation = {
NO_DATA: 'Žádná data', NO_DATA: 'Žádná data',
USER_PROFILE: 'Uživatelský profil', USER_PROFILE: 'Uživatelský profil',
STORED_VERSIONS: 'Uložené verze', STORED_VERSIONS: 'Uložené verze',
ONLINE_HELP: 'online nápověda' ONLINE_HELP: 'online nápověda',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že jste vytvořili zálohu své konfigurace a nastavení před pokračováním a nahrajte ji po instalaci nové verze.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.',
WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?'
}; };
export default cz; export default cz;

View File

@@ -359,7 +359,12 @@ const de: Translation = {
NO_DATA: 'Keine Daten', NO_DATA: 'Keine Daten',
USER_PROFILE: 'Benutzerprofil', USER_PROFILE: 'Benutzerprofil',
STORED_VERSIONS: 'Gespeicherte Versionen', STORED_VERSIONS: 'Gespeicherte Versionen',
ONLINE_HELP: 'Online-Hilfe' ONLINE_HELP: 'Online-Hilfe',
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
UPGRADE_IMPORTANT_MESSAGES_1: 'Diese Aktualisierung erfordert eine Werkseinstellung. Stellen Sie sicher, dass Sie eine Sicherung Ihrer Konfiguration und Einstellungen vor dem Fortfahren erstellt haben und diese nach der Installation der neuen Version hochladen.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.',
WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und -einstellungen erstellen. Alle Passwörter werden im Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?'
}; };
export default de; export default de;

View File

@@ -359,7 +359,12 @@ const en: Translation = {
NO_DATA: 'No data', NO_DATA: 'No data',
USER_PROFILE: 'User Profile', USER_PROFILE: 'User Profile',
STORED_VERSIONS: 'Stored Versions', STORED_VERSIONS: 'Stored Versions',
ONLINE_HELP: 'online help' ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you have made a backup of your configuration and settings before continuing, and upload this after the new version is installed.',
UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.',
WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?'
}; };
export default en; export default en;

View File

@@ -359,7 +359,12 @@ const fr: Translation = {
NO_DATA: 'Aucune donnée', NO_DATA: 'Aucune donnée',
USER_PROFILE: 'Profil utilisateur', USER_PROFILE: 'Profil utilisateur',
STORED_VERSIONS: 'Versions stockées', STORED_VERSIONS: 'Versions stockées',
ONLINE_HELP: 'aide en ligne' ONLINE_HELP: 'aide en ligne',
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous d\'avoir créé une sauvegarde de vos configurations et paramètres avant de continuer et de la charger après l\'installation de la nouvelle version.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.',
WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?'
}; };
export default fr; export default fr;

View File

@@ -359,7 +359,12 @@ const it: Translation = {
NO_DATA: 'Nessun dato', NO_DATA: 'Nessun dato',
USER_PROFILE: 'Profilo utente', USER_PROFILE: 'Profilo utente',
STORED_VERSIONS: 'Versioni memorizzate', STORED_VERSIONS: 'Versioni memorizzate',
ONLINE_HELP: 'aiuto online' ONLINE_HELP: 'aiuto online',
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di aver creato un backup delle tue configurazioni e impostazioni prima di continuare e di caricarlo dopo l\'installazione della nuova versione.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.',
WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?'
}; };
export default it; export default it;

View File

@@ -359,7 +359,12 @@ const nl: Translation = {
NO_DATA: 'Geen data', NO_DATA: 'Geen data',
USER_PROFILE: 'Gebruikersprofiel', USER_PROFILE: 'Gebruikersprofiel',
STORED_VERSIONS: 'Opgeslagen versies', STORED_VERSIONS: 'Opgeslagen versies',
ONLINE_HELP: 'online help' ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u een back-up van uw configuratie en instellingen hebt gemaakt voordat u doorgaat en upload deze na de installatie van de nieuwe versie.',
UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.',
WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?'
}; };
export default nl; export default nl;

View File

@@ -359,7 +359,12 @@ const no: Translation = {
NO_DATA: 'Ingen data', NO_DATA: 'Ingen data',
USER_PROFILE: 'Brukerprofil', USER_PROFILE: 'Brukerprofil',
STORED_VERSIONS: 'Lagret versjoner', STORED_VERSIONS: 'Lagret versjoner',
ONLINE_HELP: 'online hjelp' ONLINE_HELP: 'online hjelp',
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du har laget en sikkerhetskopi av din konfigurasjon og innstillinger før du fortsetter, og last denne opp etter at den nye versjonen er installert.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.',
WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?'
}; };
export default no; export default no;

View File

@@ -359,7 +359,12 @@ const pl: BaseTranslation = {
NO_DATA: 'Brak danych', NO_DATA: 'Brak danych',
USER_PROFILE: 'Profil użytkownika', USER_PROFILE: 'Profil użytkownika',
STORED_VERSIONS: 'Zapisane wersje', STORED_VERSIONS: 'Zapisane wersje',
ONLINE_HELP: 'pomoc online' ONLINE_HELP: 'pomoc online',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że masz utworzoną kopię swoich ustawień i konfiguracji przed kontynuowaniem i przesuń ją po zainstalowaniu nowej wersji.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.',
WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?'
}; };
export default pl; export default pl;

View File

@@ -4,7 +4,7 @@ const sk: Translation = {
LANGUAGE: 'Jazyk', LANGUAGE: 'Jazyk',
RETRY: 'Opakovať', RETRY: 'Opakovať',
LOADING: 'Načítanie', LOADING: 'Načítanie',
IS_REQUIRED: '{0} je požadovaných', IS_REQUIRED: '{0} je požadovaná',
SIGN_IN: 'Prihlásiť sa', SIGN_IN: 'Prihlásiť sa',
SIGN_OUT: 'Odhlásiť sa', SIGN_OUT: 'Odhlásiť sa',
USERNAME: 'Užívateľské meno', USERNAME: 'Užívateľské meno',
@@ -276,11 +276,11 @@ const sk: Translation = {
NETWORK_SUBNET: 'Maska podsiete', NETWORK_SUBNET: 'Maska podsiete',
NETWORK_DNS: 'DNS servery', NETWORK_DNS: 'DNS servery',
ADDRESS_OF: '{0} adresa', ADDRESS_OF: '{0} adresa',
ADMINISTRATOR: 'Administrator', ADMINISTRATOR: 'Administrátor',
GUEST: 'Hosť', GUEST: 'Hosť',
NEW: 'Nová', NEW: 'Novú',
NEW_NAME_OF: 'Nový názov {0}', NEW_NAME_OF: 'Nový názov {0}',
ENTITY: 'entita', ENTITY: 'entitu',
MIN: 'min', MIN: 'min',
MAX: 'max', MAX: 'max',
BLOCK_NAVIGATE_1: 'Máte neuložené zmeny', BLOCK_NAVIGATE_1: 'Máte neuložené zmeny',
@@ -359,7 +359,12 @@ const sk: Translation = {
NO_DATA: 'Žiadne dáta', NO_DATA: 'Žiadne dáta',
USER_PROFILE: 'Profil používateľa', USER_PROFILE: 'Profil používateľa',
STORED_VERSIONS: 'Uložené verzie', STORED_VERSIONS: 'Uložené verzie',
ONLINE_HELP: 'online pomoc' ONLINE_HELP: 'online pomoc',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že ste vytvorili zálohu svojich konfigurácií a nastavení pred pokračovaním a nahrajte ju po instalácii novej verzie.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.',
WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?'
}; };
export default sk; export default sk;

View File

@@ -359,7 +359,12 @@ const sv: Translation = {
NO_DATA: 'Ingen data', NO_DATA: 'Ingen data',
USER_PROFILE: 'Användarprofil', USER_PROFILE: 'Användarprofil',
STORED_VERSIONS: 'Lagrad versioner', STORED_VERSIONS: 'Lagrad versioner',
ONLINE_HELP: 'online hjälp' ONLINE_HELP: 'online hjälp',
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du har gjort en säkerhetskopia av din konfiguration och inställningar innan du fortsätter och ladda upp denna efter att den nya versionen är installerad.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.',
WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?'
}; };
export default sv; export default sv;

View File

@@ -359,7 +359,12 @@ const tr: Translation = {
NO_DATA: 'Hiçbir veri yok', NO_DATA: 'Hiçbir veri yok',
USER_PROFILE: 'Kullanıcı Profili', USER_PROFILE: 'Kullanıcı Profili',
STORED_VERSIONS: 'Kaydedilmiş Sürümler', STORED_VERSIONS: 'Kaydedilmiş Sürümler',
ONLINE_HELP: 'online yardım' ONLINE_HELP: 'online yardım',
UPGRADE_IMPORTANT_MESSAGES: 'Önemli Mesajları Güncelle',
UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Yeni bir büyük sürüme yükselteceksiniz. Değişiklikleri ChangeLogı okuduğunuzdan emin olun.',
WARNING_SYSTEM_BACKUP: 'Bu, sistem yapılandırmanızı ve ayarlarınızın bir yedeklemesi oluşturacaktır. Tüm şifreler yedekleme dosyasında okunabilir olacaktır. Paylaşırken dikkatli olun! Devam etmek istediğinize emin misiniz?'
}; };
export default tr; export default tr;

View File

@@ -13,7 +13,7 @@
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"formidable": "^3.5.4", "formidable": "^3.5.4",
"itty-router": "^5.0.23", "itty-router": "^5.0.23",
"prettier": "^3.8.1" "prettier": "^3.8.3"
}, },
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
} }

View File

@@ -13,7 +13,7 @@ importers:
version: 3.1.3 version: 3.1.3
'@trivago/prettier-plugin-sort-imports': '@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.2(prettier@3.8.1) version: 6.0.2(prettier@3.8.3)
formidable: formidable:
specifier: ^3.5.4 specifier: ^3.5.4
version: 3.5.4 version: 3.5.4
@@ -21,8 +21,8 @@ importers:
specifier: ^5.0.23 specifier: ^5.0.23
version: 5.0.23 version: 5.0.23
prettier: prettier:
specifier: ^3.8.1 specifier: ^3.8.3
version: 3.8.1 version: 3.8.3
packages: packages:
@@ -112,8 +112,8 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.3: brace-expansion@2.1.0:
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -145,8 +145,8 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
lodash-es@4.17.23: lodash-es@4.18.1:
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
minimatch@9.0.9: minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
@@ -167,8 +167,8 @@ packages:
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
prettier@3.8.1: prettier@3.8.3:
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@@ -246,17 +246,17 @@ snapshots:
dependencies: dependencies:
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1)': '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3)':
dependencies: dependencies:
'@babel/generator': 7.29.1 '@babel/generator': 7.29.1
'@babel/parser': 7.29.2 '@babel/parser': 7.29.2
'@babel/traverse': 7.29.0 '@babel/traverse': 7.29.0
'@babel/types': 7.29.0 '@babel/types': 7.29.0
javascript-natural-sort: 0.7.1 javascript-natural-sort: 0.7.1
lodash-es: 4.17.23 lodash-es: 4.18.1
minimatch: 9.0.9 minimatch: 9.0.9
parse-imports-exports: 0.2.4 parse-imports-exports: 0.2.4
prettier: 3.8.1 prettier: 3.8.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -264,7 +264,7 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
brace-expansion@2.0.3: brace-expansion@2.1.0:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -291,11 +291,11 @@ snapshots:
jsesc@3.1.0: {} jsesc@3.1.0: {}
lodash-es@4.17.23: {} lodash-es@4.18.1: {}
minimatch@9.0.9: minimatch@9.0.9:
dependencies: dependencies:
brace-expansion: 2.0.3 brace-expansion: 2.1.0
ms@2.1.3: {} ms@2.1.3: {}
@@ -311,6 +311,6 @@ snapshots:
picocolors@1.1.1: {} picocolors@1.1.1: {}
prettier@3.8.1: {} prettier@3.8.3: {}
wrappy@1.0.2: {} wrappy@1.0.2: {}

View File

@@ -388,13 +388,34 @@ function custom_support() {
'', '',
"For help and questions please <a target='_blank' href='https://emsesp.org'>contact</a> your installer." "For help and questions please <a target='_blank' href='https://emsesp.org'>contact</a> your installer."
], ],
img_url: 'https://emsesp.org/_media/images/designer.png' img_url: 'https://emsesp.org/media/images/designer.png'
// img_url: 'https://picsum.photos/200/300' // img_url: 'https://picsum.photos/200/300'
} }
}; };
} }
// called by Action endpoint // called by Action endpoint upgradeImportantMessages
function upgradeImportantMessages(version: string) {
// 0 is do nothing
// 1 means 3.9 and factory reset required
// 2 means a major version upgrade
let upgradeImportantMessageType_n = 0;
// see if its a filename with a .bin extension
if (version.endsWith('.bin')) {
upgradeImportantMessageType_n = 1; // 1 means 3.9 and factory reset required
} else if (version.endsWith('.md')) {
upgradeImportantMessageType_n = 0;
} else {
// this is a version string like "3.9.0"
upgradeImportantMessageType_n = 2;
}
console.log('upgradeImportantMessageType: ' + upgradeImportantMessageType_n);
return { upgradeImportantMessageType: upgradeImportantMessageType_n };
}
// called by Action endpoint checkUpgrade
function check_upgrade(version: string) { function check_upgrade(version: string) {
let data = {}; let data = {};
if (version) { if (version) {
@@ -5170,6 +5191,9 @@ router
// set partition // set partition
console.log('setting partition to', content.param); console.log('setting partition to', content.param);
return status(200); return status(200);
} else if (action === 'upgradeImportantMessages') {
// check upgrade important messages
return upgradeImportantMessages(content.param);
} }
} }
return status(404); // cmd not found return status(404); // cmd not found

View File

@@ -59,7 +59,7 @@ framework = arduino
board_build.partitions = partitions/esp32_partition_4M.csv board_build.partitions = partitions/esp32_partition_4M.csv
board_upload.flash_size = 4MB board_upload.flash_size = 4MB
board_build.app_partition_name = app0 board_build.app_partition_name = app0
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.03.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.7 based on IDF 5.5.3.260313 platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; 16MB Flash variants ; 16MB Flash variants
[espressif32_base_16M] [espressif32_base_16M]
@@ -67,7 +67,7 @@ framework = arduino
board_build.partitions = partitions/esp32_partition_16M.csv board_build.partitions = partitions/esp32_partition_16M.csv
board_upload.flash_size = 16MB board_upload.flash_size = 16MB
board_build.app_partition_name = app0 board_build.app_partition_name = app0
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.03.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.7 based on IDF 5.5.3.260313 platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; 32MB Flash variants ; 32MB Flash variants
[espressif32_base_32M] [espressif32_base_32M]
@@ -75,7 +75,7 @@ framework = arduino
board_build.partitions = partitions/esp32_partition_32M.csv board_build.partitions = partitions/esp32_partition_32M.csv
board_upload.flash_size = 32MB board_upload.flash_size = 32MB
board_build.app_partition_name = app0 board_build.app_partition_name = app0
platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.03.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.7 based on IDF 5.5.3.260313 platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
[env] [env]
build_flags = build_flags =

View File

@@ -184,7 +184,7 @@ void UploadFileService::handleError(AsyncWebServerRequest * request, int code) {
} }
void UploadFileService::handleEarlyDisconnect() { void UploadFileService::handleEarlyDisconnect() {
emsesp::EMSESP::logger().info("Upload completed"); emsesp::EMSESP::logger().info("Upload finished");
emsesp::EMSESP::system_.uart_init(); // re-enable UART emsesp::EMSESP::system_.uart_init(); // re-enable UART
_is_firmware = false; _is_firmware = false;

View File

@@ -38,15 +38,6 @@
#include "../test/test.h" #include "../test/test.h"
#endif #endif
#ifndef NO_TLS_SUPPORT
#define ENABLE_SMTP
#define USE_ESP_SSLCLIENT
#define READYCLIENT_SSL_CLIENT ESP_SSLClient
#define READYCLIENT_TYPE_1 // TYPE 1 when using ESP_SSLClient
#include <ESP_SSLClient.h>
#include <ReadyMail.h>
#endif
namespace emsesp { namespace emsesp {
// Languages supported. Note: the order is important // Languages supported. Note: the order is important
@@ -115,110 +106,6 @@ bool System::command_send(const char * value, const int8_t id) {
return EMSESP::txservice_.send_raw(value); // ignore id return EMSESP::txservice_.send_raw(value); // ignore id
} }
bool System::command_sendmail(const char * value, const int8_t id) {
bool enabled = false;
bool ssl, starttls;
uint16_t port;
String server, login, pass, sender, recp, subject;
EMSESP::webSettingsService.read([&](WebSettings & settings) {
enabled = settings.email_enabled;
ssl = settings.email_ssl;
starttls = settings.email_starttls;
server = settings.email_server;
port = settings.email_port;
login = settings.email_login;
pass = settings.email_pass;
sender = settings.email_sender;
recp = settings.email_recp;
subject = settings.email_subject;
});
if (!enabled) {
return false;
}
LOG_DEBUG("Command sendmail port %d%s called with '%s'", port, ssl ? " (SSL)" : starttls ? " (STARTTLS)" : "", value);
// LOG_DEBUG("Command sendmail port %d called with '%s'", port, value);
bool success = false;
#ifndef NO_TLS_SUPPORT
WiFiClient * basic_client;
ESP_SSLClient * ssl_client;
ReadyClient * r_client; // rClient(ssl_client);
SMTPClient * smtp; // smtp(rClient);
basic_client = new WiFiClient;
ssl_client = new ESP_SSLClient;
r_client = new ReadyClient(*ssl_client);
smtp = new SMTPClient(*r_client);
ssl_client->setClient(basic_client);
ssl_client->setInsecure();
ssl_client->setBufferSizes(1024, 1024);
r_client->addPort(port, starttls ? readymail_protocol_tls : ssl ? readymail_protocol_ssl : readymail_protocol_plain_text);
// smtp->connect(server, port, sendmailCallback);
smtp->connect(server, port);
if (!smtp->isConnected()) {
LOG_ERROR("Sendmail connection error");
delete smtp;
delete r_client;
delete ssl_client;
delete basic_client;
return false;
}
// LOG_INFO("authenticate %s:%s", login.c_str(), pass.c_str());
smtp->authenticate(login, pass, readymail_auth_password);
if (!smtp->isAuthenticated()) {
LOG_ERROR("Sendmail authenticate error");
delete smtp;
delete r_client;
delete ssl_client;
delete basic_client;
return false;
}
JsonDocument doc;
String body = value;
if (body.length()) {
auto error = deserializeJson(doc, (const char *)value);
if (!error && doc.as<JsonObject>().size() >= 0) {
subject = doc["subject"] | subject;
recp = doc["to"] | recp;
sender = doc["from"] | sender;
body = doc["body"] | body;
}
}
SMTPMessage & msg = smtp->getMessage();
msg.headers.add(rfc822_subject, subject);
msg.headers.add(rfc822_from, sender);
msg.headers.add(rfc822_to, recp);
// Use addCustom to add custom header e.g. Importance and Priority.
// msg.headers.addCustom("Importance", PRIORITY);
// msg.headers.addCustom("X-MSMail-Priority", PRIORITY);
// msg.headers.addCustom("X-Priority", PRIORITY_NUM);
msg.text.body(body);
// bodyText.replace("\r\n", "<br>\r\n");
// msg.html.body("<html><body><div style=\"color:#cc0066;\">" + bodyText + "</div></body></html>");
// msg.html.transferEncoding("base64");
// With embedFile function, the html message will send as attachment.
// if (EMBED_MESSAGE)
// msg.html.embedFile(true, "msg.html", embed_message_type_attachment);
msg.timestamp = time(nullptr);
success = smtp->send(msg);
delete smtp;
delete r_client;
delete ssl_client;
delete basic_client;
#endif
return success;
}
// return string of languages and count // return string of languages and count
std::string System::languages_string() { std::string System::languages_string() {
std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages ("; std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages (";
@@ -309,7 +196,7 @@ bool System::command_publish(const char * value, const int8_t id) {
// syslog level // syslog level
// commenting this out - don't see the point on having an API service to change the syslog level // commenting this out - don't see the point on having an API service to change the syslog level
/* /*
bool System::command_syslog_level(const char * value, const int8_t id) { bool System::command_syslog_level(const char * value, const int8_t id) {
uint8_t s = 0xff; uint8_t s = 0xff;
if (Helpers::value2enum(value, s, FL_(list_syslog_level))) { if (Helpers::value2enum(value, s, FL_(list_syslog_level))) {
bool changed = false; bool changed = false;
@@ -327,8 +214,8 @@ bool System::command_syslog_level(const char * value, const int8_t id) {
return true; return true;
} }
return false; return false;
} }
*/ */
// send message - to system log and MQTT // send message - to system log and MQTT
bool System::command_message(const char * value, const int8_t id, JsonObject output) { bool System::command_message(const char * value, const int8_t id, JsonObject output) {
@@ -576,6 +463,7 @@ void System::system_restart(const char * partitionname) {
Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1 Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1
EMSuart::stop(); // stop UART so there is no interference EMSuart::stop(); // stop UART so there is no interference
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
delay(1000); // wait 1 second delay(1000); // wait 1 second
ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called
@@ -677,6 +565,7 @@ void System::store_settings(WebSettings & settings) {
locale_ = settings.locale; locale_ = settings.locale;
developer_mode_ = settings.developer_mode; developer_mode_ = settings.developer_mode;
// start services // start services
if (settings.modbus_enabled) { if (settings.modbus_enabled) {
if (EMSESP::modbus_ == nullptr) { if (EMSESP::modbus_ == nullptr) {
@@ -718,11 +607,20 @@ void System::start() {
appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_; appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_;
refreshHeapMem(); // refresh free heap and max alloc heap refreshHeapMem(); // refresh free heap and max alloc heap
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR < 5
temp_sensor_config_t temp_sensor = TSENS_CONFIG_DEFAULT();
temp_sensor_get_config(&temp_sensor);
temp_sensor.dac_offset = TSENS_DAC_DEFAULT; // DEFAULT: range:-10℃ ~ 80℃, error < 1℃.
temp_sensor_set_config(temp_sensor);
temp_sensor_start();
temp_sensor_read_celsius(&temperature_);
#else
temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
temperature_sensor_install(&temp_sensor_config, &temperature_handle_); temperature_sensor_install(&temp_sensor_config, &temperature_handle_);
temperature_sensor_enable(temperature_handle_); temperature_sensor_enable(temperature_handle_);
temperature_sensor_get_celsius(temperature_handle_, &temperature_); temperature_sensor_get_celsius(temperature_handle_, &temperature_);
#endif #endif
#endif
#endif #endif
EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) {
@@ -943,9 +841,16 @@ void System::send_info_mqtt() {
doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP()); doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP()); doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
#if ESP_IDF_VERSION_MAJOR < 5
if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") {
doc["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
}
#else
if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") {
doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6()); doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6());
} }
#endif
} }
#endif #endif
Mqtt::queue_publish_retain(F_(info), doc.as<JsonObject>()); // topic called "info" and it's Retained Mqtt::queue_publish_retain(F_(info), doc.as<JsonObject>()); // topic called "info" and it's Retained
@@ -1054,8 +959,13 @@ void System::network_init() {
delay(500); delay(500);
digitalWrite(eth_power_, HIGH); digitalWrite(eth_power_, HIGH);
} }
#if ESP_IDF_VERSION_MAJOR < 5
eth_present_ = ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode);
#else
eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode); eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode);
#endif #endif
#endif
} }
// check health of system, done every 5 seconds // check health of system, done every 5 seconds
@@ -1066,9 +976,13 @@ void System::system_check() {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR < 5
temp_sensor_read_celsius(&temperature_);
#else
temperature_sensor_get_celsius(temperature_handle_, &temperature_); temperature_sensor_get_celsius(temperature_handle_, &temperature_);
#endif #endif
#endif #endif
#endif
#ifdef EMSESP_PINGTEST #ifdef EMSESP_PINGTEST
static uint64_t ping_count = 0; static uint64_t ping_count = 0;
@@ -1120,7 +1034,6 @@ void System::commands_init() {
Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(sendmail), System::command_sendmail, FL_(sendmail_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY);
@@ -1362,9 +1275,16 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str()); shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str());
shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str()); shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str());
shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str()); shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str());
#if ESP_IDF_VERSION_MAJOR < 5
if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.localIPv6()).c_str());
}
#else
if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str()); shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str());
} }
#endif
break; break;
case WL_CONNECT_FAILED: case WL_CONNECT_FAILED:
@@ -1395,9 +1315,15 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str()); shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str());
shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str()); shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str());
shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str()); shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str());
#if ESP_IDF_VERSION_MAJOR < 5
if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.localIPv6()).c_str());
}
#else
if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") { if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") {
shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str()); shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str());
} }
#endif
} }
shell.println(); shell.println();
@@ -1419,6 +1345,7 @@ void System::show_system(uuid::console::Shell & shell) {
} }
shell.println(); shell.println();
#endif #endif
} }
@@ -1722,6 +1649,7 @@ bool System::check_upgrade() {
} }
// map each config filename to its human-readable section key // map each config filename to its human-readable section key
#ifndef EMSESP_STANDALONE
static const std::pair<const char *, const char *> SECTION_MAP[] = { static const std::pair<const char *, const char *> SECTION_MAP[] = {
{NETWORK_SETTINGS_FILE, "Network"}, {NETWORK_SETTINGS_FILE, "Network"},
{AP_SETTINGS_FILE, "AP"}, {AP_SETTINGS_FILE, "AP"},
@@ -1734,6 +1662,7 @@ static const std::pair<const char *, const char *> SECTION_MAP[] = {
{EMSESP_CUSTOMENTITY_FILE, "Entities"}, {EMSESP_CUSTOMENTITY_FILE, "Entities"},
{EMSESP_MODULES_FILE, "Modules"}, {EMSESP_MODULES_FILE, "Modules"},
}; };
#endif
// convert a single config file into a section of the output json object // convert a single config file into a section of the output json object
void System::exportSettings(const std::string & type, const char * filename, JsonObject output) { void System::exportSettings(const std::string & type, const char * filename, JsonObject output) {
@@ -1834,29 +1763,38 @@ void System::exportSystemBackup(JsonObject output) {
const char * nvs_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, "nvs1") ? "nvs1" : "nvs"; // nvs1 is on 16MBs const char * nvs_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, "nvs1") ? "nvs1" : "nvs"; // nvs1 is on 16MBs
nvs_iterator_t it = nullptr; nvs_iterator_t it = nullptr;
#if ESP_IDF_VERSION_MAJOR < 5
it = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY);
if (it == nullptr) {
#else
esp_err_t err = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY, &it); esp_err_t err = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY, &it);
if (err != ESP_OK) { if (err != ESP_OK) {
#endif
LOG_ERROR("Failed to find NVS entry for %s", nvs_part); LOG_ERROR("Failed to find NVS entry for %s", nvs_part);
return; return;
} }
JsonArray entries = node["nvs"].to<JsonArray>(); JsonArray entries = node["nvs"].to<JsonArray>();
#if ESP_IDF_VERSION_MAJOR < 5
while (it != nullptr) {
nvs_entry_info_t info;
nvs_entry_info(it, &info);
#else
while (err == ESP_OK) { while (err == ESP_OK) {
nvs_entry_info_t info; nvs_entry_info_t info;
nvs_entry_info(it, &info); nvs_entry_info(it, &info);
#endif
JsonObject entry = entries.add<JsonObject>(); JsonObject entry = entries.add<JsonObject>();
entry["type"] = info.type; // e.g. NVS_TYPE_U32 or NVS_TYPE_STR etc entry["type"] = info.type;
entry["key"] = info.key; entry["key"] = info.key;
LOG_DEBUG("Exporting NVS value: %s = %d", info.key, info.type); LOG_DEBUG("Exporting NVS value: %s = %d", info.key, info.type);
// serialize based on the type. We use putString, putChar, putUChar, putDouble, putBool, putULong only
switch (info.type) { switch (info.type) {
case NVS_TYPE_I8: case NVS_TYPE_I8:
entry["value"] = EMSESP::nvs_.getChar(info.key); entry["value"] = EMSESP::nvs_.getChar(info.key);
break; break;
case NVS_TYPE_U8: case NVS_TYPE_U8:
// also used for bool
entry["value"] = EMSESP::nvs_.getUChar(info.key); entry["value"] = EMSESP::nvs_.getUChar(info.key);
break; break;
case NVS_TYPE_I32: case NVS_TYPE_I32:
@@ -1872,19 +1810,22 @@ void System::exportSystemBackup(JsonObject output) {
entry["value"] = EMSESP::nvs_.getULong64(info.key); entry["value"] = EMSESP::nvs_.getULong64(info.key);
break; break;
case NVS_TYPE_BLOB: case NVS_TYPE_BLOB:
// used for double (e.g. sensor values, nrgheat, nrgww), and stored as bytes in NVS entry["value"] = EMSESP::nvs_.getDouble(info.key); // bytes used for double values
entry["value"] = EMSESP::nvs_.getDouble(info.key);
break; break;
case NVS_TYPE_STR: case NVS_TYPE_STR:
case NVS_TYPE_ANY: case NVS_TYPE_ANY:
default: default:
// any other value we store as a string
entry["value"] = EMSESP::nvs_.getString(info.key); entry["value"] = EMSESP::nvs_.getString(info.key);
break; break;
} }
#if ESP_IDF_VERSION_MAJOR < 5
it = nvs_entry_next(it);
}
#else
err = nvs_entry_next(&it); err = nvs_entry_next(&it);
} }
#endif
if (it != nullptr) { if (it != nullptr) {
nvs_release_iterator(it); nvs_release_iterator(it);
@@ -2686,12 +2627,12 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
node["maxWebLogBuffer"] = settings.weblog_buffer; node["maxWebLogBuffer"] = settings.weblog_buffer;
/* /*
#if defined(EMSESP_UNITY) #if defined(EMSESP_UNITY)
node["webLogBuffer"] = 0; node["webLogBuffer"] = 0;
#else #else
node["webLogBuffer"] = EMSESP::webLogService.num_log_messages(); node["webLogBuffer"] = EMSESP::webLogService.num_log_messages();
#endif #endif
*/ */
node["modbusEnabled"] = settings.modbus_enabled; node["modbusEnabled"] = settings.modbus_enabled;
node["forceHeatingOff"] = settings.boiler_heatingoff; node["forceHeatingOff"] = settings.boiler_heatingoff;
node["developerMode"] = settings.developer_mode; node["developerMode"] = settings.developer_mode;
@@ -3382,10 +3323,6 @@ void System::set_valid_system_gpios() {
} else { } else {
valid_system_gpios_ = string_range_to_vector("0-39", "6-11, 20, 24, 28-31"); valid_system_gpios_ = string_range_to_vector("0-39", "6-11, 20, 24, 28-31");
} }
#elif CONFIG_IDF_TARGET_ESP32C6
// https://docs.espressif.com/projects/esp-idf/en/v5.5.3/esp32c6/api-reference/peripherals/gpio.html
// 24-30 used for flash, 12-13 USB, 16-17 uart0
valid_system_gpios_ = string_range_to_vector("0-30", "12-13, 16-17, 24-30");
#elif defined(EMSESP_STANDALONE) #elif defined(EMSESP_STANDALONE)
valid_system_gpios_ = string_range_to_vector("0-39"); valid_system_gpios_ = string_range_to_vector("0-39");
#endif #endif

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.8.2-dev.C14" #define EMSESP_APP_VERSION "3.8.2-dev.C13"

View File

@@ -83,20 +83,6 @@ void WebSettings::read(WebSettings & settings, JsonObject root) {
root["modbus_max_clients"] = settings.modbus_max_clients; root["modbus_max_clients"] = settings.modbus_max_clients;
root["modbus_timeout"] = settings.modbus_timeout; root["modbus_timeout"] = settings.modbus_timeout;
root["developer_mode"] = settings.developer_mode; root["developer_mode"] = settings.developer_mode;
#ifndef NO_TLS_SUPPORT
root["email_enabled"] = settings.email_enabled;
#else
root["email_enabled"] = false;
#endif
root["email_ssl"] = settings.email_ssl;
root["email_starttls"] = settings.email_starttls;
root["email_server"] = settings.email_server;
root["email_port"] = settings.email_port;
root["email_login"] = settings.email_login;
root["email_pass"] = settings.email_pass;
root["email_sender"] = settings.email_sender;
root["email_recp"] = settings.email_recp;
root["email_subject"] = settings.email_subject;
} }
// call on initialization and also when settings are updated/saved via web or console // call on initialization and also when settings are updated/saved via web or console
@@ -257,9 +243,13 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
// Modbus settings // Modbus settings
settings.modbus_enabled = root["modbus_enabled"] | EMSESP_DEFAULT_MODBUS_ENABLED; settings.modbus_enabled = root["modbus_enabled"] | EMSESP_DEFAULT_MODBUS_ENABLED;
check_flag(original_settings.modbus_enabled, settings.modbus_enabled, ChangeFlags::RESTART);
settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT; settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT;
check_flag(original_settings.modbus_port, settings.modbus_port, ChangeFlags::RESTART);
settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS; settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS;
check_flag(original_settings.modbus_max_clients, settings.modbus_max_clients, ChangeFlags::RESTART);
settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT; settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT;
check_flag(original_settings.modbus_timeout, settings.modbus_timeout, ChangeFlags::RESTART);
// //
// these may need mqtt restart to rebuild HA discovery topics // these may need mqtt restart to rebuild HA discovery topics
@@ -310,20 +300,6 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
settings.weblog_level = root["weblog_level"] | EMSESP_DEFAULT_WEBLOG_LEVEL; settings.weblog_level = root["weblog_level"] | EMSESP_DEFAULT_WEBLOG_LEVEL;
settings.weblog_compact = root["weblog_compact"] | EMSESP_DEFAULT_WEBLOG_COMPACT; settings.weblog_compact = root["weblog_compact"] | EMSESP_DEFAULT_WEBLOG_COMPACT;
settings.email_enabled = root["email_enabled"] | FACTORY_EMAIL_ENABLE;
settings.email_ssl = root["email_ssl"] | FACTORY_EMAIL_SSL;
settings.email_starttls = root["email_starttls"] | FACTORY_EMAIL_STARTTLS;
settings.email_server = root["email_server"] | FACTORY_EMAIL_SERVER;
settings.email_port = root["email_port"] | FACTORY_EMAIL_PORT;
settings.email_login = root["email_login"] | FACTORY_EMAIL_LOGIN;
settings.email_pass = root["email_pass"] | FACTORY_EMAIL_PASSWORD;
settings.email_sender = root["email_sender"] | FACTORY_EMAIL_FROM;
settings.email_recp = root["email_recp"] | FACTORY_EMAIL_TO;
settings.email_subject = root["email_subject"] | FACTORY_EMAIL_SUBJECT;
if (settings.email_ssl && settings.email_starttls) {
settings.email_ssl = false;
}
// if no psram limit weblog buffer to 25 messages // if no psram limit weblog buffer to 25 messages
if (EMSESP::system_.PSram() > 0) { if (EMSESP::system_.PSram() > 0) {
settings.weblog_buffer = root["weblog_buffer"] | EMSESP_DEFAULT_WEBLOG_BUFFER; settings.weblog_buffer = root["weblog_buffer"] | EMSESP_DEFAULT_WEBLOG_BUFFER;
@@ -482,14 +458,23 @@ void WebSettings::set_board_profile(WebSettings & settings) {
#if CONFIG_IDF_TARGET_ESP32 #if CONFIG_IDF_TARGET_ESP32
// check for no PSRAM, could be a E32 or S32? // check for no PSRAM, could be a E32 or S32?
if (!ESP.getPsramSize()) { if (!ESP.getPsramSize()) {
#if ESP_ARDUINO_VERSION_MAJOR < 3
if (ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN)) {
#else
if (ETH.begin(ETH_PHY_LAN8720, 1, 23, 18, 16, ETH_CLOCK_GPIO0_IN)) { if (ETH.begin(ETH_PHY_LAN8720, 1, 23, 18, 16, ETH_CLOCK_GPIO0_IN)) {
#endif
settings.board_profile = "E32"; // Ethernet without PSRAM settings.board_profile = "E32"; // Ethernet without PSRAM
} else { } else {
settings.board_profile = "S32"; // ESP32 standard WiFi without PSRAM settings.board_profile = "S32"; // ESP32 standard WiFi without PSRAM
} }
} else { } else {
// check for boards with PSRAM, could be a E32V2 otherwise default back to the S32 // check for boards with PSRAM, could be a E32V2 otherwise default back to the S32
#if ESP_ARDUINO_VERSION_MAJOR < 3
if (ETH.begin(0, 15, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_OUT)) {
#else
if (ETH.begin(ETH_PHY_LAN8720, 0, 23, 18, 15, ETH_CLOCK_GPIO0_OUT)) { if (ETH.begin(ETH_PHY_LAN8720, 0, 23, 18, 15, ETH_CLOCK_GPIO0_OUT)) {
#endif
if (analogReadMilliVolts(39) > 700) { // core voltage > 2.6V if (analogReadMilliVolts(39) > 700) { // core voltage > 2.6V
settings.board_profile = "E32V2_2"; // Ethernet, PSRAM, internal sensors settings.board_profile = "E32V2_2"; // Ethernet, PSRAM, internal sensors
} else { } else {
@@ -517,9 +502,9 @@ void WebSettings::set_board_profile(WebSettings & settings) {
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
uint32_t psram_size = ESP.getPsramSize() / 1024; // in KB uint32_t psram_size = ESP.getPsramSize() / 1024; // in KB
if (psram_size > 0) { if (psram_size > 0) {
EMSESP::logger().info("Loaded board profile %s, PSRAM: %lu KB", settings.board_profile.c_str(), psram_size); EMSESP::logger().info("Loaded board profile %s (PSRAM: %lu KB)", settings.board_profile.c_str(), psram_size);
} else { } else {
EMSESP::logger().info("Loaded board profile %s, PSRAM: not available", settings.board_profile.c_str()); EMSESP::logger().info("Loaded board profile %s (PSRAM: not available)", settings.board_profile.c_str());
} }
#endif #endif

View File

@@ -216,6 +216,9 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
} else if (action == "resetMQTT" && is_admin) { } else if (action == "resetMQTT" && is_admin) {
EMSESP::mqtt_.reset_mqtt(); EMSESP::mqtt_.reset_mqtt();
ok = true; ok = true;
} else if (action == "upgradeImportantMessages") {
root["upgradeImportantMessageType"] = upgradeImportantMessages(param);
ok = true;
} }
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY) #if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
@@ -237,13 +240,75 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
request->send(response); request->send(response);
} }
// action = upgradeImportantMessages
// returns the type of upgrade important message to display in the UI
// 0 = no message (if just a minor version upgrade)
// 1 = going from <= 3.8 to 3.9 (has new partition layout)
// 2 = major version upgrade
// version can be like 3.8.2 or a filename like EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin
uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
if (version.empty()) {
return 0;
}
// it's a filename with a .bin or .md extension, try and extract the version from it
// e.g. EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin -> major=3 minor=8 patch=2
version::Semver200_version latest_version;
if ((version.find(".bin") != std::string::npos) || (version.find(".md") != std::string::npos)) {
std::string filename = version;
auto pos = filename.find("EMS-ESP-");
if (pos == std::string::npos) {
EMSESP::logger().err("Invalid version string: %s", version.c_str());
return 0;
}
pos += 8; // skip past "EMS-ESP-"
auto underscore1 = filename.find('_', pos);
auto underscore2 = filename.find('_', underscore1 + 1);
auto dash = filename.find('-', underscore2 + 1);
if (underscore1 == std::string::npos || underscore2 == std::string::npos || dash == std::string::npos) {
EMSESP::logger().err("Invalid version string: %s", version.c_str());
return 0;
}
std::string major_version = filename.substr(pos, underscore1 - pos);
std::string minor_version = filename.substr(underscore1 + 1, underscore2 - underscore1 - 1);
std::string patch_version = filename.substr(underscore2 + 1, dash - underscore2 - 1);
latest_version = version::Semver200_version(major_version + "." + minor_version + "." + patch_version);
} else {
// if it's .json file exit
if (version.find(".json") != std::string::npos) {
return 0;
} else {
// treat it like a version string like "3.9.0"
latest_version = version::Semver200_version(version);
}
}
version::Semver200_version current_version(current_version_s); // get current version
if (latest_version > current_version && current_version.minor() < latest_version.minor()) {
return 0; // if it's just a minor version upgrade return 0
}
if ((current_version.major() <= 3 && current_version.minor() <= 8) && (latest_version.major() == 3 && latest_version.minor() == 9)) {
return 1; // if moving from below 3.8.x to 3.9.x return 1
}
if (latest_version > current_version && current_version.major() < latest_version.major()) {
return 2; // if it's a major version upgrade return 2
}
return 0; // if it's not a valid version upgrade return 0
}
// action = checkUpgrade // action = checkUpgrade
// versions holds the latest development version and stable version in one string, comma separated // versions holds the latest development version and stable version in one string, comma separated
bool WebStatusService::checkUpgrade(JsonObject root, std::string & versions) { bool WebStatusService::checkUpgrade(JsonObject root, std::string & version) {
if (!versions.empty()) { if (!version.empty()) {
version::Semver200_version current_version(current_version_s); version::Semver200_version current_version(current_version_s);
version::Semver200_version latest_dev_version(versions.substr(0, versions.find(','))); version::Semver200_version latest_dev_version(version.substr(0, version.find(',')));
version::Semver200_version latest_stable_version(versions.substr(versions.find(',') + 1)); version::Semver200_version latest_stable_version(version.substr(version.find(',') + 1));
bool dev_upgradeable = latest_dev_version > current_version; bool dev_upgradeable = latest_dev_version > current_version;
bool stable_upgradeable = latest_stable_version > current_version; bool stable_upgradeable = latest_stable_version > current_version;
@@ -342,7 +407,7 @@ bool WebStatusService::getCustomSupport(JsonObject root) {
#if defined(EMSESP_STANDALONE) #if defined(EMSESP_STANDALONE)
// dummy test data for "test api3" // dummy test data for "test api3"
deserializeJson(doc, "{\"type\":\"customSupport\",\"Support\":{\"html\":[\"html code\",\"here\"], \"img_url\": \"https://emsesp.org/_media/images/designer.png\"}"); deserializeJson(doc, "{\"type\":\"customSupport\",\"Support\":{\"html\":[\"html code\",\"here\"], \"img_url\": \"https://emsesp.org/media/images/designer.png\"}");
#else #else
// check if we have custom support file uploaded // check if we have custom support file uploaded
File file = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "r"); File file = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "r");

View File

@@ -36,6 +36,7 @@ class WebStatusService {
bool uploadURL(const char * url); bool uploadURL(const char * url);
bool setSystemStatus(const char * status); bool setSystemStatus(const char * status);
void allvalues(JsonObject output); void allvalues(JsonObject output);
uint8_t upgradeImportantMessages(std::string & version);
std::string current_version_s = EMSESP_APP_VERSION; std::string current_version_s = EMSESP_APP_VERSION;
}; };

View File

@@ -13,6 +13,6 @@
"", "",
"For help and questions please <a target='_blank' href='https://emsesp.org'>contact</a> your installer." "For help and questions please <a target='_blank' href='https://emsesp.org'>contact</a> your installer."
], ],
"img_url": "https://emsesp.org/_media/images/designer.png" "img_url": "https://emsesp.org/media/images/designer.png"
} }
} }

View File

@@ -0,0 +1,103 @@
{
"Boiler Nefit Trendline HRC30 (DeviceID:0x08, ProductID:123, Version:06.01)": {
"force heating off": "off",
"heating active": "off",
"tapwater active": "off",
"selected flow temperature": 5,
"heating pump modulation": 0,
"current flow temperature": 41.4,
"return temperature": 37.7,
"system pressure": 1.3,
"actual boiler temperature": 44.2,
"gas": "off",
"gas stage 2": "off",
"flame current": 0,
"fan": "off",
"ignition": "off",
"oil preheating": "off",
"burner min power": 0,
"burner max power": 50,
"burner min period": 10,
"hysteresis on temperature": -6,
"hysteresis off temperature": 6,
"heating activated": "on",
"heating temperature": 70,
"heating pump": "off",
"boiler pump max power": 70,
"boiler pump min power": 50,
"boiler pump mode": "proportional",
"pump delay": 2,
"burner selected max power": 0,
"burner current power": 0,
"burner starts": 394602,
"total burner operating time": "480 days 4 hours 23 minutes",
"burner stage 2 operating time": "0 days 0 hours 0 minutes",
"total heat operating time": "395 days 2 hours 14 minutes",
"burner starts heating": 46245,
"total UBA operating time": "3932 days 23 hours 58 minutes",
"last error code": "2E(207) 100.75.2000 65:00 (0 min)",
"service code": "0H",
"service code number": 203,
"maintenance message": "H00",
"maintenance scheduled": "manual",
"time to next maintenance": 6000,
"next maintenance date": "01.01.2012",
"dhw turn on/off": "on",
"dhw set temperature": 62,
"dhw selected temperature": 60,
"dhw type": "flow",
"dhw comfort": "hot",
"dhw flow temperature offset": 40,
"dhw max power": 100,
"dhw circulation pump available": "off",
"dhw charging type": "3-way valve",
"dhw hysteresis on temperature": -5,
"dhw hysteresis off temperature": 0,
"dhw disinfection temperature": 70,
"dhw circulation pump mode": "off",
"dhw circulation active": "off",
"dhw current intern temperature": 33.5,
"dhw current tap water flow": 0,
"dhw storage intern temperature": 33.5,
"dhw activated": "on",
"dhw one time charging": "off",
"dhw disinfecting": "off",
"dhw charging": "off",
"dhw recharging": "off",
"dhw temperature ok": "on",
"dhw active": "off",
"dhw 3-way valve active": "on",
"dhw set pump power": 0,
"dhw starts": 348357,
"dhw active time": "85 days 2 hours 9 minutes",
"nominal Power": 30,
"total energy": 3088.69,
"energy heating": 2532.94,
"dhw energy": 555.75
},
"Thermostat RC20 (DeviceID:0x17, ProductID:77, Version:03.03)": {
"date/time": "10.12.2023 13:49",
"hc1 how hot lounge should be": 19,
"hc1 current room temp": 19.5,
"hc1 mqtt discovery current room temperature": "roomTemp",
"hc1 mode": "auto",
"hc1 manual temperature": 21.5,
"hc1 temperature when mode is off": 7,
"hc1 day temperature T2": 20,
"hc1 day temperature T3": 20,
"hc1 day temperature T4": 20,
"hc1 night temperature T1": 15,
"hc1 program switchtime": "00 mo 00:00 T1"
},
"Controller Module BC10 (DeviceID:0x09, ProductID:190, Version:01.03)": {},
"Custom Entities": {
"boiler_flowtemp": 5,
"nominalpower": 30,
"minmodulation": 23,
"maxmodulation": 115
},
"Analog Sensors": {},
"Temperature Sensors": {
"zolder": 18.3
}
}

View File

@@ -0,0 +1,37 @@
{
"type": "customizations",
"Customizations": {
"ts": [],
"as": [],
"masked_entities": [
{
"product_id": 77,
"device_id": 23,
"custom_name": "",
"custom_brand": "",
"entity_ids": [
"08datetime",
"08hc1/seltemp|<30",
"08hc1/currtemp",
"08hc1/mode"
]
},
{
"product_id": 123,
"device_id": 8,
"custom_name": "",
"custom_brand": "",
"entity_ids": [
"08heatingactive",
"08burngas",
"08fanwork",
"08ignwork",
"08burnmaxpower|>23<121",
"08burnminperiod|<120",
"08lastcode",
"08servicecode"
]
}
]
}
}

View File

@@ -0,0 +1,6 @@
{
"type": "entities",
"Entities": {
"entities": []
}
}

View File

@@ -0,0 +1,6 @@
{
"type": "schedule",
"Schedule": {
"schedule": []
}
}

View File

@@ -0,0 +1,145 @@
{
"type": "settings",
"System": {
"version": "3.8.2"
},
"Network": {
"ssid": "",
"bssid": "",
"password": "",
"hostname": "ems-esp",
"static_ip_config": false,
"bandwidth20": false,
"nosleep": true,
"enableMDNS": true,
"enableCORS": false,
"CORSOrigin": "*",
"tx_power": 0
},
"AP": {
"provision_mode": 2,
"ssid": "ems-esp",
"password": "ems-esp-neo",
"channel": 1,
"ssid_hidden": false,
"max_clients": 4,
"local_ip": "192.168.4.1",
"gateway_ip": "192.168.4.1",
"subnet_mask": "255.255.255.0"
},
"MQTT": {
"enableTLS": false,
"rootCA": "",
"enabled": true,
"host": "192.168.X.X",
"port": 1883,
"base": "ems-esp",
"username": "xxxx",
"password": "xxxx",
"client_id": "esp32-395c7bcc",
"keep_alive": 60,
"clean_session": false,
"entity_format": 1,
"publish_time_boiler": 60,
"publish_time_thermostat": 60,
"publish_time_solar": 60,
"publish_time_mixer": 60,
"publish_time_water": 60,
"publish_time_other": 60,
"publish_time_sensor": 60,
"publish_time_heartbeat": 10,
"mqtt_qos": 0,
"mqtt_retain": false,
"ha_enabled": true,
"nested_format": 1,
"discovery_prefix": "homeassistant",
"discovery_type": 0,
"ha_number_mode": 1,
"publish_single": false,
"publish_single2cmd": false,
"send_response": false
},
"NTP": {
"enabled": true,
"server": "time.google.com",
"tz_label": "Europe/Amsterdam",
"tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
},
"Security": {
"jwt_secret": "ems-esp-neo",
"users": [
{
"username": "admin",
"password": "admin",
"admin": true
},
{
"username": "guest",
"password": "guest",
"admin": false
}
]
},
"Settings": {
"version": "3.8.2",
"board_profile": "E32V2",
"platform": "ESP32",
"locale": "en",
"tx_mode": 1,
"ems_bus_id": 11,
"syslog_enabled": false,
"syslog_level": 3,
"trace_raw": false,
"syslog_mark_interval": 0,
"syslog_host": "",
"syslog_port": 514,
"boiler_heatingoff": false,
"remote_timeout": 24,
"remote_timeout_en": false,
"shower_timer": true,
"shower_alert": false,
"shower_alert_coldshot": 10,
"shower_alert_trigger": 7,
"shower_min_duration": 180,
"rx_gpio": 4,
"tx_gpio": 5,
"dallas_gpio": 14,
"dallas_parasite": false,
"led_gpio": 2,
"hide_led": true,
"led_type": 0,
"low_clock": false,
"telnet_enabled": true,
"notoken_api": false,
"readonly_mode": false,
"analog_enabled": true,
"pbutton_gpio": 34,
"solar_maxflow": 30,
"fahrenheit": false,
"bool_format": 1,
"bool_dashboard": 1,
"enum_format": 1,
"weblog_level": 6,
"weblog_buffer": 500,
"weblog_compact": true,
"phy_type": 1,
"eth_power": 15,
"eth_phy_addr": 0,
"eth_clock_mode": 1,
"modbus_enabled": false,
"modbus_port": 502,
"modbus_max_clients": 10,
"modbus_timeout": 300,
"developer_mode": true,
"email_enabled": false,
"email_ssl": false,
"email_starttls": true,
"email_server": "smtp.example.net",
"email_port": 587,
"email_login": "",
"email_pass": "",
"email_sender": "ems-esp@example.net",
"email_recp": "",
"email_subject": "ems-esp notification"
}
}

View File

@@ -0,0 +1,249 @@
{
"type": "systembackup",
"version": "3.8.2",
"date": "2026-04-15T08:06:45",
"systembackup": [
{
"type": "settings",
"Network": {
"ssid": "",
"bssid": "",
"password": "",
"hostname": "ems-esp",
"static_ip_config": false,
"bandwidth20": false,
"nosleep": true,
"enableMDNS": true,
"enableCORS": false,
"CORSOrigin": "*",
"tx_power": 0
},
"AP": {
"provision_mode": 2,
"ssid": "ems-esp",
"password": "ems-esp-neo",
"channel": 1,
"ssid_hidden": false,
"max_clients": 4,
"local_ip": "192.168.X.X",
"gateway_ip": "192.168.X.X",
"subnet_mask": "255.255.255.0"
},
"MQTT": {
"enableTLS": false,
"rootCA": "",
"enabled": true,
"host": "192.168.X.X",
"port": 1883,
"base": "ems-esp",
"username": "xxxx",
"password": "xxxx",
"client_id": "esp32-395c7bcc",
"keep_alive": 60,
"clean_session": false,
"entity_format": 1,
"publish_time_boiler": 60,
"publish_time_thermostat": 60,
"publish_time_solar": 60,
"publish_time_mixer": 60,
"publish_time_water": 60,
"publish_time_other": 60,
"publish_time_sensor": 60,
"publish_time_heartbeat": 10,
"mqtt_qos": 0,
"mqtt_retain": false,
"ha_enabled": true,
"nested_format": 1,
"discovery_prefix": "homeassistant",
"discovery_type": 0,
"ha_number_mode": 1,
"publish_single": false,
"publish_single2cmd": false,
"send_response": false
},
"NTP": {
"enabled": true,
"server": "time.google.com",
"tz_label": "Europe/Amsterdam",
"tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
},
"Security": {
"jwt_secret": "ems-esp-neo",
"users": [
{
"username": "admin",
"password": "admin",
"admin": true
},
{
"username": "guest",
"password": "guest",
"admin": false
}
]
},
"Settings": {
"version": "3.8.2",
"board_profile": "E32V2",
"platform": "ESP32",
"locale": "en",
"tx_mode": 1,
"ems_bus_id": 11,
"syslog_enabled": false,
"syslog_level": 3,
"trace_raw": false,
"syslog_mark_interval": 0,
"syslog_host": "",
"syslog_port": 514,
"boiler_heatingoff": false,
"remote_timeout": 24,
"remote_timeout_en": false,
"shower_timer": true,
"shower_alert": false,
"shower_alert_coldshot": 10,
"shower_alert_trigger": 7,
"shower_min_duration": 180,
"rx_gpio": 4,
"tx_gpio": 5,
"dallas_gpio": 14,
"dallas_parasite": false,
"led_gpio": 2,
"hide_led": true,
"led_type": 0,
"low_clock": false,
"telnet_enabled": true,
"notoken_api": false,
"readonly_mode": false,
"analog_enabled": true,
"pbutton_gpio": 34,
"solar_maxflow": 30,
"fahrenheit": false,
"bool_format": 1,
"bool_dashboard": 1,
"enum_format": 1,
"weblog_level": 6,
"weblog_buffer": 75,
"weblog_compact": true,
"phy_type": 1,
"eth_power": 15,
"eth_phy_addr": 0,
"eth_clock_mode": 1,
"modbus_enabled": false,
"modbus_port": 502,
"modbus_max_clients": 10,
"modbus_timeout": 300,
"developer_mode": true
}
},
{
"type": "schedule",
"Schedule": {
"schedule": []
}
},
{
"type": "customizations",
"Customizations": {
"ts": [],
"as": [],
"masked_entities": [
{
"product_id": 77,
"device_id": 23,
"custom_name": "",
"custom_brand": "",
"entity_ids": [
"08datetime",
"08hc1/seltemp|<30",
"08hc1/currtemp",
"08hc1/mode"
]
},
{
"product_id": 123,
"device_id": 8,
"custom_name": "",
"custom_brand": "",
"entity_ids": [
"08heatingactive",
"08burngas",
"08fanwork",
"08ignwork",
"08burnmaxpower|>23<121",
"08burnminperiod|<120",
"08lastcode",
"08servicecode"
]
}
]
}
},
{
"type": "entities",
"Entities": {
"entities": []
}
},
{
"type": "modules",
"Modules": {
"modules": []
}
},
{
"type": "nvs",
"nvs": [
{
"type": 1,
"key": "nompower",
"value": 30
},
{
"type": 33,
"key": "boot",
"value": "3.8.1-dev.4"
},
{
"type": 4,
"key": "d_boot",
"value": 1767525325
},
{
"type": 33,
"key": "app1",
"value": "3.8.2-dev.13"
},
{
"type": 33,
"key": "app0",
"value": "3.8.1"
},
{
"type": 4,
"key": "d_app0",
"value": 1774988066
},
{
"type": 1,
"key": "fresh_firmware",
"value": 0
},
{
"type": 4,
"key": "d_app1",
"value": 1776194060
},
{
"type": 66,
"key": "nrgheat",
"value": 485871.5481
},
{
"type": 66,
"key": "nrgww",
"value": 101649.2176
}
]
}
]
}