Merge pull request #2685 from proddy/dev

small changes to make web tables prettier and consistent
This commit is contained in:
Proddy
2025-10-25 17:12:42 +02:00
committed by GitHub
31 changed files with 529 additions and 348 deletions

View File

@@ -70,7 +70,7 @@ Visit [emsesp.org](https://docs.emsesp.org) for more details on how to install a
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
If you find an issue or have a request, see [here](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
If you find an issue or have a request, see [how to request support](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
## 🎥  **Live Demo**
@@ -82,13 +82,19 @@ EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned an
If you like **EMS-ESP**, please give it a ✨ on GitHub, or even better fork it and contribute. You can also offer a small donation. This is an open-source project maintained by volunteers, and your support is greatly appreciated.
## 📦  **Building**
To build the web interface only, run `platformio run -e build_webUI`. This will install the necessary dependencies and build the web interface and also create the embedded code used need to build the firmware. You can run the web interface locally by going to the `interface` directory and running `pnpm standalone`.
To build the firmware, run `platformio run`. This will build the firmware for all ESP32 modules and place the binaries in the `build/firmware` folder. If you want to configure the build for a single platform create a local `pio_local.ni` file in the root directory (see example in `pio_local.ini_example`).
## 📢  **Libraries used**
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the core framework that provides the Web UI, which has been heavily modified
- [esp8266-react](https://github.com/rjwats/esp8266-react) originally by @rjwats for the core framework that provides the Web UI, which has been heavily modified
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these awesome open source libraries
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server and TCP backends
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server
## 📜  **License**

View File

@@ -635,8 +635,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@paralleldrive/cuid2@2.2.2':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
@@ -3565,7 +3565,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@paralleldrive/cuid2@2.2.2':
'@paralleldrive/cuid2@2.3.1':
dependencies:
'@noble/hashes': 1.8.0
@@ -4714,7 +4714,7 @@ snapshots:
formidable@3.5.4:
dependencies:
'@paralleldrive/cuid2': 2.2.2
'@paralleldrive/cuid2': 2.3.1
dezalgo: 1.0.4
once: 1.4.0

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { ToastContainer, Zoom } from 'react-toastify';
import AppRouting from 'AppRouting';
@@ -8,7 +8,8 @@ import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors';
const availableLocales = [
// Memoize available locales to prevent recreation on every render
const AVAILABLE_LOCALES = [
'de',
'en',
'it',
@@ -20,47 +21,59 @@ const availableLocales = [
'sv',
'tr',
'cz'
];
] as Locales[];
const App = () => {
const App = memo(() => {
const [wasLoaded, setWasLoaded] = useState(false);
const [locale, setLocale] = useState<Locales>('en');
useEffect(() => {
// determine locale, take from session if set other default to browser language
const browserLocale = detectLocale('en', availableLocales, navigatorDetector);
// Memoize locale initialization to prevent unnecessary re-runs
const initializeLocale = useCallback(async () => {
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
localStorage.setItem('lang', newLocale);
setLocale(newLocale);
void loadLocaleAsync(newLocale).then(() => setWasLoaded(true));
await loadLocaleAsync(newLocale);
setWasLoaded(true);
}, []);
useEffect(() => {
void initializeLocale();
}, [initializeLocale]);
// Memoize toast container props to prevent recreation
const toastContainerProps = useMemo(
() => ({
position: 'bottom-left' as const,
autoClose: 3000,
hideProgressBar: false,
newestOnTop: false,
closeOnClick: true,
rtl: false,
pauseOnFocusLoss: true,
draggable: false,
pauseOnHover: false,
transition: Zoom,
closeButton: false,
theme: 'dark' as const,
toastStyle: {
border: '1px solid #177ac9',
width: 'fit-content'
}
}),
[]
);
if (!wasLoaded) return null;
return (
<TypesafeI18n locale={locale}>
<CustomTheme>
<AppRouting />
<ToastContainer
position="bottom-left"
autoClose={3000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable={false}
pauseOnHover={false}
transition={Zoom}
closeButton={false}
theme="dark"
toastStyle={{
border: '1px solid #177ac9'
}}
/>
<ToastContainer {...toastContainerProps} />
</CustomTheme>
</TypesafeI18n>
);
};
});
export default App;

View File

@@ -1,3 +1,4 @@
import { memo } from 'react';
import type { FC } from 'react';
import { CssBaseline, ThemeProvider, responsiveFontSizes } from '@mui/material';
@@ -5,6 +6,7 @@ import { createTheme } from '@mui/material/styles';
import type { RequiredChildrenProps } from 'utils';
// Memoize dialog style to prevent recreation
export const dialogStyle = {
'& .MuiDialog-paper': {
borderRadius: '8px',
@@ -12,8 +14,9 @@ export const dialogStyle = {
borderStyle: 'solid',
borderWidth: '1px'
}
};
} as const;
// Memoize theme creation to prevent recreation
const theme = responsiveFontSizes(
createTheme({
typography: {
@@ -30,15 +33,27 @@ const theme = responsiveFontSizes(
text: {
disabled: '#eee' // white
}
},
components: {
MuiListItemText: {
styleOverrides: {
primary: {
fontSize: 14
},
secondary: {
color: '#9e9e9e' // grey[500]
}
}
}
}
})
);
const CustomTheme: FC<RequiredChildrenProps> = ({ children }) => (
const CustomTheme: FC<RequiredChildrenProps> = memo(({ children }) => (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
);
));
export default CustomTheme;

View File

@@ -206,7 +206,7 @@ const Dashboard = memo(() => {
// if its a device (parent node) and has entities
if (di.nodes?.length) {
return (
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>
<span style={{ fontSize: '15px' }}>
<DeviceIcon type_id={di.t ?? 0} />
&nbsp;&nbsp;{showType(di.n, di.t)}
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
@@ -312,7 +312,7 @@ const Dashboard = memo(() => {
flexDirection="column"
sx={{
borderRadius: 1,
border: '1px solid grey'
border: '1px solid rgb(65, 65, 65)'
}}
>
<IconContext.Provider

View File

@@ -1,8 +1,10 @@
import {
memo,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useState
} from 'react';
import { IconContext } from 'react-icons';
@@ -75,7 +77,7 @@ import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
import type { Device, DeviceValue } from './types';
import { deviceValueItemValidation } from './validators';
const Devices = () => {
const Devices = memo(() => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
@@ -141,11 +143,13 @@ const Devices = () => {
return left + (right - left < 400 ? 0 : 200);
};
const common_theme = useTheme({
BaseRow: `
const common_theme = useMemo(
() =>
useTheme({
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
@@ -153,7 +157,7 @@ const Devices = () => {
border-bottom: 1px solid #565656;
}
`,
Row: `
Row: `
cursor: pointer;
background-color: #1E1E1E;
.td {
@@ -163,30 +167,47 @@ const Devices = () => {
background-color: #177ac9;
}
`
});
}),
[]
);
const device_theme = useTheme([
common_theme,
{
Table: `
const device_theme = useMemo(
() =>
useTheme([
common_theme,
{
BaseRow: `
font-size: 15px;
.td {
height: 28px;
}
`,
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
`,
HeaderRow: `
HeaderRow: `
.th {
padding: 8px;
`,
Row: `
font-weight: bold;
Row: `
&:nth-of-type(odd) .td {
background-color: #303030;
},
&:hover .td {
background-color: #177ac9;
},
`
}
]);
}
]),
[common_theme]
);
const data_theme = useTheme([
common_theme,
{
Table: `
const data_theme = useMemo(
() =>
useTheme([
common_theme,
{
Table: `
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
height: auto;
max-height: 100%;
@@ -195,12 +216,12 @@ const Devices = () => {
display:none;
}
`,
BaseRow: `
BaseRow: `
.td {
height: 32px;
}
`,
BaseCell: `
BaseCell: `
&:nth-of-type(1) {
border-left: 1px solid #177ac9;
},
@@ -211,12 +232,12 @@ const Devices = () => {
border-right: 1px solid #177ac9;
}
`,
HeaderRow: `
HeaderRow: `
.th {
border-top: 1px solid #565656;
}
`,
Row: `
Row: `
&:nth-of-type(odd) .td {
background-color: #303030;
},
@@ -224,8 +245,10 @@ const Devices = () => {
background-color: #177ac9;
}
`
}
]);
}
]),
[common_theme]
);
const getSortIcon = (state: State, sortKey: unknown) => {
if (state.sortKey === sortKey && state.reverse) {
@@ -324,8 +347,10 @@ const Devices = () => {
return sc;
};
const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask;
const hasMask = useCallback(
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
[]
);
const handleDownloadCsv = () => {
const deviceIndex = coreData.devices.findIndex(
@@ -510,55 +535,65 @@ const Devices = () => {
const renderCoreData = () => (
<>
<IconContext.Provider
value={{
color: 'lightblue',
size: '18',
style: { verticalAlign: 'middle' }
<Box
padding={1}
justifyContent="center"
flexDirection="column"
sx={{
borderRadius: 1,
border: '1px solid rgb(65, 65, 65)'
}}
>
{!coreData.connected && (
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
)}
<IconContext.Provider
value={{
color: 'lightblue',
size: '18',
style: { verticalAlign: 'middle' }
}}
>
{!coreData.connected && (
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
)}
{coreData.connected && (
<Table
data={{ nodes: coreData.devices }}
select={device_select}
theme={device_theme}
layout={{ custom: true }}
>
{(tableList: Device[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.length === 0 && (
<CircularProgress sx={{ margin: 1 }} size={18} />
)}
{tableList.map((device: Device) => (
<Row key={device.id} item={device}>
<Cell>
<DeviceIcon type_id={device.t} />
&nbsp;&nbsp;
{device.n}
<span style={{ color: 'lightblue' }}>
&nbsp;&nbsp;({device.e})
</span>
</Cell>
<Cell stiff>{device.tn}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
)}
</IconContext.Provider>
{coreData.connected && (
<Table
data={{ nodes: coreData.devices }}
select={device_select}
theme={device_theme}
layout={{ custom: true }}
>
{(tableList: Device[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.length === 0 && (
<CircularProgress sx={{ margin: 1 }} size={18} />
)}
{tableList.map((device: Device) => (
<Row key={device.id} item={device}>
<Cell>
<DeviceIcon type_id={device.t} />
&nbsp;&nbsp;
{device.n}
<span style={{ color: 'lightblue' }}>
&nbsp;&nbsp;({device.e})
</span>
</Cell>
<Cell stiff>{device.tn}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
)}
</IconContext.Provider>
</Box>
</>
);
@@ -574,35 +609,41 @@ const Devices = () => {
return;
}
const showDeviceValue = (dv: DeviceValue) => {
const showDeviceValue = useCallback((dv: DeviceValue) => {
setSelectedDeviceValue(dv);
setDeviceValueDialogOpen(true);
};
}, []);
const renderNameCell = (dv: DeviceValue) => (
<>
{dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
<StarIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</>
const renderNameCell = useCallback(
(dv: DeviceValue) => (
<>
{dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
<StarIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</>
),
[hasMask]
);
const shown_data = onlyFav
? deviceData.nodes.filter(
const shown_data = useMemo(() => {
if (onlyFav) {
return deviceData.nodes.filter(
(dv: DeviceValue) =>
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
)
: deviceData.nodes.filter((dv: DeviceValue) =>
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
);
}
return deviceData.nodes.filter((dv: DeviceValue) =>
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
);
}, [deviceData.nodes, onlyFav, search]);
const deviceIndex = coreData.devices.findIndex(
(d: Device) => d.id === device_select.state.id
@@ -795,6 +836,6 @@ const Devices = () => {
)}
</SectionContent>
);
};
});
export default Devices;

View File

@@ -19,9 +19,10 @@ import {
Grid,
IconButton,
Link,
List,
ListItem,
ListItemText,
Table,
TableBody,
TableCell,
TableRow,
Typography
} from '@mui/material';
@@ -41,6 +42,7 @@ import {
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { TranslationFunctions } from 'i18n/i18n-types';
import { prettyDateTime } from 'utils/time';
// Constants moved outside component to avoid recreation
@@ -86,7 +88,7 @@ const VersionInfoDialog = memo(
latestVersion?: VersionInfo;
latestDevVersion?: VersionInfo;
locale: string;
LL: any;
LL: TranslationFunctions;
onClose: () => void;
}) => {
if (showVersionInfo === 0) return null;
@@ -97,30 +99,69 @@ const VersionInfoDialog = memo(
return (
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
<DialogTitle>Version Information</DialogTitle>
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
<DialogContent dividers>
<List dense>
<ListItem>
<ListItemText
primary={<span style={{ color: 'lightblue' }}>{LL.TYPE(0)}</span>}
secondary={isStable ? LL.STABLE() : LL.DEVELOPMENT()}
/>
</ListItem>
<ListItem>
<ListItemText
primary={<span style={{ color: 'lightblue' }}>{LL.VERSION()}</span>}
secondary={version?.name}
/>
</ListItem>
{version?.published_at && (
<ListItem>
<ListItemText
primary={<span style={{ color: 'lightblue' }}>Release Date</span>}
secondary={prettyDateTime(locale, new Date(version.published_at))}
/>
</ListItem>
)}
</List>
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
<TableBody>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13,
width: 90
}}
>
{LL.TYPE(0)}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{isStable ? LL.STABLE() : LL.DEVELOPMENT()}
</TableCell>
</TableRow>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
{LL.VERSION()}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{version?.name}
</TableCell>
</TableRow>
{version?.published_at && (
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
Build Date
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{prettyDateTime(locale, new Date(version.published_at))}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button
@@ -159,7 +200,7 @@ const InstallDialog = memo(
latestDevVersion?: VersionInfo;
downloadOnly: boolean;
platform: string;
LL: any;
LL: TranslationFunctions;
onClose: () => void;
onInstall: (url: string) => void;
}) => {

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import type { FC } from 'react';
import { useLocation } from 'react-router';
@@ -13,22 +13,26 @@ import { LayoutContext } from './context';
export const DRAWER_WIDTH = 210;
const Layout: FC<RequiredChildrenProps> = ({ children }) => {
const Layout: FC<RequiredChildrenProps> = memo(({ children }) => {
const [mobileOpen, setMobileOpen] = useState(false);
const [title, setTitle] = useState(PROJECT_NAME);
const { pathname } = useLocation();
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
// Memoize drawer toggle handler to prevent unnecessary re-renders
const handleDrawerToggle = useCallback(() => {
setMobileOpen((prev) => !prev);
}, []);
useEffect(() => setMobileOpen(false), [pathname]);
// Close drawer when route changes
useEffect(() => {
setMobileOpen(false);
}, [pathname]);
// cache the object to prevent unnecessary re-renders
const obj = useMemo(() => ({ title, setTitle }), [title]);
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
return (
<LayoutContext.Provider value={obj}>
<LayoutContext.Provider value={contextValue}>
<LayoutAppBar title={title} onToggleDrawer={handleDrawerToggle} />
<LayoutDrawer mobileOpen={mobileOpen} onClose={handleDrawerToggle} />
<Box component="main" sx={{ marginLeft: { md: `${DRAWER_WIDTH}px` } }}>
@@ -37,6 +41,6 @@ const Layout: FC<RequiredChildrenProps> = ({ children }) => {
</Box>
</LayoutContext.Provider>
);
};
});
export default Layout;

View File

@@ -73,19 +73,6 @@ const LayoutMenu = () => {
>
<ListItemText
primary={LL.MODULES()}
// secondary={
// LL.CUSTOMIZATIONS() +
// ', ' +
// LL.SCHEDULER() +
// ', ' +
// LL.CUSTOM_ENTITIES(0) +
// '...'
// }
// secondaryTypographyProps={{
// noWrap: true,
// fontSize: 12,
// color: menuOpen ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0.5)'
// }}
sx={{ my: 0 }}
slotProps={{
primary: {

View File

@@ -28,11 +28,47 @@ const LayoutMenuItem = ({
to={to}
disabled={disabled || false}
selected={selected}
sx={{
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transform: selected ? 'scale(1.02)' : 'scale(1)',
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
borderRadius: '8px',
margin: '2px 8px',
'&:hover': {
backgroundColor: 'rgba(68, 82, 211, 0.39)',
transform: selected ? 'scale(1.02)' : 'scale(1.01)'
},
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: selected ? '4px' : '0px',
backgroundColor: '#90caf9',
borderRadius: '0 2px 2px 0',
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
}
}}
>
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
<ListItemIcon
sx={{
color: selected ? '#90caf9' : '#9e9e9e',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transform: selected ? 'scale(1.1)' : 'scale(1)',
transitionProperty: 'color, transform'
}}
>
<Icon />
</ListItemIcon>
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>
<ListItemText
sx={{
color: selected ? '#90caf9' : '#f5f5f5',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
// fontWeight: selected ? '600' : '400',
transitionProperty: 'color, font-weight'
}}
>
{label}
</ListItemText>
</ListItemButton>

View File

@@ -1,6 +1,8 @@
import { memo } from 'react';
import { Box, CircularProgress } from '@mui/material';
const LazyLoader = () => (
const LazyLoader = memo(() => (
<Box
display="flex"
justifyContent="center"
@@ -15,6 +17,6 @@ const LazyLoader = () => (
>
<CircularProgress size={40} />
</Box>
);
));
export default LazyLoader;

View File

@@ -352,7 +352,8 @@ const cz: Translation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ sestavení',
INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení',
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi'
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru'
};
export default cz;

View File

@@ -352,7 +352,8 @@ const de: Translation = {
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Release Typ',
INTERNET_CONNECTION_REQUIRED: 'Für die automatische Versionsprüfung und Aktualisierung ist eine Internetverbindung erforderlich',
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln'
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln',
FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation'
};
export default de;

View File

@@ -180,10 +180,10 @@ const en: Translation = {
DEVELOPMENT: 'Development',
UPTIME: 'System Uptime',
FREE_MEMORY: 'Free Memory',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size , Speed)',
APPSIZE: 'Application (Partition: Used / Free)',
FILESYSTEM: 'File System (Used / Free)',
PSRAM: 'PSRAM (size / free)',
FLASH: 'Flash Chip (size , speed)',
APPSIZE: 'Application (partition: used / free)',
FILESYSTEM: 'File System (used / free)',
BUFFER_SIZE: 'Max Buffer Size',
COMPACT: 'Compact',
DOWNLOAD_SETTINGS_TEXT: 'Create a backup of your configuration and settings',
@@ -352,7 +352,8 @@ const en: Translation = {
PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Type',
INTERNET_CONNECTION_REQUIRED: 'Internet connection required for automatic version checking and upgrading',
SWITCH_RELEASE_TYPE: 'Switch to {0} release'
SWITCH_RELEASE_TYPE: 'Switch to {0} release',
FIRMWARE_VERSION_INFO: 'Firmware Version Information'
};
export default en;

View File

@@ -352,7 +352,8 @@ const fr: Translation = {
PLATFORM: 'Plateforme',
RELEASE_TYPE: 'Type de version',
INTERNET_CONNECTION_REQUIRED: 'Connexion Internet requise pour la vérification automatique des versions et la mise à niveau',
SWITCH_RELEASE_TYPE: 'Passer à la version {0}'
SWITCH_RELEASE_TYPE: 'Passer à la version {0}',
FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware'
};
export default fr;

View File

@@ -352,7 +352,8 @@ const it: Translation = {
PLATFORM: 'Piattaforma',
RELEASE_TYPE: 'Tipo di rilascio',
INTERNET_CONNECTION_REQUIRED: 'Connessione internet richiesta per il controllo automatico delle versioni e l\'aggiornamento',
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio'
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio',
FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware'
};
export default it;

View File

@@ -179,12 +179,12 @@ const nl: Translation = {
STABLE: 'Stable',
DEVELOPMENT: 'Development',
UPTIME: 'Systeem Uptime',
FREE_MEMORY: 'Free Memory',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size , Speed)',
APPSIZE: 'Application (Partition: Used / Free)',
FILESYSTEM: 'File System (Used / Free)',
BUFFER_SIZE: 'Max Buffer Size',
FREE_MEMORY: 'Vrij geheugen',
PSRAM: 'PSRAM (grootte / vrij)',
FLASH: 'Flash Chip (grootte , snelheid)',
APPSIZE: 'Applicatie (partition: gebruikt / vrij)',
FILESYSTEM: 'Bestandssysteem (gebruikt / vrij)',
BUFFER_SIZE: 'Max buffer grootte',
COMPACT: 'Compact',
DOWNLOAD_SETTINGS_TEXT: 'Maak een back-up van uw configuratie en instellingen',
UPLOAD_TEXT: 'Upload een nieuw firmwarebestand (.bin) of een back-upbestand (.json)',
@@ -226,7 +226,7 @@ const nl: Translation = {
MQTT_INT_MIXER: 'Mixer Modules',
MQTT_INT_WATER: 'Water Modules',
MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Default',
DEFAULT: 'Standaard',
MQTT_ENTITY_FORMAT: 'Entity ID formaat',
MQTT_ENTITY_FORMAT_0: 'Eén instantie, lange naam (v3.4)',
MQTT_ENTITY_FORMAT_1: 'Eén instantie, korte naam',
@@ -250,20 +250,20 @@ const nl: Translation = {
AP_PROVIDE_TEXT_3: 'nooit',
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
AP_HIDE_SSID: 'SSID verbergen',
AP_CLIENTS: 'AP Clients',
AP_MAX_CLIENTS: 'Max Clients',
AP_LOCAL_IP: 'Local IP',
AP_CLIENTS: 'AP Gebruikers',
AP_MAX_CLIENTS: 'Max Gebruikers',
AP_LOCAL_IP: 'Lokale IP',
NETWORK_SCAN: 'Scan WiFi Netwerken',
IDLE: 'Idle',
IDLE: 'Inactief',
LOST: 'Verloren',
SCANNING: 'Scannen',
SCAN_AGAIN: 'Opnieuw scannen',
NETWORK_SCANNER: 'Netwerk Scanner',
NETWORK_SCANNER: 'Netwerk Scannen',
NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden',
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken',
TX_POWER: 'Tx Vermogen',
HOSTNAME: 'Hostname',
HOSTNAME: 'Hostnaam',
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
NETWORK_LOW_BAND: 'Lagere WiFi bandbreedte gebruiken',
NETWORK_USE_DNS: 'Activeer mDNS Service',
@@ -273,9 +273,9 @@ const nl: Translation = {
NETWORK_GATEWAY: 'Gateway',
NETWORK_SUBNET: 'Subnetmasker',
NETWORK_DNS: 'DNS Servers',
ADDRESS_OF: '{0} Address',
ADMINISTRATOR: 'Administrator',
GUEST: 'Gast',
ADDRESS_OF: '{0} Adres',
ADMINISTRATOR: 'Beheerder',
GUEST: 'Bezoeker',
NEW: 'Nieuwe',
NEW_NAME_OF: 'Hernoem {0}',
ENTITY: 'Entiteit',
@@ -352,7 +352,8 @@ const nl: Translation = {
PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Typ',
INTERNET_CONNECTION_REQUIRED: 'Internetverbinding vereist voor automatische versiecontrole en -upgrade',
SWITCH_RELEASE_TYPE: 'Switch naar {0} release'
SWITCH_RELEASE_TYPE: 'Switch naar {0} release',
FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie'
};
export default nl;

View File

@@ -352,7 +352,8 @@ const no: Translation = {
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelses type',
INTERNET_CONNECTION_REQUIRED: 'Internettilkobling kreves for automatisk versjonskontroll og oppgradering',
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse'
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse',
FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon'
};
export default no;

View File

@@ -352,7 +352,8 @@ const pl: BaseTranslation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ wydania',
INTERNET_CONNECTION_REQUIRED: 'Połączenie internetowe jest wymagane do automatycznej kontroli wersji i aktualizacji',
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie'
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie',
FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware'
};
export default pl;

View File

@@ -352,7 +352,8 @@ const sk: Translation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ vydania',
INTERNET_CONNECTION_REQUIRED: 'Internetové pripojenie je potrebné pre automatickú kontrolu a aktualizáciu',
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu'
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu',
FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware'
};
export default sk;

View File

@@ -352,7 +352,8 @@ const sv: Translation = {
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelsestyp',
INTERNET_CONNECTION_REQUIRED: 'Internetanslutning krävs för automatisk version kontroll och uppdatering',
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva'
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva',
FIRMWARE_VERSION_INFO: 'Information om firmwareversion'
};
export default sv;

View File

@@ -352,7 +352,8 @@ const tr: Translation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Sürüm Tipi',
INTERNET_CONNECTION_REQUIRED: 'Otomatik sürüm kontrolü ve güncelleme için internet bağlantısı gereklidir',
SWITCH_RELEASE_TYPE: '{0} sürümüne geç'
SWITCH_RELEASE_TYPE: '{0} sürümüne geç',
FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi'
};
export default tr;

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { useBlocker } from 'react-router';
import { toast } from 'react-toastify';
@@ -35,20 +35,26 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
setDirtyFlags([]);
});
const updateDataValue = (new_data: D) => {
updateData({ data: new_data });
};
// Memoize updateDataValue to prevent unnecessary re-renders
const updateDataValue = useCallback(
(new_data: D) => {
updateData({ data: new_data });
},
[updateData]
);
const loadData = async () => {
// Memoize loadData to prevent unnecessary re-renders
const loadData = useCallback(async () => {
setDirtyFlags([]);
setErrorMessage(undefined);
await readData().catch((error: Error) => {
toast.error(error.message);
setErrorMessage(error.message);
});
};
}, [readData]);
const saveData = async () => {
// Memoize saveData to prevent unnecessary re-renders
const saveData = useCallback(async () => {
if (!data) {
return;
}
@@ -64,14 +70,15 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
setErrorMessage(error.message);
}
});
};
}, [data, writeData]);
return {
loadData,
saveData,
saving: saving as boolean,
updateDataValue,
data: data as D, // Explicitly define the type of 'data'
origData: origData as D, // Explicitly define the type of 'origData' to 'D'
data: data as D,
origData: origData as D,
dirtyFlags,
setDirtyFlags,
setOrigData,

View File

@@ -54,6 +54,7 @@
"skipDefaultLibCheck": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
"assumeChangesOnlyAffectDirectDependencies": true,
// Path mapping for cleaner imports
"baseUrl": ".",

View File

@@ -281,11 +281,12 @@ export default defineConfig(
},
rollupOptions: {
// Enable tree shaking
// Enable aggressive tree shaking
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
tryCatchDeoptimization: false
tryCatchDeoptimization: false,
unknownGlobalSideEffects: false
},
output: {
// Optimize chunk naming for better caching
@@ -301,6 +302,24 @@ export default defineConfig(
if (id.includes('preact')) {
return '@preact';
}
if (id.includes('@mui/material')) {
return '@mui-material';
}
if (id.includes('@mui/icons-material')) {
return '@mui-icons';
}
if (id.includes('alova')) {
return '@alova';
}
if (id.includes('typesafe-i18n')) {
return '@i18n';
}
if (id.includes('react-toastify')) {
return '@toastify';
}
if (id.includes('@table-library')) {
return '@table-library';
}
if (id.includes('uuid')) {
return '@uuid';
}
@@ -316,8 +335,14 @@ export default defineConfig(
if (id.includes('components/')) {
return 'components';
}
if (id.includes('pages/') || id.includes('routes/')) {
return 'pages';
if (id.includes('app/')) {
return 'app';
}
if (id.includes('utils/')) {
return 'utils';
}
if (id.includes('api/')) {
return 'api';
}
return undefined;
},

View File

@@ -84,8 +84,8 @@ packages:
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
'@paralleldrive/cuid2@2.2.2':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@trivago/prettier-plugin-sort-imports@5.2.2':
resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==}
@@ -223,7 +223,7 @@ snapshots:
'@noble/hashes@1.8.0': {}
'@paralleldrive/cuid2@2.2.2':
'@paralleldrive/cuid2@2.3.1':
dependencies:
'@noble/hashes': 1.8.0
@@ -252,7 +252,7 @@ snapshots:
formidable@3.5.4:
dependencies:
'@paralleldrive/cuid2': 2.2.2
'@paralleldrive/cuid2': 2.3.1
dezalgo: 1.0.4
once: 1.4.0

View File

@@ -114,7 +114,9 @@ lib_deps =
[env:build_webUI]
platform = native
targets = build
extra_scripts = post:scripts/build_interface.py
extra_scripts = pre:scripts/build_interface.py
; Exclude all source files so the C code doesn't build
build_src_filter = -<*>
;
; Builds for different board types

View File

@@ -116,5 +116,7 @@ env.AddCustomTarget(
dependencies=None,
actions=[build_webUI],
title="build web interface",
description="installs pnpm packages, updates libraries and builds web UI"
description="installs pnpm packages, updates libraries and builds web UI",
always_build=True
)

View File

@@ -2,133 +2,121 @@ import hashlib
import shutil
import re
import os
from pathlib import Path
Import("env")
OUTPUT_DIR = "build{}".format(os.path.sep)
OUTPUT_DIR = Path("build")
def bin_copy(source, target, env):
"""Optimized firmware renaming and copying function."""
# get the application version from emsesp_version.h
bag = {}
exprs = [(re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"'), 'app_version')]
with open('./src/emsesp_version.h', 'r') as f:
for l in f.readlines():
for expr, var in exprs:
m = expr.match(l)
if m and len(m.groups()) > 0:
bag[var] = m.group(1)
# Get the application version from emsesp_version.h
version_file = Path('./src/emsesp_version.h')
if not version_file.exists():
print("Error: emsesp_version.h not found!")
return
app_version = bag.get('app_version')
app_version = None
version_pattern = re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"')
# print(env.Dump())
with version_file.open('r') as f:
for line in f:
match = version_pattern.match(line)
if match:
app_version = match.group(1)
break
# get the chip type, in uppercase
mcu = env.get('BOARD_MCU').upper()
# alternatively take platform from the pio target
# platform = str(target[0]).split(os.path.sep)[2]
if not app_version:
print("Error: Could not find EMSESP_APP_VERSION in emsesp_version.h!")
return
# work out the flash memory from the PIO env name (sloppy but works)
# unfortunately the board_upload.flash_size is not passed down
flash_mem = "4MB"
pio_env = env.get('PIOENV').upper()
parts = pio_env.split('_')
# if it ends with a _P skip (we use this to denote PSRAM)
if parts[-1].endswith("P"):
index = -2
else:
index = -1
# Get the chip type, in uppercase
mcu = env.get('BOARD_MCU', '').upper()
if not mcu:
print("Error: Could not determine MCU type!")
return
# if doesn't have an M at the end
if parts[index].endswith("M"):
flash_mem = parts[index] + "B"
# Work out the flash memory from the PIO env name
flash_mem = "4MB" # default
pio_env = env.get('PIOENV', '').upper()
if pio_env:
parts = pio_env.split('_')
# If it ends with _P skip (we use this to denote PSRAM)
index = -2 if parts[-1].endswith("P") else -1
# find if BOARD_HAS_PSRAM is in the cppdefines
cppdefines = env.get("CPPDEFINES")
if 'BOARD_HAS_PSRAM' in cppdefines:
psram = True
else:
psram = False
# If it has an M at the end, use it
if parts[index].endswith("M"):
flash_mem = parts[index] + "B"
print("*********************************************")
print("EMS-ESP version: " + app_version)
# Check if BOARD_HAS_PSRAM is in the cppdefines
cppdefines = env.get("CPPDEFINES", [])
psram = 'BOARD_HAS_PSRAM' in cppdefines
# show psram as Yes or No
psram_status = "Yes" if psram else "No"
print("Has PSRAM: " + psram_status)
print("MCU: "+str(mcu))
print("Flash Mem: " + flash_mem)
print("=" * 90)
print(f"EMS-ESP version: {app_version}")
print(f"Has PSRAM: {'Yes' if psram else 'No'}")
print(f"MCU: {mcu}")
print(f"Flash Mem: {flash_mem}")
# convert . to _ so Windows doesn't complain
# Convert . to _ so Windows doesn't complain
# Format is EMS-ESP-<version>-<mcu>-<flash> with + at the end if it has PSRAM
variant = "EMS-ESP-" + \
app_version.replace(".", "_") + "-" + mcu + "-" + \
flash_mem + ("+" if psram else "")
variant = f"EMS-ESP-{app_version.replace('.', '_')}-{mcu}-{flash_mem}{'+' if psram else ''}"
# check if output directories exist and create if necessary
if not os.path.isdir(OUTPUT_DIR):
os.mkdir(OUTPUT_DIR)
# Create output directories
firmware_dir = OUTPUT_DIR / "firmware"
firmware_dir.mkdir(parents=True, exist_ok=True)
for d in ['firmware']:
if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)):
os.mkdir("{}{}".format(OUTPUT_DIR, d))
# Define file paths
bin_file = firmware_dir / f"{variant}.bin"
md5_file = firmware_dir / f"{variant}.md5"
# create string with location and file names based on variant
bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant)
md5_file = "{}firmware{}{}.md5".format(OUTPUT_DIR, os.path.sep, variant)
# Remove existing files if they exist
for file_path in [bin_file, md5_file]:
if file_path.exists():
file_path.unlink()
# check if new target files exist and remove if necessary
for f in [bin_file]:
if os.path.isfile(f):
os.remove(f)
print(f"Filename: {bin_file}")
# check if new target files exist and remove if necessary
for f in [md5_file]:
if os.path.isfile(f):
os.remove(f)
# Copy firmware.bin to firmware/<variant>.bin
shutil.copy2(str(target[0]), str(bin_file))
print("Filename: "+bin_file)
# Calculate and write MD5 hash
with bin_file.open("rb") as f:
md5_hash = hashlib.md5(f.read()).hexdigest()
# copy firmware.bin to firmware/<variant>.bin
shutil.copy(str(target[0]), bin_file)
print(f"MD5: {md5_hash}")
md5_file.write_text(md5_hash)
with open(bin_file, "rb") as f:
result = hashlib.md5(f.read())
print("MD5: "+result.hexdigest())
file1 = open(md5_file, 'w')
file1.write(result.hexdigest())
file1.close()
# Make a copy using the old 3.6.x filename format for backwards compatibility
# Note: there is a chance newer E32V2s (which use the 16MB partition table and PSRAM)
# are running a custom build of the 3.6.5 firmware as 3.6.5 was released before
# production of the gateway board. Updating via the WebUI will break the system
# and require a manual update.
# make a copy using the old 3.6.x filename format for backwards compatibility with the WebUI version check, e.g.
# create a EMS-ESP-<version>-ESP32_S3.bin if target is s3_16M_P (16MB, PSRAM)
# create a EMS-ESP-<version>-ESP32.bin if target is s_4M (4MB, no PSRAM), compatible only with S32 V1 and E32 V1.0,1.4,1.5
#
# Note: there is a chance newer E32V2s (which use the 16MB partition table and PSRAM) are running a custom build
# of the 3.6.5 firmware as 3.6.5 was released before production of the gateway board. Updating via the WebUI will break the system and require a manual update.
#
extra_variant = ""
if env.get('PIOENV') == "s3_16M_P":
extra_variant = "EMS-ESP-" + \
app_version.replace(".", "_") + "-ESP32_S3"
elif env.get('PIOENV') == "s_4M":
extra_variant = "EMS-ESP-" + app_version.replace(".", "_") + "-ESP32"
pio_env = env.get('PIOENV', '')
extra_variant = None
if pio_env == "s3_16M_P":
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32_S3"
elif pio_env == "s_4M":
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32"
if extra_variant:
extra_bin_file = "{}firmware{}{}.bin".format(
OUTPUT_DIR, os.path.sep, extra_variant)
if os.path.isfile(extra_bin_file):
os.remove(extra_bin_file)
extra_bin_file = firmware_dir / f"{extra_variant}.bin"
extra_md5_file = firmware_dir / f"{extra_variant}.md5"
extra_md5_file = "{}firmware{}{}.md5".format(
OUTPUT_DIR, os.path.sep, extra_variant)
if os.path.isfile(extra_md5_file):
os.remove(extra_md5_file)
# Remove existing files if they exist
for file_path in [extra_bin_file, extra_md5_file]:
if file_path.exists():
file_path.unlink()
shutil.copy(bin_file, extra_bin_file)
shutil.copy(md5_file, extra_md5_file)
print("Filename copy for 3.6.x: "+extra_bin_file)
# Copy files
shutil.copy2(str(bin_file), str(extra_bin_file))
shutil.copy2(str(md5_file), str(extra_md5_file))
print(f"Filename copy for 3.6.x: {extra_bin_file}")
print("*********************************************")
print("=" * 90)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_copy])

View File

@@ -425,7 +425,7 @@ char * Helpers::render_string(char * result, const char * c, const uint8_t len)
c++;
p++;
}
*p = '\0'; // terminat result
*p = '\0'; // terminate result
return result;
}
@@ -467,7 +467,7 @@ char * Helpers::utf8tolatin1(char * result, const char * c, const uint8_t len) {
c++;
p++;
}
*p = '\0'; // terminat result
*p = '\0'; // terminate result
return result;
}
// creates string of hex values from an array of bytes

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.7.3-dev.22"
#define EMSESP_APP_VERSION "3.7.3-dev.23"