mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Compare commits
23 Commits
3ac7d43a81
...
b5ac637231
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5ac637231 | ||
|
|
4e0ef8b0e3 | ||
|
|
c32ee4dfb5 | ||
|
|
accfeab6fa | ||
|
|
d98f3cc8c5 | ||
|
|
342f238983 | ||
|
|
73f8ea0fc5 | ||
|
|
f2bd1ff575 | ||
|
|
b3ec23d6bd | ||
|
|
1fc7fa4720 | ||
|
|
58ae058465 | ||
|
|
7ece395d1b | ||
|
|
4bb876031e | ||
|
|
e4f129db04 | ||
|
|
90038e08dc | ||
|
|
4c30e930cf | ||
|
|
dd69e02f6b | ||
|
|
0e133840c9 | ||
|
|
e12b277472 | ||
|
|
dc7b1809c1 | ||
|
|
8c2146ff55 | ||
|
|
882d412409 | ||
|
|
3f88a9469c |
@@ -7,6 +7,8 @@ on:
|
||||
jobs:
|
||||
github-releases-to-discord:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
2
.github/workflows/pr_check.yml
vendored
2
.github/workflows/pr_check.yml
vendored
@@ -1,4 +1,6 @@
|
||||
name: 'Pre-check on PR'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
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).
|
||||
|
||||
If you find an issue or have a request, see [here](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
|
||||
If you find an issue or have a request, see [how to request support](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
|
||||
|
||||
## 🎥 **Live Demo**
|
||||
|
||||
@@ -82,13 +82,19 @@ EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned an
|
||||
|
||||
If you like **EMS-ESP**, please give it a ✨ on GitHub, or even better fork it and contribute. You can also offer a small donation. This is an open-source project maintained by volunteers, and your support is greatly appreciated.
|
||||
|
||||
## 📦 **Building**
|
||||
|
||||
To build the web interface only, run `platformio run -e build_webUI`. This will install the necessary dependencies and build the web interface and also create the embedded code used need to build the firmware. You can run the web interface locally by going to the `interface` directory and running `pnpm standalone`.
|
||||
|
||||
To build the firmware, run `platformio run`. This will build the firmware for all ESP32 modules and place the binaries in the `build/firmware` folder. If you want to configure the build for a single platform create a local `pio_local.ni` file in the root directory (see example in `pio_local.ini_example`).
|
||||
|
||||
## 📢 **Libraries used**
|
||||
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the core framework that provides the Web UI, which has been heavily modified
|
||||
- [esp8266-react](https://github.com/rjwats/esp8266-react) originally by @rjwats for the core framework that provides the Web UI, which has been heavily modified
|
||||
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these awesome open source libraries
|
||||
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing
|
||||
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client
|
||||
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server and TCP backends
|
||||
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server
|
||||
|
||||
## 📜 **License**
|
||||
|
||||
|
||||
8
interface/pnpm-lock.yaml
generated
8
interface/pnpm-lock.yaml
generated
@@ -635,8 +635,8 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
||||
|
||||
'@popperjs/core@2.11.8':
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
@@ -3565,7 +3565,7 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.19.1
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
@@ -4714,7 +4714,7 @@ snapshots:
|
||||
|
||||
formidable@3.5.4:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2': 2.2.2
|
||||
'@paralleldrive/cuid2': 2.3.1
|
||||
dezalgo: 1.0.4
|
||||
once: 1.4.0
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ToastContainer, Zoom } from 'react-toastify';
|
||||
|
||||
import AppRouting from 'AppRouting';
|
||||
@@ -8,7 +8,8 @@ import type { Locales } from 'i18n/i18n-types';
|
||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors';
|
||||
|
||||
const availableLocales = [
|
||||
// Memoize available locales to prevent recreation on every render
|
||||
const AVAILABLE_LOCALES = [
|
||||
'de',
|
||||
'en',
|
||||
'it',
|
||||
@@ -20,47 +21,59 @@ const availableLocales = [
|
||||
'sv',
|
||||
'tr',
|
||||
'cz'
|
||||
];
|
||||
] as Locales[];
|
||||
|
||||
const App = () => {
|
||||
const App = memo(() => {
|
||||
const [wasLoaded, setWasLoaded] = useState(false);
|
||||
const [locale, setLocale] = useState<Locales>('en');
|
||||
|
||||
useEffect(() => {
|
||||
// determine locale, take from session if set other default to browser language
|
||||
const browserLocale = detectLocale('en', availableLocales, navigatorDetector);
|
||||
// Memoize locale initialization to prevent unnecessary re-runs
|
||||
const initializeLocale = useCallback(async () => {
|
||||
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
|
||||
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
|
||||
localStorage.setItem('lang', newLocale);
|
||||
setLocale(newLocale);
|
||||
void loadLocaleAsync(newLocale).then(() => setWasLoaded(true));
|
||||
await loadLocaleAsync(newLocale);
|
||||
setWasLoaded(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void initializeLocale();
|
||||
}, [initializeLocale]);
|
||||
|
||||
// Memoize toast container props to prevent recreation
|
||||
const toastContainerProps = useMemo(
|
||||
() => ({
|
||||
position: 'bottom-left' as const,
|
||||
autoClose: 3000,
|
||||
hideProgressBar: false,
|
||||
newestOnTop: false,
|
||||
closeOnClick: true,
|
||||
rtl: false,
|
||||
pauseOnFocusLoss: true,
|
||||
draggable: false,
|
||||
pauseOnHover: false,
|
||||
transition: Zoom,
|
||||
closeButton: false,
|
||||
theme: 'dark' as const,
|
||||
toastStyle: {
|
||||
border: '1px solid #177ac9',
|
||||
width: 'fit-content'
|
||||
}
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
if (!wasLoaded) return null;
|
||||
|
||||
return (
|
||||
<TypesafeI18n locale={locale}>
|
||||
<CustomTheme>
|
||||
<AppRouting />
|
||||
<ToastContainer
|
||||
position="bottom-left"
|
||||
autoClose={3000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable={false}
|
||||
pauseOnHover={false}
|
||||
transition={Zoom}
|
||||
closeButton={false}
|
||||
theme="dark"
|
||||
toastStyle={{
|
||||
border: '1px solid #177ac9'
|
||||
}}
|
||||
/>
|
||||
<ToastContainer {...toastContainerProps} />
|
||||
</CustomTheme>
|
||||
</TypesafeI18n>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { CssBaseline, ThemeProvider, responsiveFontSizes } from '@mui/material';
|
||||
@@ -5,6 +6,7 @@ import { createTheme } from '@mui/material/styles';
|
||||
|
||||
import type { RequiredChildrenProps } from 'utils';
|
||||
|
||||
// Memoize dialog style to prevent recreation
|
||||
export const dialogStyle = {
|
||||
'& .MuiDialog-paper': {
|
||||
borderRadius: '8px',
|
||||
@@ -12,8 +14,9 @@ export const dialogStyle = {
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px'
|
||||
}
|
||||
};
|
||||
} as const;
|
||||
|
||||
// Memoize theme creation to prevent recreation
|
||||
const theme = responsiveFontSizes(
|
||||
createTheme({
|
||||
typography: {
|
||||
@@ -30,15 +33,27 @@ const theme = responsiveFontSizes(
|
||||
text: {
|
||||
disabled: '#eee' // white
|
||||
}
|
||||
},
|
||||
components: {
|
||||
MuiListItemText: {
|
||||
styleOverrides: {
|
||||
primary: {
|
||||
fontSize: 14
|
||||
},
|
||||
secondary: {
|
||||
color: '#9e9e9e' // grey[500]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const CustomTheme: FC<RequiredChildrenProps> = ({ children }) => (
|
||||
const CustomTheme: FC<RequiredChildrenProps> = memo(({ children }) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
));
|
||||
|
||||
export default CustomTheme;
|
||||
|
||||
@@ -206,7 +206,7 @@ const Dashboard = memo(() => {
|
||||
// if its a device (parent node) and has entities
|
||||
if (di.nodes?.length) {
|
||||
return (
|
||||
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>
|
||||
<span style={{ fontSize: '15px' }}>
|
||||
<DeviceIcon type_id={di.t ?? 0} />
|
||||
{showType(di.n, di.t)}
|
||||
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||
@@ -312,7 +312,7 @@ const Dashboard = memo(() => {
|
||||
flexDirection="column"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
border: '1px solid grey'
|
||||
border: '1px solid rgb(65, 65, 65)'
|
||||
}}
|
||||
>
|
||||
<IconContext.Provider
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import { IconContext } from 'react-icons';
|
||||
@@ -75,7 +77,7 @@ import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
|
||||
import type { Device, DeviceValue } from './types';
|
||||
import { deviceValueItemValidation } from './validators';
|
||||
|
||||
const Devices = () => {
|
||||
const Devices = memo(() => {
|
||||
const { LL } = useI18nContext();
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
|
||||
@@ -141,11 +143,13 @@ const Devices = () => {
|
||||
return left + (right - left < 400 ? 0 : 200);
|
||||
};
|
||||
|
||||
const common_theme = useTheme({
|
||||
BaseRow: `
|
||||
const common_theme = useMemo(
|
||||
() =>
|
||||
useTheme({
|
||||
BaseRow: `
|
||||
font-size: 14px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
HeaderRow: `
|
||||
text-transform: uppercase;
|
||||
background-color: black;
|
||||
color: #90CAF9;
|
||||
@@ -153,7 +157,7 @@ const Devices = () => {
|
||||
border-bottom: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
Row: `
|
||||
cursor: pointer;
|
||||
background-color: #1E1E1E;
|
||||
.td {
|
||||
@@ -163,30 +167,47 @@ const Devices = () => {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
});
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const device_theme = useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
const device_theme = useMemo(
|
||||
() =>
|
||||
useTheme([
|
||||
common_theme,
|
||||
{
|
||||
BaseRow: `
|
||||
font-size: 15px;
|
||||
.td {
|
||||
height: 28px;
|
||||
}
|
||||
`,
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
|
||||
`,
|
||||
HeaderRow: `
|
||||
HeaderRow: `
|
||||
.th {
|
||||
padding: 8px;
|
||||
`,
|
||||
Row: `
|
||||
font-weight: bold;
|
||||
Row: `
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
},
|
||||
&:hover .td {
|
||||
background-color: #177ac9;
|
||||
},
|
||||
`
|
||||
}
|
||||
]);
|
||||
}
|
||||
]),
|
||||
[common_theme]
|
||||
);
|
||||
|
||||
const data_theme = useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
const data_theme = useMemo(
|
||||
() =>
|
||||
useTheme([
|
||||
common_theme,
|
||||
{
|
||||
Table: `
|
||||
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
@@ -195,12 +216,12 @@ const Devices = () => {
|
||||
display:none;
|
||||
}
|
||||
`,
|
||||
BaseRow: `
|
||||
BaseRow: `
|
||||
.td {
|
||||
height: 32px;
|
||||
}
|
||||
`,
|
||||
BaseCell: `
|
||||
BaseCell: `
|
||||
&:nth-of-type(1) {
|
||||
border-left: 1px solid #177ac9;
|
||||
},
|
||||
@@ -211,12 +232,12 @@ const Devices = () => {
|
||||
border-right: 1px solid #177ac9;
|
||||
}
|
||||
`,
|
||||
HeaderRow: `
|
||||
HeaderRow: `
|
||||
.th {
|
||||
border-top: 1px solid #565656;
|
||||
}
|
||||
`,
|
||||
Row: `
|
||||
Row: `
|
||||
&:nth-of-type(odd) .td {
|
||||
background-color: #303030;
|
||||
},
|
||||
@@ -224,8 +245,10 @@ const Devices = () => {
|
||||
background-color: #177ac9;
|
||||
}
|
||||
`
|
||||
}
|
||||
]);
|
||||
}
|
||||
]),
|
||||
[common_theme]
|
||||
);
|
||||
|
||||
const getSortIcon = (state: State, sortKey: unknown) => {
|
||||
if (state.sortKey === sortKey && state.reverse) {
|
||||
@@ -324,8 +347,10 @@ const Devices = () => {
|
||||
return sc;
|
||||
};
|
||||
|
||||
const hasMask = (id: string, mask: number) =>
|
||||
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
||||
const hasMask = useCallback(
|
||||
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDownloadCsv = () => {
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
@@ -510,55 +535,65 @@ const Devices = () => {
|
||||
|
||||
const renderCoreData = () => (
|
||||
<>
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
color: 'lightblue',
|
||||
size: '18',
|
||||
style: { verticalAlign: 'middle' }
|
||||
<Box
|
||||
padding={1}
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
border: '1px solid rgb(65, 65, 65)'
|
||||
}}
|
||||
>
|
||||
{!coreData.connected && (
|
||||
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
|
||||
)}
|
||||
<IconContext.Provider
|
||||
value={{
|
||||
color: 'lightblue',
|
||||
size: '18',
|
||||
style: { verticalAlign: 'middle' }
|
||||
}}
|
||||
>
|
||||
{!coreData.connected && (
|
||||
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
|
||||
)}
|
||||
|
||||
{coreData.connected && (
|
||||
<Table
|
||||
data={{ nodes: coreData.devices }}
|
||||
select={device_select}
|
||||
theme={device_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: Device[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.length === 0 && (
|
||||
<CircularProgress sx={{ margin: 1 }} size={18} />
|
||||
)}
|
||||
{tableList.map((device: Device) => (
|
||||
<Row key={device.id} item={device}>
|
||||
<Cell>
|
||||
<DeviceIcon type_id={device.t} />
|
||||
|
||||
{device.n}
|
||||
<span style={{ color: 'lightblue' }}>
|
||||
({device.e})
|
||||
</span>
|
||||
</Cell>
|
||||
<Cell stiff>{device.tn}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
{coreData.connected && (
|
||||
<Table
|
||||
data={{ nodes: coreData.devices }}
|
||||
select={device_select}
|
||||
theme={device_theme}
|
||||
layout={{ custom: true }}
|
||||
>
|
||||
{(tableList: Device[]) => (
|
||||
<>
|
||||
<Header>
|
||||
<HeaderRow>
|
||||
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.TYPE(0)}</HeaderCell>
|
||||
</HeaderRow>
|
||||
</Header>
|
||||
<Body>
|
||||
{tableList.length === 0 && (
|
||||
<CircularProgress sx={{ margin: 1 }} size={18} />
|
||||
)}
|
||||
{tableList.map((device: Device) => (
|
||||
<Row key={device.id} item={device}>
|
||||
<Cell>
|
||||
<DeviceIcon type_id={device.t} />
|
||||
|
||||
{device.n}
|
||||
<span style={{ color: 'lightblue' }}>
|
||||
({device.e})
|
||||
</span>
|
||||
</Cell>
|
||||
<Cell stiff>{device.tn}</Cell>
|
||||
</Row>
|
||||
))}
|
||||
</Body>
|
||||
</>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</IconContext.Provider>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -574,35 +609,41 @@ const Devices = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const showDeviceValue = (dv: DeviceValue) => {
|
||||
const showDeviceValue = useCallback((dv: DeviceValue) => {
|
||||
setSelectedDeviceValue(dv);
|
||||
setDeviceValueDialogOpen(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderNameCell = (dv: DeviceValue) => (
|
||||
<>
|
||||
{dv.id.slice(2)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
||||
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
</>
|
||||
const renderNameCell = useCallback(
|
||||
(dv: DeviceValue) => (
|
||||
<>
|
||||
{dv.id.slice(2)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
|
||||
<StarIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
|
||||
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
|
||||
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[hasMask]
|
||||
);
|
||||
|
||||
const shown_data = onlyFav
|
||||
? deviceData.nodes.filter(
|
||||
const shown_data = useMemo(() => {
|
||||
if (onlyFav) {
|
||||
return deviceData.nodes.filter(
|
||||
(dv: DeviceValue) =>
|
||||
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
|
||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: deviceData.nodes.filter((dv: DeviceValue) =>
|
||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}
|
||||
return deviceData.nodes.filter((dv: DeviceValue) =>
|
||||
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}, [deviceData.nodes, onlyFav, search]);
|
||||
|
||||
const deviceIndex = coreData.devices.findIndex(
|
||||
(d: Device) => d.id === device_select.state.id
|
||||
@@ -795,6 +836,6 @@ const Devices = () => {
|
||||
)}
|
||||
</SectionContent>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default Devices;
|
||||
|
||||
@@ -19,9 +19,10 @@ import {
|
||||
Grid,
|
||||
IconButton,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
} from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { TranslationFunctions } from 'i18n/i18n-types';
|
||||
import { prettyDateTime } from 'utils/time';
|
||||
|
||||
// Constants moved outside component to avoid recreation
|
||||
@@ -86,7 +88,7 @@ const VersionInfoDialog = memo(
|
||||
latestVersion?: VersionInfo;
|
||||
latestDevVersion?: VersionInfo;
|
||||
locale: string;
|
||||
LL: any;
|
||||
LL: TranslationFunctions;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
if (showVersionInfo === 0) return null;
|
||||
@@ -97,30 +99,69 @@ const VersionInfoDialog = memo(
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
|
||||
<DialogTitle>Version Information</DialogTitle>
|
||||
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={<span style={{ color: 'lightblue' }}>{LL.TYPE(0)}</span>}
|
||||
secondary={isStable ? LL.STABLE() : LL.DEVELOPMENT()}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={<span style={{ color: 'lightblue' }}>{LL.VERSION()}</span>}
|
||||
secondary={version?.name}
|
||||
/>
|
||||
</ListItem>
|
||||
{version?.published_at && (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={<span style={{ color: 'lightblue' }}>Release Date</span>}
|
||||
secondary={prettyDateTime(locale, new Date(version.published_at))}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
|
||||
<TableBody>
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13,
|
||||
width: 90
|
||||
}}
|
||||
>
|
||||
{LL.TYPE(0)}
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{isStable ? LL.STABLE() : LL.DEVELOPMENT()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13
|
||||
}}
|
||||
>
|
||||
{LL.VERSION()}
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{version?.name}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{version?.published_at && (
|
||||
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
sx={{
|
||||
color: 'lightblue',
|
||||
borderBottom: 'none',
|
||||
pr: 1,
|
||||
py: 0.5,
|
||||
fontSize: 13
|
||||
}}
|
||||
>
|
||||
Build Date
|
||||
</TableCell>
|
||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
||||
{prettyDateTime(locale, new Date(version.published_at))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
@@ -159,7 +200,7 @@ const InstallDialog = memo(
|
||||
latestDevVersion?: VersionInfo;
|
||||
downloadOnly: boolean;
|
||||
platform: string;
|
||||
LL: any;
|
||||
LL: TranslationFunctions;
|
||||
onClose: () => void;
|
||||
onInstall: (url: string) => void;
|
||||
}) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
@@ -13,22 +13,26 @@ import { LayoutContext } from './context';
|
||||
|
||||
export const DRAWER_WIDTH = 210;
|
||||
|
||||
const Layout: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
const Layout: FC<RequiredChildrenProps> = memo(({ children }) => {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [title, setTitle] = useState(PROJECT_NAME);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
// Memoize drawer toggle handler to prevent unnecessary re-renders
|
||||
const handleDrawerToggle = useCallback(() => {
|
||||
setMobileOpen((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
useEffect(() => setMobileOpen(false), [pathname]);
|
||||
// Close drawer when route changes
|
||||
useEffect(() => {
|
||||
setMobileOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
// cache the object to prevent unnecessary re-renders
|
||||
const obj = useMemo(() => ({ title, setTitle }), [title]);
|
||||
// Memoize context value to prevent unnecessary re-renders
|
||||
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={obj}>
|
||||
<LayoutContext.Provider value={contextValue}>
|
||||
<LayoutAppBar title={title} onToggleDrawer={handleDrawerToggle} />
|
||||
<LayoutDrawer mobileOpen={mobileOpen} onClose={handleDrawerToggle} />
|
||||
<Box component="main" sx={{ marginLeft: { md: `${DRAWER_WIDTH}px` } }}>
|
||||
@@ -37,6 +41,6 @@ const Layout: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
</Box>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -73,19 +73,6 @@ const LayoutMenu = () => {
|
||||
>
|
||||
<ListItemText
|
||||
primary={LL.MODULES()}
|
||||
// secondary={
|
||||
// LL.CUSTOMIZATIONS() +
|
||||
// ', ' +
|
||||
// LL.SCHEDULER() +
|
||||
// ', ' +
|
||||
// LL.CUSTOM_ENTITIES(0) +
|
||||
// '...'
|
||||
// }
|
||||
// secondaryTypographyProps={{
|
||||
// noWrap: true,
|
||||
// fontSize: 12,
|
||||
// color: menuOpen ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0.5)'
|
||||
// }}
|
||||
sx={{ my: 0 }}
|
||||
slotProps={{
|
||||
primary: {
|
||||
|
||||
@@ -28,11 +28,47 @@ const LayoutMenuItem = ({
|
||||
to={to}
|
||||
disabled={disabled || false}
|
||||
selected={selected}
|
||||
sx={{
|
||||
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
transform: selected ? 'scale(1.02)' : 'scale(1)',
|
||||
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
|
||||
borderRadius: '8px',
|
||||
margin: '2px 8px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(68, 82, 211, 0.39)',
|
||||
transform: selected ? 'scale(1.02)' : 'scale(1.01)'
|
||||
},
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: selected ? '4px' : '0px',
|
||||
backgroundColor: '#90caf9',
|
||||
borderRadius: '0 2px 2px 0',
|
||||
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: selected ? '#90caf9' : '#9e9e9e',
|
||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
transform: selected ? 'scale(1.1)' : 'scale(1)',
|
||||
transitionProperty: 'color, transform'
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
</ListItemIcon>
|
||||
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>
|
||||
<ListItemText
|
||||
sx={{
|
||||
color: selected ? '#90caf9' : '#f5f5f5',
|
||||
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
|
||||
// fontWeight: selected ? '600' : '400',
|
||||
transitionProperty: 'color, font-weight'
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</ListItemText>
|
||||
</ListItemButton>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { Box, CircularProgress } from '@mui/material';
|
||||
|
||||
const LazyLoader = () => (
|
||||
const LazyLoader = memo(() => (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
@@ -15,6 +17,6 @@ const LazyLoader = () => (
|
||||
>
|
||||
<CircularProgress size={40} />
|
||||
</Box>
|
||||
);
|
||||
));
|
||||
|
||||
export default LazyLoader;
|
||||
|
||||
@@ -352,7 +352,8 @@ const cz: Translation = {
|
||||
PLATFORM: 'Platforma',
|
||||
RELEASE_TYPE: 'Typ sestavení',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení',
|
||||
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi'
|
||||
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
|
||||
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru'
|
||||
};
|
||||
|
||||
export default cz;
|
||||
|
||||
@@ -352,7 +352,8 @@ const de: Translation = {
|
||||
PLATFORM: 'Plattform',
|
||||
RELEASE_TYPE: 'Release Typ',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Für die automatische Versionsprüfung und Aktualisierung ist eine Internetverbindung erforderlich',
|
||||
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln'
|
||||
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln',
|
||||
FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation'
|
||||
};
|
||||
|
||||
export default de;
|
||||
|
||||
@@ -180,10 +180,10 @@ const en: Translation = {
|
||||
DEVELOPMENT: 'Development',
|
||||
UPTIME: 'System Uptime',
|
||||
FREE_MEMORY: 'Free Memory',
|
||||
PSRAM: 'PSRAM (Size / Free)',
|
||||
FLASH: 'Flash Chip (Size , Speed)',
|
||||
APPSIZE: 'Application (Partition: Used / Free)',
|
||||
FILESYSTEM: 'File System (Used / Free)',
|
||||
PSRAM: 'PSRAM (size / free)',
|
||||
FLASH: 'Flash Chip (size , speed)',
|
||||
APPSIZE: 'Application (partition: used / free)',
|
||||
FILESYSTEM: 'File System (used / free)',
|
||||
BUFFER_SIZE: 'Max Buffer Size',
|
||||
COMPACT: 'Compact',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Create a backup of your configuration and settings',
|
||||
@@ -352,7 +352,8 @@ const en: Translation = {
|
||||
PLATFORM: 'Platform',
|
||||
RELEASE_TYPE: 'Release Type',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Internet connection required for automatic version checking and upgrading',
|
||||
SWITCH_RELEASE_TYPE: 'Switch to {0} release'
|
||||
SWITCH_RELEASE_TYPE: 'Switch to {0} release',
|
||||
FIRMWARE_VERSION_INFO: 'Firmware Version Information'
|
||||
};
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -352,7 +352,8 @@ const fr: Translation = {
|
||||
PLATFORM: 'Plateforme',
|
||||
RELEASE_TYPE: 'Type de version',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Connexion Internet requise pour la vérification automatique des versions et la mise à niveau',
|
||||
SWITCH_RELEASE_TYPE: 'Passer à la version {0}'
|
||||
SWITCH_RELEASE_TYPE: 'Passer à la version {0}',
|
||||
FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware'
|
||||
};
|
||||
|
||||
export default fr;
|
||||
|
||||
@@ -352,7 +352,8 @@ const it: Translation = {
|
||||
PLATFORM: 'Piattaforma',
|
||||
RELEASE_TYPE: 'Tipo di rilascio',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Connessione internet richiesta per il controllo automatico delle versioni e l\'aggiornamento',
|
||||
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio'
|
||||
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio',
|
||||
FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware'
|
||||
};
|
||||
|
||||
export default it;
|
||||
|
||||
@@ -179,12 +179,12 @@ const nl: Translation = {
|
||||
STABLE: 'Stable',
|
||||
DEVELOPMENT: 'Development',
|
||||
UPTIME: 'Systeem Uptime',
|
||||
FREE_MEMORY: 'Free Memory',
|
||||
PSRAM: 'PSRAM (Size / Free)',
|
||||
FLASH: 'Flash Chip (Size , Speed)',
|
||||
APPSIZE: 'Application (Partition: Used / Free)',
|
||||
FILESYSTEM: 'File System (Used / Free)',
|
||||
BUFFER_SIZE: 'Max Buffer Size',
|
||||
FREE_MEMORY: 'Vrij geheugen',
|
||||
PSRAM: 'PSRAM (grootte / vrij)',
|
||||
FLASH: 'Flash Chip (grootte , snelheid)',
|
||||
APPSIZE: 'Applicatie (partition: gebruikt / vrij)',
|
||||
FILESYSTEM: 'Bestandssysteem (gebruikt / vrij)',
|
||||
BUFFER_SIZE: 'Max buffer grootte',
|
||||
COMPACT: 'Compact',
|
||||
DOWNLOAD_SETTINGS_TEXT: 'Maak een back-up van uw configuratie en instellingen',
|
||||
UPLOAD_TEXT: 'Upload een nieuw firmwarebestand (.bin) of een back-upbestand (.json)',
|
||||
@@ -226,7 +226,7 @@ const nl: Translation = {
|
||||
MQTT_INT_MIXER: 'Mixer Modules',
|
||||
MQTT_INT_WATER: 'Water Modules',
|
||||
MQTT_QUEUE: 'MQTT Queue',
|
||||
DEFAULT: 'Default',
|
||||
DEFAULT: 'Standaard',
|
||||
MQTT_ENTITY_FORMAT: 'Entity ID formaat',
|
||||
MQTT_ENTITY_FORMAT_0: 'Eén instantie, lange naam (v3.4)',
|
||||
MQTT_ENTITY_FORMAT_1: 'Eén instantie, korte naam',
|
||||
@@ -250,20 +250,20 @@ const nl: Translation = {
|
||||
AP_PROVIDE_TEXT_3: 'nooit',
|
||||
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
|
||||
AP_HIDE_SSID: 'SSID verbergen',
|
||||
AP_CLIENTS: 'AP Clients',
|
||||
AP_MAX_CLIENTS: 'Max Clients',
|
||||
AP_LOCAL_IP: 'Local IP',
|
||||
AP_CLIENTS: 'AP Gebruikers',
|
||||
AP_MAX_CLIENTS: 'Max Gebruikers',
|
||||
AP_LOCAL_IP: 'Lokale IP',
|
||||
NETWORK_SCAN: 'Scan WiFi Netwerken',
|
||||
IDLE: 'Idle',
|
||||
IDLE: 'Inactief',
|
||||
LOST: 'Verloren',
|
||||
SCANNING: 'Scannen',
|
||||
SCAN_AGAIN: 'Opnieuw scannen',
|
||||
NETWORK_SCANNER: 'Netwerk Scanner',
|
||||
NETWORK_SCANNER: 'Netwerk Scannen',
|
||||
NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden',
|
||||
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
|
||||
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken',
|
||||
TX_POWER: 'Tx Vermogen',
|
||||
HOSTNAME: 'Hostname',
|
||||
HOSTNAME: 'Hostnaam',
|
||||
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
|
||||
NETWORK_LOW_BAND: 'Lagere WiFi bandbreedte gebruiken',
|
||||
NETWORK_USE_DNS: 'Activeer mDNS Service',
|
||||
@@ -273,9 +273,9 @@ const nl: Translation = {
|
||||
NETWORK_GATEWAY: 'Gateway',
|
||||
NETWORK_SUBNET: 'Subnetmasker',
|
||||
NETWORK_DNS: 'DNS Servers',
|
||||
ADDRESS_OF: '{0} Address',
|
||||
ADMINISTRATOR: 'Administrator',
|
||||
GUEST: 'Gast',
|
||||
ADDRESS_OF: '{0} Adres',
|
||||
ADMINISTRATOR: 'Beheerder',
|
||||
GUEST: 'Bezoeker',
|
||||
NEW: 'Nieuwe',
|
||||
NEW_NAME_OF: 'Hernoem {0}',
|
||||
ENTITY: 'Entiteit',
|
||||
@@ -352,7 +352,8 @@ const nl: Translation = {
|
||||
PLATFORM: 'Platform',
|
||||
RELEASE_TYPE: 'Release Typ',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Internetverbinding vereist voor automatische versiecontrole en -upgrade',
|
||||
SWITCH_RELEASE_TYPE: 'Switch naar {0} release'
|
||||
SWITCH_RELEASE_TYPE: 'Switch naar {0} release',
|
||||
FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie'
|
||||
};
|
||||
|
||||
export default nl;
|
||||
@@ -352,7 +352,8 @@ const no: Translation = {
|
||||
PLATFORM: 'Plattform',
|
||||
RELEASE_TYPE: 'Utgivelses type',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Internettilkobling kreves for automatisk versjonskontroll og oppgradering',
|
||||
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse'
|
||||
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse',
|
||||
FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon'
|
||||
};
|
||||
|
||||
export default no;
|
||||
|
||||
@@ -352,7 +352,8 @@ const pl: BaseTranslation = {
|
||||
PLATFORM: 'Platforma',
|
||||
RELEASE_TYPE: 'Typ wydania',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Połączenie internetowe jest wymagane do automatycznej kontroli wersji i aktualizacji',
|
||||
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie'
|
||||
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie',
|
||||
FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware'
|
||||
};
|
||||
|
||||
export default pl;
|
||||
|
||||
@@ -352,7 +352,8 @@ const sk: Translation = {
|
||||
PLATFORM: 'Platforma',
|
||||
RELEASE_TYPE: 'Typ vydania',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Internetové pripojenie je potrebné pre automatickú kontrolu a aktualizáciu',
|
||||
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu'
|
||||
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu',
|
||||
FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware'
|
||||
};
|
||||
|
||||
export default sk;
|
||||
|
||||
@@ -352,7 +352,8 @@ const sv: Translation = {
|
||||
PLATFORM: 'Plattform',
|
||||
RELEASE_TYPE: 'Utgivelsestyp',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Internetanslutning krävs för automatisk version kontroll och uppdatering',
|
||||
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva'
|
||||
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva',
|
||||
FIRMWARE_VERSION_INFO: 'Information om firmwareversion'
|
||||
};
|
||||
|
||||
export default sv;
|
||||
|
||||
@@ -352,7 +352,8 @@ const tr: Translation = {
|
||||
PLATFORM: 'Platforma',
|
||||
RELEASE_TYPE: 'Sürüm Tipi',
|
||||
INTERNET_CONNECTION_REQUIRED: 'Otomatik sürüm kontrolü ve güncelleme için internet bağlantısı gereklidir',
|
||||
SWITCH_RELEASE_TYPE: '{0} sürümüne geç'
|
||||
SWITCH_RELEASE_TYPE: '{0} sürümüne geç',
|
||||
FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi'
|
||||
};
|
||||
|
||||
export default tr;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useBlocker } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@@ -35,20 +35,26 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
||||
setDirtyFlags([]);
|
||||
});
|
||||
|
||||
const updateDataValue = (new_data: D) => {
|
||||
updateData({ data: new_data });
|
||||
};
|
||||
// Memoize updateDataValue to prevent unnecessary re-renders
|
||||
const updateDataValue = useCallback(
|
||||
(new_data: D) => {
|
||||
updateData({ data: new_data });
|
||||
},
|
||||
[updateData]
|
||||
);
|
||||
|
||||
const loadData = async () => {
|
||||
// Memoize loadData to prevent unnecessary re-renders
|
||||
const loadData = useCallback(async () => {
|
||||
setDirtyFlags([]);
|
||||
setErrorMessage(undefined);
|
||||
await readData().catch((error: Error) => {
|
||||
toast.error(error.message);
|
||||
setErrorMessage(error.message);
|
||||
});
|
||||
};
|
||||
}, [readData]);
|
||||
|
||||
const saveData = async () => {
|
||||
// Memoize saveData to prevent unnecessary re-renders
|
||||
const saveData = useCallback(async () => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
@@ -64,14 +70,15 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [data, writeData]);
|
||||
|
||||
return {
|
||||
loadData,
|
||||
saveData,
|
||||
saving: saving as boolean,
|
||||
updateDataValue,
|
||||
data: data as D, // Explicitly define the type of 'data'
|
||||
origData: origData as D, // Explicitly define the type of 'origData' to 'D'
|
||||
data: data as D,
|
||||
origData: origData as D,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
setOrigData,
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"skipDefaultLibCheck": true,
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": ".tsbuildinfo",
|
||||
"assumeChangesOnlyAffectDirectDependencies": true,
|
||||
|
||||
// Path mapping for cleaner imports
|
||||
"baseUrl": ".",
|
||||
|
||||
@@ -281,11 +281,12 @@ export default defineConfig(
|
||||
},
|
||||
|
||||
rollupOptions: {
|
||||
// Enable tree shaking
|
||||
// Enable aggressive tree shaking
|
||||
treeshake: {
|
||||
moduleSideEffects: false,
|
||||
propertyReadSideEffects: false,
|
||||
tryCatchDeoptimization: false
|
||||
tryCatchDeoptimization: false,
|
||||
unknownGlobalSideEffects: false
|
||||
},
|
||||
output: {
|
||||
// Optimize chunk naming for better caching
|
||||
@@ -301,6 +302,24 @@ export default defineConfig(
|
||||
if (id.includes('preact')) {
|
||||
return '@preact';
|
||||
}
|
||||
if (id.includes('@mui/material')) {
|
||||
return '@mui-material';
|
||||
}
|
||||
if (id.includes('@mui/icons-material')) {
|
||||
return '@mui-icons';
|
||||
}
|
||||
if (id.includes('alova')) {
|
||||
return '@alova';
|
||||
}
|
||||
if (id.includes('typesafe-i18n')) {
|
||||
return '@i18n';
|
||||
}
|
||||
if (id.includes('react-toastify')) {
|
||||
return '@toastify';
|
||||
}
|
||||
if (id.includes('@table-library')) {
|
||||
return '@table-library';
|
||||
}
|
||||
if (id.includes('uuid')) {
|
||||
return '@uuid';
|
||||
}
|
||||
@@ -316,8 +335,14 @@ export default defineConfig(
|
||||
if (id.includes('components/')) {
|
||||
return 'components';
|
||||
}
|
||||
if (id.includes('pages/') || id.includes('routes/')) {
|
||||
return 'pages';
|
||||
if (id.includes('app/')) {
|
||||
return 'app';
|
||||
}
|
||||
if (id.includes('utils/')) {
|
||||
return 'utils';
|
||||
}
|
||||
if (id.includes('api/')) {
|
||||
return 'api';
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
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==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2':
|
||||
resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==}
|
||||
@@ -223,7 +223,7 @@ snapshots:
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
@@ -252,7 +252,7 @@ snapshots:
|
||||
|
||||
formidable@3.5.4:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2': 2.2.2
|
||||
'@paralleldrive/cuid2': 2.3.1
|
||||
dezalgo: 1.0.4
|
||||
once: 1.4.0
|
||||
|
||||
|
||||
@@ -114,7 +114,9 @@ lib_deps =
|
||||
[env:build_webUI]
|
||||
platform = native
|
||||
targets = build
|
||||
extra_scripts = post:scripts/build_interface.py
|
||||
extra_scripts = pre:scripts/build_interface.py
|
||||
; Exclude all source files so the C code doesn't build
|
||||
build_src_filter = -<*>
|
||||
|
||||
;
|
||||
; Builds for different board types
|
||||
|
||||
@@ -109,12 +109,14 @@ def build_webUI(*args, **kwargs):
|
||||
print("Web interface build failed!")
|
||||
env.Exit(1)
|
||||
env.Exit(0)
|
||||
|
||||
|
||||
# Create custom target that only runs the script and then exits, without continuing with the pio workflow
|
||||
env.AddCustomTarget(
|
||||
name="build",
|
||||
dependencies=None,
|
||||
actions=[build_webUI],
|
||||
title="build web interface",
|
||||
description="installs pnpm packages, updates libraries and builds web UI"
|
||||
description="installs pnpm packages, updates libraries and builds web UI",
|
||||
always_build=True
|
||||
)
|
||||
|
||||
|
||||
@@ -2,133 +2,121 @@ import hashlib
|
||||
import shutil
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
Import("env")
|
||||
|
||||
OUTPUT_DIR = "build{}".format(os.path.sep)
|
||||
OUTPUT_DIR = Path("build")
|
||||
|
||||
|
||||
def bin_copy(source, target, env):
|
||||
"""Optimized firmware renaming and copying function."""
|
||||
|
||||
# Get the application version from emsesp_version.h
|
||||
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
|
||||
bag = {}
|
||||
exprs = [(re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"'), 'app_version')]
|
||||
with open('./src/emsesp_version.h', 'r') as f:
|
||||
for l in f.readlines():
|
||||
for expr, var in exprs:
|
||||
m = expr.match(l)
|
||||
if m and len(m.groups()) > 0:
|
||||
bag[var] = m.group(1)
|
||||
# Get the chip type, in uppercase
|
||||
mcu = env.get('BOARD_MCU', '').upper()
|
||||
if not mcu:
|
||||
print("Error: Could not determine MCU type!")
|
||||
return
|
||||
|
||||
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
|
||||
mcu = env.get('BOARD_MCU').upper()
|
||||
# alternatively take platform from the pio target
|
||||
# platform = str(target[0]).split(os.path.sep)[2]
|
||||
print("=" * 90)
|
||||
print(f"EMS-ESP version: {app_version}")
|
||||
print(f"Has PSRAM: {'Yes' if psram else 'No'}")
|
||||
print(f"MCU: {mcu}")
|
||||
print(f"Flash Mem: {flash_mem}")
|
||||
|
||||
# work out the flash memory from the PIO env name (sloppy but works)
|
||||
# unfortunately the board_upload.flash_size is not passed down
|
||||
flash_mem = "4MB"
|
||||
pio_env = env.get('PIOENV').upper()
|
||||
parts = pio_env.split('_')
|
||||
# if it ends with a _P skip (we use this to denote PSRAM)
|
||||
if parts[-1].endswith("P"):
|
||||
index = -2
|
||||
else:
|
||||
index = -1
|
||||
|
||||
# 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
|
||||
# Convert . to _ so Windows doesn't complain
|
||||
# Format is EMS-ESP-<version>-<mcu>-<flash> with + at the end if it has PSRAM
|
||||
variant = "EMS-ESP-" + \
|
||||
app_version.replace(".", "_") + "-" + mcu + "-" + \
|
||||
flash_mem + ("+" if psram else "")
|
||||
variant = f"EMS-ESP-{app_version.replace('.', '_')}-{mcu}-{flash_mem}{'+' if psram else ''}"
|
||||
|
||||
# check if output directories exist and create if necessary
|
||||
if not os.path.isdir(OUTPUT_DIR):
|
||||
os.mkdir(OUTPUT_DIR)
|
||||
# Create output directories
|
||||
firmware_dir = OUTPUT_DIR / "firmware"
|
||||
firmware_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for d in ['firmware']:
|
||||
if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)):
|
||||
os.mkdir("{}{}".format(OUTPUT_DIR, d))
|
||||
# Define file paths
|
||||
bin_file = firmware_dir / f"{variant}.bin"
|
||||
md5_file = firmware_dir / f"{variant}.md5"
|
||||
|
||||
# create string with location and file names based on variant
|
||||
bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant)
|
||||
md5_file = "{}firmware{}{}.md5".format(OUTPUT_DIR, os.path.sep, variant)
|
||||
# Remove existing files if they exist
|
||||
for file_path in [bin_file, md5_file]:
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
# check if new target files exist and remove if necessary
|
||||
for f in [bin_file]:
|
||||
if os.path.isfile(f):
|
||||
os.remove(f)
|
||||
print(f"Filename: {bin_file}")
|
||||
|
||||
# check if new target files exist and remove if necessary
|
||||
for f in [md5_file]:
|
||||
if os.path.isfile(f):
|
||||
os.remove(f)
|
||||
# Copy firmware.bin to firmware/<variant>.bin
|
||||
shutil.copy2(str(target[0]), str(bin_file))
|
||||
|
||||
print("Filename: "+bin_file)
|
||||
# Calculate and write MD5 hash
|
||||
with bin_file.open("rb") as f:
|
||||
md5_hash = hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
print(f"MD5: {md5_hash}")
|
||||
md5_file.write_text(md5_hash)
|
||||
|
||||
# copy firmware.bin to firmware/<variant>.bin
|
||||
shutil.copy(str(target[0]), bin_file)
|
||||
|
||||
with open(bin_file, "rb") as f:
|
||||
result = hashlib.md5(f.read())
|
||||
print("MD5: "+result.hexdigest())
|
||||
file1 = open(md5_file, 'w')
|
||||
file1.write(result.hexdigest())
|
||||
file1.close()
|
||||
|
||||
# make a copy using the old 3.6.x filename format for backwards compatibility with the WebUI version check, e.g.
|
||||
# create a EMS-ESP-<version>-ESP32_S3.bin if target is s3_16M_P (16MB, PSRAM)
|
||||
# create a EMS-ESP-<version>-ESP32.bin if target is s_4M (4MB, no PSRAM), compatible only with S32 V1 and E32 V1.0,1.4,1.5
|
||||
#
|
||||
# Note: there is a chance newer E32V2s (which use the 16MB partition table and PSRAM) are running a custom build
|
||||
# of the 3.6.5 firmware as 3.6.5 was released before production of the gateway board. Updating via the WebUI will break the system and require a manual update.
|
||||
#
|
||||
extra_variant = ""
|
||||
if env.get('PIOENV') == "s3_16M_P":
|
||||
extra_variant = "EMS-ESP-" + \
|
||||
app_version.replace(".", "_") + "-ESP32_S3"
|
||||
elif env.get('PIOENV') == "s_4M":
|
||||
extra_variant = "EMS-ESP-" + app_version.replace(".", "_") + "-ESP32"
|
||||
# Make a copy using the old 3.6.x filename format for backwards compatibility
|
||||
# Note: there is a chance newer E32V2s (which use the 16MB partition table and PSRAM)
|
||||
# are running a custom build of the 3.6.5 firmware as 3.6.5 was released before
|
||||
# production of the gateway board. Updating via the WebUI will break the system
|
||||
# and require a manual update.
|
||||
|
||||
pio_env = env.get('PIOENV', '')
|
||||
extra_variant = None
|
||||
|
||||
if pio_env == "s3_16M_P":
|
||||
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32_S3"
|
||||
elif pio_env == "s_4M":
|
||||
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32"
|
||||
|
||||
if extra_variant:
|
||||
extra_bin_file = "{}firmware{}{}.bin".format(
|
||||
OUTPUT_DIR, os.path.sep, extra_variant)
|
||||
if os.path.isfile(extra_bin_file):
|
||||
os.remove(extra_bin_file)
|
||||
extra_bin_file = firmware_dir / f"{extra_variant}.bin"
|
||||
extra_md5_file = firmware_dir / f"{extra_variant}.md5"
|
||||
|
||||
# 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(
|
||||
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("*********************************************")
|
||||
print("=" * 90)
|
||||
|
||||
|
||||
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++;
|
||||
p++;
|
||||
}
|
||||
*p = '\0'; // terminat result
|
||||
*p = '\0'; // terminate result
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ char * Helpers::utf8tolatin1(char * result, const char * c, const uint8_t len) {
|
||||
c++;
|
||||
p++;
|
||||
}
|
||||
*p = '\0'; // terminat result
|
||||
*p = '\0'; // terminate result
|
||||
return result;
|
||||
}
|
||||
// creates string of hex values from an array of bytes
|
||||
|
||||
@@ -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