mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
Merge pull request #2685 from proddy/dev
small changes to make web tables prettier and consistent
This commit is contained in:
12
README.md
12
README.md
@@ -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**
|
||||||
|
|
||||||
|
|||||||
8
interface/pnpm-lock.yaml
generated
8
interface/pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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} />
|
||||||
{showType(di.n, di.t)}
|
{showType(di.n, di.t)}
|
||||||
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
<span style={{ color: 'lightblue' }}> ({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
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
{device.n}
|
{device.n}
|
||||||
<span style={{ color: 'lightblue' }}>
|
<span style={{ color: 'lightblue' }}>
|
||||||
({device.e})
|
({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)}
|
<>
|
||||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
{dv.id.slice(2)}
|
||||||
<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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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": ".",
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
8
mock-api/pnpm-lock.yaml
generated
8
mock-api/pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -109,12 +109,14 @@ def build_webUI(*args, **kwargs):
|
|||||||
print("Web interface build failed!")
|
print("Web interface build failed!")
|
||||||
env.Exit(1)
|
env.Exit(1)
|
||||||
env.Exit(0)
|
env.Exit(0)
|
||||||
|
|
||||||
# Create custom target that only runs the script and then exits, without continuing with the pio workflow
|
# Create custom target that only runs the script and then exits, without continuing with the pio workflow
|
||||||
env.AddCustomTarget(
|
env.AddCustomTarget(
|
||||||
name="build",
|
name="build",
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
version_file = Path('./src/emsesp_version.h')
|
||||||
|
if not version_file.exists():
|
||||||
|
print("Error: emsesp_version.h not found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
app_version = None
|
||||||
|
version_pattern = re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"')
|
||||||
|
|
||||||
|
with version_file.open('r') as f:
|
||||||
|
for line in f:
|
||||||
|
match = version_pattern.match(line)
|
||||||
|
if match:
|
||||||
|
app_version = match.group(1)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not app_version:
|
||||||
|
print("Error: Could not find EMSESP_APP_VERSION in emsesp_version.h!")
|
||||||
|
return
|
||||||
|
|
||||||
# get the application version from emsesp_version.h
|
# Get the chip type, in uppercase
|
||||||
bag = {}
|
mcu = env.get('BOARD_MCU', '').upper()
|
||||||
exprs = [(re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"'), 'app_version')]
|
if not mcu:
|
||||||
with open('./src/emsesp_version.h', 'r') as f:
|
print("Error: Could not determine MCU type!")
|
||||||
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')
|
# 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
|
||||||
|
|
||||||
|
# If it has an M at the end, use it
|
||||||
|
if parts[index].endswith("M"):
|
||||||
|
flash_mem = parts[index] + "B"
|
||||||
|
|
||||||
# print(env.Dump())
|
# Check if BOARD_HAS_PSRAM is in the cppdefines
|
||||||
|
cppdefines = env.get("CPPDEFINES", [])
|
||||||
|
psram = 'BOARD_HAS_PSRAM' in cppdefines
|
||||||
|
|
||||||
# get the chip type, in uppercase
|
print("=" * 90)
|
||||||
mcu = env.get('BOARD_MCU').upper()
|
print(f"EMS-ESP version: {app_version}")
|
||||||
# alternatively take platform from the pio target
|
print(f"Has PSRAM: {'Yes' if psram else 'No'}")
|
||||||
# platform = str(target[0]).split(os.path.sep)[2]
|
print(f"MCU: {mcu}")
|
||||||
|
print(f"Flash Mem: {flash_mem}")
|
||||||
|
|
||||||
# work out the flash memory from the PIO env name (sloppy but works)
|
# Convert . to _ so Windows doesn't complain
|
||||||
# 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
|
|
||||||
|
|
||||||
# if doesn't have an M at the end
|
|
||||||
if parts[index].endswith("M"):
|
|
||||||
flash_mem = parts[index] + "B"
|
|
||||||
|
|
||||||
# find if BOARD_HAS_PSRAM is in the cppdefines
|
|
||||||
cppdefines = env.get("CPPDEFINES")
|
|
||||||
if 'BOARD_HAS_PSRAM' in cppdefines:
|
|
||||||
psram = True
|
|
||||||
else:
|
|
||||||
psram = False
|
|
||||||
|
|
||||||
print("*********************************************")
|
|
||||||
print("EMS-ESP version: " + app_version)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# 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()
|
||||||
|
|
||||||
|
print(f"MD5: {md5_hash}")
|
||||||
|
md5_file.write_text(md5_hash)
|
||||||
|
|
||||||
# copy firmware.bin to firmware/<variant>.bin
|
# Make a copy using the old 3.6.x filename format for backwards compatibility
|
||||||
shutil.copy(str(target[0]), bin_file)
|
# 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
|
||||||
with open(bin_file, "rb") as f:
|
# production of the gateway board. Updating via the WebUI will break the system
|
||||||
result = hashlib.md5(f.read())
|
# and require a manual update.
|
||||||
print("MD5: "+result.hexdigest())
|
|
||||||
file1 = open(md5_file, 'w')
|
pio_env = env.get('PIOENV', '')
|
||||||
file1.write(result.hexdigest())
|
extra_variant = None
|
||||||
file1.close()
|
|
||||||
|
if pio_env == "s3_16M_P":
|
||||||
# make a copy using the old 3.6.x filename format for backwards compatibility with the WebUI version check, e.g.
|
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32_S3"
|
||||||
# create a EMS-ESP-<version>-ESP32_S3.bin if target is s3_16M_P (16MB, PSRAM)
|
elif pio_env == "s_4M":
|
||||||
# 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
|
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32"
|
||||||
#
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
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)
|
# Remove existing files if they exist
|
||||||
|
for file_path in [extra_bin_file, extra_md5_file]:
|
||||||
|
if file_path.exists():
|
||||||
|
file_path.unlink()
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
extra_md5_file = "{}firmware{}{}.md5".format(
|
print("=" * 90)
|
||||||
OUTPUT_DIR, os.path.sep, extra_variant)
|
|
||||||
if os.path.isfile(extra_md5_file):
|
|
||||||
os.remove(extra_md5_file)
|
|
||||||
|
|
||||||
shutil.copy(bin_file, extra_bin_file)
|
|
||||||
shutil.copy(md5_file, extra_md5_file)
|
|
||||||
print("Filename copy for 3.6.x: "+extra_bin_file)
|
|
||||||
|
|
||||||
print("*********************************************")
|
|
||||||
|
|
||||||
|
|
||||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_copy])
|
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_copy])
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define EMSESP_APP_VERSION "3.7.3-dev.22"
|
#define EMSESP_APP_VERSION "3.7.3-dev.23"
|
||||||
|
|||||||
Reference in New Issue
Block a user