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). 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** ## 🎥  **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. 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** ## 📢  **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 - [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 - [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client - [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** ## 📜  **License**

View File

@@ -635,8 +635,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@paralleldrive/cuid2@2.2.2': '@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@popperjs/core@2.11.8': '@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
@@ -3565,7 +3565,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1 fastq: 1.19.1
'@paralleldrive/cuid2@2.2.2': '@paralleldrive/cuid2@2.3.1':
dependencies: dependencies:
'@noble/hashes': 1.8.0 '@noble/hashes': 1.8.0
@@ -4714,7 +4714,7 @@ snapshots:
formidable@3.5.4: formidable@3.5.4:
dependencies: dependencies:
'@paralleldrive/cuid2': 2.2.2 '@paralleldrive/cuid2': 2.3.1
dezalgo: 1.0.4 dezalgo: 1.0.4
once: 1.4.0 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 { ToastContainer, Zoom } from 'react-toastify';
import AppRouting from 'AppRouting'; import AppRouting from 'AppRouting';
@@ -8,7 +8,8 @@ import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async'; import { loadLocaleAsync } from 'i18n/i18n-util.async';
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors'; import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors';
const availableLocales = [ // Memoize available locales to prevent recreation on every render
const AVAILABLE_LOCALES = [
'de', 'de',
'en', 'en',
'it', 'it',
@@ -20,47 +21,59 @@ const availableLocales = [
'sv', 'sv',
'tr', 'tr',
'cz' 'cz'
]; ] as Locales[];
const App = () => { const App = memo(() => {
const [wasLoaded, setWasLoaded] = useState(false); const [wasLoaded, setWasLoaded] = useState(false);
const [locale, setLocale] = useState<Locales>('en'); const [locale, setLocale] = useState<Locales>('en');
useEffect(() => { // Memoize locale initialization to prevent unnecessary re-runs
// determine locale, take from session if set other default to browser language const initializeLocale = useCallback(async () => {
const browserLocale = detectLocale('en', availableLocales, navigatorDetector); const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales; const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
localStorage.setItem('lang', newLocale); localStorage.setItem('lang', newLocale);
setLocale(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; if (!wasLoaded) return null;
return ( return (
<TypesafeI18n locale={locale}> <TypesafeI18n locale={locale}>
<CustomTheme> <CustomTheme>
<AppRouting /> <AppRouting />
<ToastContainer <ToastContainer {...toastContainerProps} />
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'
}}
/>
</CustomTheme> </CustomTheme>
</TypesafeI18n> </TypesafeI18n>
); );
}; });
export default App; export default App;

View File

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

View File

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

View File

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

View File

@@ -19,9 +19,10 @@ import {
Grid, Grid,
IconButton, IconButton,
Link, Link,
List, Table,
ListItem, TableBody,
ListItemText, TableCell,
TableRow,
Typography Typography
} from '@mui/material'; } from '@mui/material';
@@ -41,6 +42,7 @@ import {
} from 'components'; } from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { TranslationFunctions } from 'i18n/i18n-types';
import { prettyDateTime } from 'utils/time'; import { prettyDateTime } from 'utils/time';
// Constants moved outside component to avoid recreation // Constants moved outside component to avoid recreation
@@ -86,7 +88,7 @@ const VersionInfoDialog = memo(
latestVersion?: VersionInfo; latestVersion?: VersionInfo;
latestDevVersion?: VersionInfo; latestDevVersion?: VersionInfo;
locale: string; locale: string;
LL: any; LL: TranslationFunctions;
onClose: () => void; onClose: () => void;
}) => { }) => {
if (showVersionInfo === 0) return null; if (showVersionInfo === 0) return null;
@@ -97,30 +99,69 @@ const VersionInfoDialog = memo(
return ( return (
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}> <Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
<DialogTitle>Version Information</DialogTitle> <DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<List dense> <Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
<ListItem> <TableBody>
<ListItemText <TableRow sx={{ height: 24, borderBottom: 'none' }}>
primary={<span style={{ color: 'lightblue' }}>{LL.TYPE(0)}</span>} <TableCell
secondary={isStable ? LL.STABLE() : LL.DEVELOPMENT()} component="th"
/> scope="row"
</ListItem> sx={{
<ListItem> color: 'lightblue',
<ListItemText borderBottom: 'none',
primary={<span style={{ color: 'lightblue' }}>{LL.VERSION()}</span>} pr: 1,
secondary={version?.name} py: 0.5,
/> fontSize: 13,
</ListItem> width: 90
{version?.published_at && ( }}
<ListItem> >
<ListItemText {LL.TYPE(0)}
primary={<span style={{ color: 'lightblue' }}>Release Date</span>} </TableCell>
secondary={prettyDateTime(locale, new Date(version.published_at))} <TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
/> {isStable ? LL.STABLE() : LL.DEVELOPMENT()}
</ListItem> </TableCell>
)} </TableRow>
</List> <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> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
@@ -159,7 +200,7 @@ const InstallDialog = memo(
latestDevVersion?: VersionInfo; latestDevVersion?: VersionInfo;
downloadOnly: boolean; downloadOnly: boolean;
platform: string; platform: string;
LL: any; LL: TranslationFunctions;
onClose: () => void; onClose: () => void;
onInstall: (url: string) => 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 type { FC } from 'react';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
@@ -13,22 +13,26 @@ import { LayoutContext } from './context';
export const DRAWER_WIDTH = 210; export const DRAWER_WIDTH = 210;
const Layout: FC<RequiredChildrenProps> = ({ children }) => { const Layout: FC<RequiredChildrenProps> = memo(({ children }) => {
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
const [title, setTitle] = useState(PROJECT_NAME); const [title, setTitle] = useState(PROJECT_NAME);
const { pathname } = useLocation(); const { pathname } = useLocation();
const handleDrawerToggle = () => { // Memoize drawer toggle handler to prevent unnecessary re-renders
setMobileOpen(!mobileOpen); 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 // Memoize context value to prevent unnecessary re-renders
const obj = useMemo(() => ({ title, setTitle }), [title]); const contextValue = useMemo(() => ({ title, setTitle }), [title]);
return ( return (
<LayoutContext.Provider value={obj}> <LayoutContext.Provider value={contextValue}>
<LayoutAppBar title={title} onToggleDrawer={handleDrawerToggle} /> <LayoutAppBar title={title} onToggleDrawer={handleDrawerToggle} />
<LayoutDrawer mobileOpen={mobileOpen} onClose={handleDrawerToggle} /> <LayoutDrawer mobileOpen={mobileOpen} onClose={handleDrawerToggle} />
<Box component="main" sx={{ marginLeft: { md: `${DRAWER_WIDTH}px` } }}> <Box component="main" sx={{ marginLeft: { md: `${DRAWER_WIDTH}px` } }}>
@@ -37,6 +41,6 @@ const Layout: FC<RequiredChildrenProps> = ({ children }) => {
</Box> </Box>
</LayoutContext.Provider> </LayoutContext.Provider>
); );
}; });
export default Layout; export default Layout;

View File

@@ -73,19 +73,6 @@ const LayoutMenu = () => {
> >
<ListItemText <ListItemText
primary={LL.MODULES()} 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 }} sx={{ my: 0 }}
slotProps={{ slotProps={{
primary: { primary: {

View File

@@ -28,11 +28,47 @@ const LayoutMenuItem = ({
to={to} to={to}
disabled={disabled || false} disabled={disabled || false}
selected={selected} 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 /> <Icon />
</ListItemIcon> </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} {label}
</ListItemText> </ListItemText>
</ListItemButton> </ListItemButton>

View File

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

View File

@@ -352,7 +352,8 @@ const cz: Translation = {
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ sestavení', RELEASE_TYPE: 'Typ sestavení',
INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení', 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; export default cz;

View File

@@ -352,7 +352,8 @@ const de: Translation = {
PLATFORM: 'Plattform', PLATFORM: 'Plattform',
RELEASE_TYPE: 'Release Typ', RELEASE_TYPE: 'Release Typ',
INTERNET_CONNECTION_REQUIRED: 'Für die automatische Versionsprüfung und Aktualisierung ist eine Internetverbindung erforderlich', 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; export default de;

View File

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

View File

@@ -352,7 +352,8 @@ const fr: Translation = {
PLATFORM: 'Plateforme', PLATFORM: 'Plateforme',
RELEASE_TYPE: 'Type de version', RELEASE_TYPE: 'Type de version',
INTERNET_CONNECTION_REQUIRED: 'Connexion Internet requise pour la vérification automatique des versions et la mise à niveau', 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; export default fr;

View File

@@ -352,7 +352,8 @@ const it: Translation = {
PLATFORM: 'Piattaforma', PLATFORM: 'Piattaforma',
RELEASE_TYPE: 'Tipo di rilascio', RELEASE_TYPE: 'Tipo di rilascio',
INTERNET_CONNECTION_REQUIRED: 'Connessione internet richiesta per il controllo automatico delle versioni e l\'aggiornamento', 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; export default it;

View File

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

View File

@@ -352,7 +352,8 @@ const no: Translation = {
PLATFORM: 'Plattform', PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelses type', RELEASE_TYPE: 'Utgivelses type',
INTERNET_CONNECTION_REQUIRED: 'Internettilkobling kreves for automatisk versjonskontroll og oppgradering', 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; export default no;

View File

@@ -352,7 +352,8 @@ const pl: BaseTranslation = {
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ wydania', RELEASE_TYPE: 'Typ wydania',
INTERNET_CONNECTION_REQUIRED: 'Połączenie internetowe jest wymagane do automatycznej kontroli wersji i aktualizacji', 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; export default pl;

View File

@@ -352,7 +352,8 @@ const sk: Translation = {
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ vydania', RELEASE_TYPE: 'Typ vydania',
INTERNET_CONNECTION_REQUIRED: 'Internetové pripojenie je potrebné pre automatickú kontrolu a aktualizáciu', 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; export default sk;

View File

@@ -352,7 +352,8 @@ const sv: Translation = {
PLATFORM: 'Plattform', PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelsestyp', RELEASE_TYPE: 'Utgivelsestyp',
INTERNET_CONNECTION_REQUIRED: 'Internetanslutning krävs för automatisk version kontroll och uppdatering', 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; export default sv;

View File

@@ -352,7 +352,8 @@ const tr: Translation = {
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Sürüm Tipi', RELEASE_TYPE: 'Sürüm Tipi',
INTERNET_CONNECTION_REQUIRED: 'Otomatik sürüm kontrolü ve güncelleme için internet bağlantısı gereklidir', 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; export default tr;

View File

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

View File

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

View File

@@ -281,11 +281,12 @@ export default defineConfig(
}, },
rollupOptions: { rollupOptions: {
// Enable tree shaking // Enable aggressive tree shaking
treeshake: { treeshake: {
moduleSideEffects: false, moduleSideEffects: false,
propertyReadSideEffects: false, propertyReadSideEffects: false,
tryCatchDeoptimization: false tryCatchDeoptimization: false,
unknownGlobalSideEffects: false
}, },
output: { output: {
// Optimize chunk naming for better caching // Optimize chunk naming for better caching
@@ -301,6 +302,24 @@ export default defineConfig(
if (id.includes('preact')) { if (id.includes('preact')) {
return '@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')) { if (id.includes('uuid')) {
return '@uuid'; return '@uuid';
} }
@@ -316,8 +335,14 @@ export default defineConfig(
if (id.includes('components/')) { if (id.includes('components/')) {
return 'components'; return 'components';
} }
if (id.includes('pages/') || id.includes('routes/')) { if (id.includes('app/')) {
return 'pages'; return 'app';
}
if (id.includes('utils/')) {
return 'utils';
}
if (id.includes('api/')) {
return 'api';
} }
return undefined; return undefined;
}, },

View File

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

View File

@@ -114,7 +114,9 @@ lib_deps =
[env:build_webUI] [env:build_webUI]
platform = native platform = native
targets = build 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 ; Builds for different board types

View File

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