13 Commits

Author SHA1 Message Date
Proddy
179ddcb348 Merge pull request #2678 from proddy/dev
fix native/standalone
2025-10-24 18:15:03 +02:00
proddy
efa2c8fc4b rename native to standalone 2025-10-24 18:06:52 +02:00
proddy
c958c7d61a fix native 2025-10-24 18:05:12 +02:00
Proddy
35fca9c450 Merge pull request #2677 from proddy/dev
updates to version
2025-10-24 16:28:03 +02:00
proddy
61962fbc07 only build web with target -t build 2025-10-24 16:25:28 +02:00
proddy
39e724befe don't build web on each target 2025-10-24 16:25:15 +02:00
proddy
43bb77b095 optimizations using caching 2025-10-24 16:24:55 +02:00
proddy
28e1e46586 always build web first 2025-10-24 15:35:54 +02:00
proddy
2f5b879652 handle buildweb condition when its part of a pio chain 2025-10-24 15:33:50 +02:00
proddy
16930fe8ca add back for reference 2025-10-24 15:32:35 +02:00
proddy
1db1b6e524 package update 2025-10-24 15:31:57 +02:00
proddy
43eba7a010 show dialog with version information 2025-10-24 15:31:49 +02:00
proddy
a837c9398c updated 2025-10-24 09:40:41 +02:00
10 changed files with 649 additions and 344 deletions

View File

@@ -32,6 +32,6 @@ jobs:
pip install wheel pip install wheel
pip install -U platformio pip install -U platformio
- name: Build native - name: Build standalone
run: | run: |
platformio run -e native platformio run -e standalone

View File

@@ -32,7 +32,7 @@
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"formidable": "^3.5.4", "formidable": "^3.5.4",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"magic-string": "^0.30.19", "magic-string": "^0.30.21",
"mime-types": "^3.0.1", "mime-types": "^3.0.1",
"preact": "^10.27.2", "preact": "^10.27.2",
"react": "^19.2.0", "react": "^19.2.0",

View File

@@ -39,8 +39,8 @@ importers:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
magic-string: magic-string:
specifier: ^0.30.19 specifier: ^0.30.21
version: 0.30.19 version: 0.30.21
mime-types: mime-types:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1 version: 3.0.1
@@ -663,8 +663,8 @@ packages:
'@prefresh/utils@1.2.1': '@prefresh/utils@1.2.1':
resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==}
'@prefresh/vite@2.4.10': '@prefresh/vite@2.4.11':
resolution: {integrity: sha512-lt+ODASOtXRWaPplp7/DlrgAaInnQYNvcpCglQBMx2OeJPyZ4IqPRaxsK77w96mWshjYwkqTsRSHoAM7aAn0ow==} resolution: {integrity: sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==}
peerDependencies: peerDependencies:
preact: ^10.4.0 || ^11.0.0-0 preact: ^10.4.0 || ^11.0.0-0
vite: '>=2.0.0' vite: '>=2.0.0'
@@ -1324,8 +1324,8 @@ packages:
duplexer3@0.1.5: duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
electron-to-chromium@1.5.239: electron-to-chromium@1.5.240:
resolution: {integrity: sha512-1y5w0Zsq39MSPmEjHjbizvhYoTaulVtivpxkp5q5kaPmQtsK6/2nvAzGRxNMS9DoYySp9PkW0MAQDwU1m764mg==} resolution: {integrity: sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2146,8 +2146,8 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
magic-string@0.30.19: magic-string@0.30.21:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
make-dir@1.3.0: make-dir@1.3.0:
resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==}
@@ -3580,7 +3580,7 @@ snapshots:
'@babel/core': 7.28.5 '@babel/core': 7.28.5
'@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5)
'@prefresh/vite': 2.4.10(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0)) '@prefresh/vite': 2.4.11(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0))
'@rollup/pluginutils': 4.2.1 '@rollup/pluginutils': 4.2.1
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5)
debug: 4.4.3 debug: 4.4.3
@@ -3599,7 +3599,7 @@ snapshots:
'@prefresh/utils@1.2.1': {} '@prefresh/utils@1.2.1': {}
'@prefresh/vite@2.4.10(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0))': '@prefresh/vite@2.4.11(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0))':
dependencies: dependencies:
'@babel/core': 7.28.5 '@babel/core': 7.28.5
'@prefresh/babel-plugin': 0.5.2 '@prefresh/babel-plugin': 0.5.2
@@ -3995,7 +3995,7 @@ snapshots:
dependencies: dependencies:
baseline-browser-mapping: 2.8.20 baseline-browser-mapping: 2.8.20
caniuse-lite: 1.0.30001751 caniuse-lite: 1.0.30001751
electron-to-chromium: 1.5.239 electron-to-chromium: 1.5.240
node-releases: 2.0.26 node-releases: 2.0.26
update-browserslist-db: 1.1.4(browserslist@4.27.0) update-browserslist-db: 1.1.4(browserslist@4.27.0)
@@ -4340,7 +4340,7 @@ snapshots:
duplexer3@0.1.5: {} duplexer3@0.1.5: {}
electron-to-chromium@1.5.239: {} electron-to-chromium@1.5.240: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
@@ -5178,7 +5178,7 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
magic-string@0.30.19: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
@@ -5967,7 +5967,7 @@ snapshots:
vite-prerender-plugin@0.5.12(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0)): vite-prerender-plugin@0.5.12(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0)):
dependencies: dependencies:
kolorist: 1.8.0 kolorist: 1.8.0
magic-string: 0.30.19 magic-string: 0.30.21
node-html-parser: 6.1.13 node-html-parser: 6.1.13
simple-code-frame: 1.3.0 simple-code-frame: 1.3.0
source-map: 0.7.6 source-map: 0.7.6

View File

@@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { IconContext } from 'react-icons/lib'; import { IconContext } from 'react-icons/lib';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -44,7 +44,7 @@ import {
} from './types'; } from './types';
import { deviceValueItemValidation } from './validators'; import { deviceValueItemValidation } from './validators';
const Dashboard = () => { const Dashboard = memo(() => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
@@ -163,9 +163,14 @@ const Dashboard = () => {
} }
}); });
const nodeIds = useMemo(
() => data.nodes.map((item: DashboardItem) => item.id),
[data.nodes]
);
useEffect(() => { useEffect(() => {
showAll showAll
? tree.fns.onAddAll(data.nodes.map((item: DashboardItem) => item.id)) // expand tree ? tree.fns.onAddAll(nodeIds) // expand tree
: tree.fns.onRemoveAll(); // collapse tree : tree.fns.onRemoveAll(); // collapse tree
}, [parentNodes]); }, [parentNodes]);
@@ -195,27 +200,32 @@ const Dashboard = () => {
[LL] [LL]
); );
const showName = (di: DashboardItem) => { const showName = useCallback(
if (di.id < 100) { (di: DashboardItem) => {
// if its a device (parent node) and has entities if (di.id < 100) {
if (di.nodes?.length) { // if its a device (parent node) and has entities
return ( if (di.nodes?.length) {
<span style={{ fontWeight: 'bold', fontSize: '14px' }}> return (
<DeviceIcon type_id={di.t ?? 0} /> <span style={{ fontWeight: 'bold', fontSize: '14px' }}>
&nbsp;&nbsp;{showType(di.n, di.t)} <DeviceIcon type_id={di.t ?? 0} />
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span> &nbsp;&nbsp;{showType(di.n, di.t)}
</span> <span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
); </span>
);
}
} }
} if (di.dv) {
if (di.dv) { return <span>{di.dv.id.slice(2)}</span>;
return <span>{di.dv.id.slice(2)}</span>; }
} return null;
return null; },
}; [showType]
);
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 editDashboardValue = useCallback( const editDashboardValue = useCallback(
(di: DashboardItem) => { (di: DashboardItem) => {
@@ -237,6 +247,11 @@ const Dashboard = () => {
} }
}; };
const hasFavEntities = useMemo(
() => data.nodes.filter((item: DashboardItem) => item.id <= 90).length,
[data.nodes]
);
const renderContent = () => { const renderContent = () => {
if (!data) { if (!data) {
return ( return (
@@ -244,10 +259,6 @@ const Dashboard = () => {
); );
} }
const hasFavEntities = data.nodes.filter(
(item: DashboardItem) => item.id <= 90
).length;
return ( return (
<> <>
{!data.connected && ( {!data.connected && (
@@ -391,6 +402,6 @@ const Dashboard = () => {
)} )}
</SectionContent> </SectionContent>
); );
}; });
export default Dashboard; export default Dashboard;

View File

@@ -1,10 +1,11 @@
import { useContext, useEffect, useState } from 'react'; import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Done'; import CheckIcon from '@mui/icons-material/Done';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { import {
Box, Box,
@@ -16,7 +17,11 @@ import {
DialogTitle, DialogTitle,
FormControlLabel, FormControlLabel,
Grid, Grid,
IconButton,
Link, Link,
List,
ListItem,
ListItemText,
Typography Typography
} from '@mui/material'; } from '@mui/material';
@@ -36,168 +41,143 @@ 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 { prettyDateTime } from 'utils/time';
const Version = () => { // Constants moved outside component to avoid recreation
const { LL, locale } = useI18nContext(); const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const { me } = useContext(AuthenticatedContext); const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const [restarting, setRestarting] = useState<boolean>(false); // Types for better type safety
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false); interface VersionData {
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false); emsesp_version: string;
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false); arduino_version: string;
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false); esp_platform: string;
const [stableUpgradeAvailable, setStableUpgradeAvailable] = flash_chip_size: number;
useState<boolean>(false); psram: boolean;
const [internetLive, setInternetLive] = useState<boolean>(false); build_flags?: string;
const [downloadOnly, setDownloadOnly] = useState<boolean>(false); }
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/'; interface UpgradeCheckData {
const STABLE_RELNOTES_URL = emsesp_version: string;
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md'; dev_upgradeable: boolean;
stable_upgradeable: boolean;
}
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/'; interface VersionInfo {
const DEV_RELNOTES_URL = name: string;
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md'; published_at?: string;
}
const { send: sendCheckUpgrade } = useRequest( // Memoized components for better performance
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }), const VersionInfoDialog = memo(
{ ({
immediate: false showVersionInfo,
} latestVersion,
).onSuccess((event) => { latestDevVersion,
const data = event.data as { locale,
emsesp_version: string; LL,
dev_upgradeable: boolean; onClose
stable_upgradeable: boolean; }: {
}; showVersionInfo: number;
setDevUpgradeAvailable(data.dev_upgradeable); latestVersion?: VersionInfo;
setStableUpgradeAvailable(data.stable_upgradeable); latestDevVersion?: VersionInfo;
}); locale: string;
LL: any;
onClose: () => void;
}) => {
if (showVersionInfo === 0) return null;
const { const isStable = showVersionInfo === 1;
data: data, const version = isStable ? latestVersion : latestDevVersion;
send: loadData, const relNotesUrl = isStable ? STABLE_RELNOTES_URL : DEV_RELNOTES_URL;
error
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
// older version of EMS-ESP using ESP32 (not S3) and no PSRAM, can't use OTA because of SSL support in HttpClient
if (event.data.arduino_version.startsWith('Tasmota')) {
setDownloadOnly(true);
}
setUsingDevVersion(event.data.emsesp_version.includes('dev'));
});
const { send: sendUploadURL } = useRequest( return (
(url: string) => callAction({ action: 'uploadURL', param: url }), <Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
{ <DialogTitle>Version Information</DialogTitle>
immediate: false <DialogContent dividers>
} <List dense>
); <ListItem>
<ListItemText
// called immediately to get the latest versions on page load primary={<span style={{ color: 'lightblue' }}>{LL.TYPE(0)}</span>}
const { data: latestVersion } = useRequest(getStableVersion); secondary={isStable ? LL.STABLE() : LL.DEVELOPMENT()}
const { data: latestDevVersion } = useRequest(getDevVersion); />
</ListItem>
useEffect(() => { <ListItem>
if (latestVersion && latestDevVersion) { <ListItemText
sendCheckUpgrade(latestDevVersion.name + ',' + latestVersion.name) primary={<span style={{ color: 'lightblue' }}>{LL.VERSION()}</span>}
.catch((error: Error) => { secondary={version?.name}
toast.error('Failed to check for upgrades: ' + error.message); />
}) </ListItem>
.finally(() => { {version?.published_at && (
setInternetLive(true); <ListItem>
}); <ListItemText
} primary={<span style={{ color: 'lightblue' }}>Release Date</span>}
}, [latestVersion, latestDevVersion]); secondary={prettyDateTime(locale, new Date(version.published_at))}
/>
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }); </ListItem>
const DIVISIONS: Array<{ amount: number; name: string }> = [ )}
{ amount: 60, name: 'seconds' }, </List>
{ amount: 60, name: 'minutes' }, </DialogContent>
{ amount: 24, name: 'hours' }, <DialogActions>
{ amount: 7, name: 'days' }, <Button
{ amount: 4.34524, name: 'weeks' }, variant="outlined"
{ amount: 12, name: 'months' }, component="a"
{ amount: Number.POSITIVE_INFINITY, name: 'years' } href={relNotesUrl}
]; target="_blank"
function formatTimeAgo(date: Date) { color="primary"
let duration = (date.getTime() - new Date().getTime()) / 1000; >
for (let i = 0; i < DIVISIONS.length; i++) { Changelog
const division = DIVISIONS[i]; </Button>
if (division && Math.abs(duration) < division.amount) { <Button variant="outlined" onClick={onClose} color="secondary">
return rtf.format( {LL.CLOSE()}
Math.round(duration), </Button>
division.name as Intl.RelativeTimeFormatUnit </DialogActions>
); </Dialog>
} );
if (division) {
duration /= division.amount;
}
}
return rtf.format(0, 'seconds');
} }
);
const { send: sendAPI } = useRequest((data: APIcall) => API(data), { const InstallDialog = memo(
immediate: false ({
}); openInstallDialog,
fetchDevVersion,
latestVersion,
latestDevVersion,
downloadOnly,
platform,
LL,
onClose,
onInstall
}: {
openInstallDialog: boolean;
fetchDevVersion: boolean;
latestVersion?: VersionInfo;
latestDevVersion?: VersionInfo;
downloadOnly: boolean;
platform: string;
LL: any;
onClose: () => void;
onInstall: (url: string) => void;
}) => {
const binURL = useMemo(() => {
if (!latestVersion || !latestDevVersion) return '';
const doRestart = async () => { const version = fetchDevVersion ? latestDevVersion : latestVersion;
setRestarting(true); const filename = `EMS-ESP-${version.name.replaceAll('.', '_')}-${platform}.bin`;
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const getBinURL = (showingDev: boolean) => { return fetchDevVersion
if (!internetLive) { ? `${DEV_URL}${filename}`
return ''; : `${STABLE_URL}v${version.name}/${filename}`;
} }, [fetchDevVersion, latestVersion, latestDevVersion, platform]);
const filename =
'EMS-ESP-' +
(showingDev ? latestDevVersion.name : latestVersion.name).replaceAll(
'.',
'_'
) +
'-' +
getPlatform() +
'.bin';
return showingDev
? DEV_URL + filename
: STABLE_URL + 'v' + latestVersion.name + '/' + filename;
};
const getPlatform = () => {
return (
[data.esp_platform, data.flash_chip_size >= 16384 ? '16MB' : '4MB'].join('-') +
(data.psram ? '+' : '')
);
};
const installFirmwareURL = async (url: string) => {
await sendUploadURL(url).catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
useLayoutTitle('EMS-ESP Firmware');
const renderInstallDialog = () => {
const binURL = getBinURL(fetchDevVersion);
return ( return (
<Dialog <Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
sx={dialogStyle}
open={openInstallDialog}
onClose={() => closeInstallDialog()}
>
<DialogTitle> <DialogTitle>
{LL.UPDATE() + {`${LL.UPDATE()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
' ' +
(fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()) +
' Firmware'}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Typography mb={2}> <Typography mb={2}>
@@ -211,7 +191,7 @@ const Version = () => {
<Button <Button
startIcon={<CancelIcon />} startIcon={<CancelIcon />}
variant="outlined" variant="outlined"
onClick={() => closeInstallDialog()} onClick={onClose}
color="secondary" color="secondary"
> >
{LL.CANCEL()} {LL.CANCEL()}
@@ -219,7 +199,7 @@ const Version = () => {
<Button <Button
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
variant="outlined" variant="outlined"
onClick={() => closeInstallDialog()} onClick={onClose}
color="primary" color="primary"
> >
<Link underline="none" target="_blank" href={binURL} color="primary"> <Link underline="none" target="_blank" href={binURL} color="primary">
@@ -230,7 +210,7 @@ const Version = () => {
<Button <Button
startIcon={<WarningIcon color="warning" />} startIcon={<WarningIcon color="warning" />}
variant="outlined" variant="outlined"
onClick={() => installFirmwareURL(binURL)} onClick={() => onInstall(binURL)}
color="primary" color="primary"
> >
{LL.INSTALL()} {LL.INSTALL()}
@@ -239,76 +219,185 @@ const Version = () => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
}; }
);
const showFirmwareDialog = (useDevVersion: boolean) => { // Helper function moved outside component
const getPlatform = (data: VersionData): string => {
return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`;
};
const Version = () => {
const { LL, locale } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
// State management
const [restarting, setRestarting] = useState<boolean>(false);
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false);
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false);
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
useState<boolean>(false);
const [internetLive, setInternetLive] = useState<boolean>(false);
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
const [showVersionInfo, setShowVersionInfo] = useState<number>(0);
// API calls with optimized error handling
const { send: sendCheckUpgrade } = useRequest(
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }),
{ immediate: false }
).onSuccess((event) => {
const data = event.data as UpgradeCheckData;
setDevUpgradeAvailable(data.dev_upgradeable);
setStableUpgradeAvailable(data.stable_upgradeable);
});
const {
data,
send: loadData,
error
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
const systemData = event.data as VersionData;
if (systemData.arduino_version.startsWith('Tasmota')) {
setDownloadOnly(true);
}
setUsingDevVersion(systemData.emsesp_version.includes('dev'));
});
const { send: sendUploadURL } = useRequest(
(url: string) => callAction({ action: 'uploadURL', param: url }),
{ immediate: false }
);
const { data: latestVersion } = useRequest(getStableVersion);
const { data: latestDevVersion } = useRequest(getDevVersion);
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
// Memoized values
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
const isDev = useMemo(
() => data?.emsesp_version.includes('dev') ?? false,
[data?.emsesp_version]
);
const doRestart = useCallback(async () => {
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
}, [sendAPI]);
const installFirmwareURL = useCallback(
async (url: string) => {
await sendUploadURL(url).catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
},
[sendUploadURL]
);
const showFirmwareDialog = useCallback((useDevVersion: boolean) => {
setFetchDevVersion(useDevVersion); setFetchDevVersion(useDevVersion);
setOpenInstallDialog(true); setOpenInstallDialog(true);
}; }, []);
const closeInstallDialog = () => { const closeInstallDialog = useCallback(() => {
setOpenInstallDialog(false); setOpenInstallDialog(false);
}; }, []);
const showButtons = (showingDev: boolean) => { const handleVersionInfoClose = useCallback(() => {
const choice = showingDev setShowVersionInfo(0);
? !usingDevVersion }, []);
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
: devUpgradeAvailable // Effect for checking upgrades
? LL.UPDATE_AVAILABLE() useEffect(() => {
: undefined if (latestVersion && latestDevVersion) {
: usingDevVersion const versions = `${latestDevVersion.name},${latestVersion.name}`;
? LL.SWITCH_RELEASE_TYPE(LL.STABLE()) sendCheckUpgrade(versions)
: stableUpgradeAvailable .catch((error: Error) => {
? LL.UPDATE_AVAILABLE() toast.error(`Failed to check for upgrades: ${error.message}`);
: undefined; })
.finally(() => {
setInternetLive(true);
});
}
}, [latestVersion, latestDevVersion, sendCheckUpgrade]);
useLayoutTitle('EMS-ESP Firmware');
// Memoized button rendering logic
const showButtons = useCallback(
(showingDev: boolean) => {
const choice = showingDev
? !usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
: devUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined
: usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
: stableUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined;
if (!choice) {
return (
<>
<CheckIcon
color="success"
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
/>
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
</span>
<Button
sx={{ ml: 2 }}
variant="outlined"
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{LL.REINSTALL()}
</Button>
</>
);
}
if (!me.admin) return null;
if (!choice) {
return ( return (
<> <Button
<CheckIcon sx={{ ml: 2 }}
color="success" variant="outlined"
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }} color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
/> size="small"
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}> onClick={() => showFirmwareDialog(showingDev)}
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())} >
</span> {choice}
<Button </Button>
sx={{ ml: 2 }}
variant="outlined"
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{LL.REINSTALL()}
</Button>
</>
); );
} },
[
usingDevVersion,
devUpgradeAvailable,
stableUpgradeAvailable,
me.admin,
LL,
showFirmwareDialog
]
);
if (!me.admin) { const content = useMemo(() => {
return;
}
return (
<Button
sx={{ ml: 2 }}
variant="outlined"
color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{choice}
</Button>
);
};
const content = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />; return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
} }
const isDev = data.emsesp_version.includes('dev');
return ( return (
<> <>
<Box p={2} border="1px solid grey" borderRadius={2}> <Box p={2} border="1px solid grey" borderRadius={2}>
@@ -344,7 +433,7 @@ const Version = () => {
</Grid> </Grid>
<Grid size={{ xs: 8, md: 10 }}> <Grid size={{ xs: 8, md: 10 }}>
<Typography> <Typography>
{getPlatform()} {platform}
<Typography variant="caption"> <Typography variant="caption">
&nbsp; &#40; &nbsp; &#40;
{data.psram ? ( {data.psram ? (
@@ -436,15 +525,10 @@ const Version = () => {
</Grid> </Grid>
<Grid size={{ xs: 8, md: 10 }}> <Grid size={{ xs: 8, md: 10 }}>
<Typography> <Typography>
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary"> {latestVersion?.name}
{latestVersion.name} <IconButton onClick={() => setShowVersionInfo(1)}>
</Link> <InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
{latestVersion.published_at && ( </IconButton>
<Typography component="span" variant="caption">
&nbsp;(
{formatTimeAgo(new Date(latestVersion.published_at))})
</Typography>
)}
{showButtons(false)} {showButtons(false)}
</Typography> </Typography>
</Grid> </Grid>
@@ -454,15 +538,10 @@ const Version = () => {
</Grid> </Grid>
<Grid size={{ xs: 8, md: 10 }}> <Grid size={{ xs: 8, md: 10 }}>
<Typography> <Typography>
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary"> {latestDevVersion?.name}
{latestDevVersion.name} <IconButton onClick={() => setShowVersionInfo(2)}>
</Link> <InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
{latestDevVersion.published_at && ( </IconButton>
<Typography component="span" variant="caption">
&nbsp;(
{formatTimeAgo(new Date(latestDevVersion.published_at))})
</Typography>
)}
{showButtons(true)} {showButtons(true)}
</Typography> </Typography>
</Grid> </Grid>
@@ -476,7 +555,25 @@ const Version = () => {
)} )}
{me.admin && ( {me.admin && (
<> <>
{renderInstallDialog()} <VersionInfoDialog
showVersionInfo={showVersionInfo}
latestVersion={latestVersion}
latestDevVersion={latestDevVersion}
locale={locale}
LL={LL}
onClose={handleVersionInfoClose}
/>
<InstallDialog
openInstallDialog={openInstallDialog}
fetchDevVersion={fetchDevVersion}
latestVersion={latestVersion}
latestDevVersion={latestDevVersion}
downloadOnly={downloadOnly}
platform={platform}
LL={LL}
onClose={closeInstallDialog}
onInstall={installFirmwareURL}
/>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()} {LL.UPLOAD()}
</Typography> </Typography>
@@ -486,11 +583,30 @@ const Version = () => {
</Box> </Box>
</> </>
); );
}; }, [
data,
error,
loadData,
LL,
platform,
isDev,
internetLive,
latestVersion,
latestDevVersion,
showVersionInfo,
locale,
openInstallDialog,
fetchDevVersion,
downloadOnly,
me.admin,
showButtons,
handleVersionInfoClose,
closeInstallDialog,
installFirmwareURL,
doRestart
]);
return ( return <SectionContent>{restarting ? <SystemMonitor /> : content}</SectionContent>;
<SectionContent>{restarting ? <SystemMonitor /> : content()}</SectionContent>
);
}; };
export default Version; export default memo(Version);

View File

@@ -1,18 +1,135 @@
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], { // Cache for formatters to avoid recreation
day: 'numeric', const formatterCache = new Map<string, Intl.DateTimeFormat>();
month: 'short', const rtfCache = new Map<string, Intl.RelativeTimeFormat>();
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
});
export const formatDateTime = (dateTime: string) => // Pre-computed time divisions for relative time formatting
LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19))); const TIME_DIVISIONS = [
{ amount: 60, name: 'seconds' as const },
{ amount: 60, name: 'minutes' as const },
{ amount: 24, name: 'hours' as const },
{ amount: 7, name: 'days' as const },
{ amount: 4.34524, name: 'weeks' as const },
{ amount: 12, name: 'months' as const },
{ amount: Number.POSITIVE_INFINITY, name: 'years' as const }
] as const;
export const formatLocalDateTime = (date: Date) => /**
new Date(date.getTime() - date.getTimezoneOffset() * 60000) * Get or create a cached DateTimeFormat instance
.toISOString() */
.slice(0, -1) function getDateTimeFormatter(
.substring(0, 19); options: Intl.DateTimeFormatOptions
): Intl.DateTimeFormat {
const key = JSON.stringify(options);
if (!formatterCache.has(key)) {
formatterCache.set(
key,
new Intl.DateTimeFormat([...window.navigator.languages], options)
);
}
return formatterCache.get(key)!;
}
/**
* Get or create a cached RelativeTimeFormat instance
*/
function getRelativeTimeFormatter(locale: string): Intl.RelativeTimeFormat {
if (!rtfCache.has(locale)) {
rtfCache.set(locale, new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }));
}
return rtfCache.get(locale)!;
}
/**
* Format a date as relative time (e.g., "2 hours ago", "in 3 days")
*/
function formatTimeAgo(locale: string, date: Date): string {
const now = Date.now();
const targetTime = date.getTime();
let duration = (targetTime - now) / 1000;
const rtf = getRelativeTimeFormatter(locale);
// Use for...of for better performance and readability
for (const division of TIME_DIVISIONS) {
if (Math.abs(duration) < division.amount) {
return rtf.format(Math.round(duration), division.name);
}
duration /= division.amount;
}
return rtf.format(0, 'seconds');
}
/**
* Format a date-time string to locale-specific format
*/
export const formatDateTime = (dateTime: string): string => {
if (!dateTime || typeof dateTime !== 'string') {
return 'Invalid date';
}
try {
// Extract only the first 19 characters (YYYY-MM-DDTHH:mm:ss)
const cleanDateTime = dateTime.substring(0, 19);
const date = new Date(cleanDateTime);
if (isNaN(date.getTime())) {
return 'Invalid date';
}
const formatter = getDateTimeFormatter({
day: 'numeric',
month: 'short',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
});
return formatter.format(date);
} catch (error) {
console.warn('Error formatting date:', error);
return 'Invalid date';
}
};
/**
* Convert a Date object to local date-time string (ISO format without timezone)
*/
export const formatLocalDateTime = (date: Date): string => {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return 'Invalid date';
}
// Calculate local time offset in milliseconds
const offsetMs = date.getTimezoneOffset() * 60000;
const localTime = date.getTime() - offsetMs;
// Convert to ISO string and remove timezone info
return new Date(localTime).toISOString().slice(0, 19);
};
/**
* Format a date with both short date format and relative time
*/
export const prettyDateTime = (locale: string, date: Date): string => {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return 'Invalid date';
}
if (!locale || typeof locale !== 'string') {
locale = 'en';
}
const shortFormatter = getDateTimeFormatter({
day: 'numeric',
month: 'short',
year: 'numeric'
});
const shortDate = shortFormatter.format(date);
const relativeTime = formatTimeAgo(locale, date);
return `${shortDate} (${relativeTime})`;
};

View File

@@ -19,18 +19,18 @@
my_build_flags = my_build_flags =
[platformio] [platformio]
; default_envs = s_16M_P ; BBQKees E32V2 default_envs = s_16M_P ; BBQKees E32V2
; default_envs = build_webUI ; build the web interface only
; default_envs = s3_16M_P ; BBQKees S3 ; default_envs = s3_16M_P ; BBQKees S3
; default_envs = s_4M ; BBQKees older S32, 4MB no psram ; default_envs = s_4M ; BBQKees older S32, 4MB no psram
; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram ; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram
; default_envs = c6 ; XIAO ESP32C ; default_envs = c6 ; XIAO ESP32C
default_envs = debug ; default_envs = debug
[env] [env]
; uncomment if you want to upload the firmware via OTA (must have upload_protocol = custom)
extra_scripts = extra_scripts =
pre:scripts/build_interface.py ; builds the WebUI - comment out if you don't want to build each time pre:scripts/build_interface.py ; builds the WebUI
scripts/rename_fw.py ; renames the firmware .bin file - comment out if not needed scripts/rename_fw.py ; renames the firmware .bin file - comment out if not needed
scripts/upload.py ; optionally upload the firmware via OTA (must have upload_protocol = custom) scripts/upload.py ; optionally upload the firmware via OTA (must have upload_protocol = custom)
@@ -46,28 +46,6 @@ custom_password = admin
; upload_protocol = custom ; upload_protocol = custom
; custom_emsesp_ip = <ip address> or ems-esp.local ; custom_emsesp_ip = <ip address> or ems-esp.local
; upload_protocol = custom
; custom_emsesp_ip = 10.10.10.93 ; S3
custom_emsesp_ip = 192.168.1.223 ; E32V2
; custom_emsesp_ip = 192.168.1.173 ; S32
; custom_emsesp_ip = 192.168.1.59 ; S32 (old) 4MB blue board
; lib_deps =
; bblanchon/ArduinoJson @ 7.4.2
; ESP32Async/AsyncTCP @ 3.4.9
; ESP32Async/ESPAsyncWebServer @ 3.8.1
; ; file://${PROJECT_DIR}/../modules/EMS-ESP-Modules
; https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8
; [espressif32_base_16M]
; framework = arduino
; board_build.partitions = partitions/esp32_partition_16M.csv
; board_upload.flash_size = 16MB
; board_build.app_partition_name = app0
; ; platform = espressif32@6.12.0 ; Arduino Core v2.0.17 / IDF v4.4.7
; ; platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip ; Arduino Core v3.2.0 / ESP-IDF v5.4.1
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30/platform-espressif32.zip ; Arduino Core 3.3.0, IDF 5.5.0
; ** debug settings ** ; ** debug settings **
; to be used with an ESP32-S3 which has an onboard JTAG, connect the OTG USB port to the PC ; to be used with an ESP32-S3 which has an onboard JTAG, connect the OTG USB port to the PC
; if using an external JTAG board like the ESP-PROG set debug_tool = esp-prog, and use zadig to set the correct driver for the USB port ; if using an external JTAG board like the ESP-PROG set debug_tool = esp-prog, and use zadig to set the correct driver for the USB port

View File

@@ -98,9 +98,7 @@ build_flags =
build_unflags = build_unflags =
${common.unbuild_flags} ${common.unbuild_flags}
extra_scripts = extra_scripts =
; pre:scripts/build_interface.py ; builds the WebUI post:scripts/rename_fw.py ; renames the firmware .bin file
scripts/rename_fw.py ; renames the firmware .bin file
; scripts/upload.py ; optionally upload the firmware via OTA (if upload_protocol = custom)
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = direct monitor_filters = direct
build_type = release build_type = release
@@ -181,29 +179,14 @@ build_flags =
${common.build_flags} ${common.build_flags}
-DBOARD_C6 -DBOARD_C6
; ; foundation for building and testing natively, standalone without an ESP32.
; Building and testing natively, standalone without an ESP32.
; See https://docs.platformio.org/en/latest/platforms/native.html
;
; It will generate an executable which when run will show the EMS-ESP Console where you can run tests using the `test` command.
;
; See https://docs.platformio.org/en/latest/core/installation/shell-commands.html#piocore-install-shell-commands
;
; to build and run directly on linux: pio run -e native -t exec
;
; to build and run on Windows, it needs winsock for the console input so:
; - For the first time, install Msys2 (https://www.msys2.org/) and the GCC compiler with `run pacman -S mingw-w64-ucrt-x86_64-gcc`
; - Then, build with `pio run -e native` to create the program.exe file
; - run by calling the executable from the Mysys shell e.g. `C:/msys64/msys2_shell.cmd -defterm -here -no-start -ucrt64 -c <location>/.pio/build/native/program.exe`
; - or use with Windows Terminal https://www.msys2.org/docs/terminals/
;
[env:native] [env:native]
platform = native platform = native
build_type = debug build_type = debug
build_flags =
build_src_flags = build_src_flags =
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_STANDALONE -DEMSESP_TEST
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\" -DARDUINOJSON_ENABLE_ARDUINO_STRING=1
-std=gnu++17 -Og -ggdb -std=gnu++17 -Og -ggdb
-Wall -Wextra -Wall -Wextra
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces -Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
@@ -274,8 +257,45 @@ lib_deps = Unity
test_testing_command = test_testing_command =
${platformio.build_dir}/${this.__env__}/program ${platformio.build_dir}/${this.__env__}/program
;
; Building and testing locally on OS, which we call "standalone" without an ESP32.
; See https://docs.platformio.org/en/latest/platforms/native.html
;
; It will generate an executable which when run will show the EMS-ESP Console where you can run tests using the `test` command.
;
; See https://docs.platformio.org/en/latest/core/installation/shell-commands.html#piocore-install-shell-commands
;
; to build and run directly on linux: pio run -e standalone -t exec
;
; to build and run on Windows, it needs winsock for the console input so:
; - For the first time, install Msys2 (https://www.msys2.org/) and the GCC compiler with `run pacman -S mingw-w64-ucrt-x86_64-gcc`
; - Then, build with `pio run -e standalone` to create the program.exe file
; - run by calling the executable from the Mysys shell e.g. `C:/msys64/msys2_shell.cmd -defterm -here -no-start -ucrt64 -c <location>/.pio/build/native/program.exe`
; - or use with Windows Terminal https://www.msys2.org/docs/terminals/
;
[env:standalone]
extends = env:native
build_flags =
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
; Modbus
; Creating the modbus registers is a multi-step process. Before it was in a shell script called generate_csv_and_headers.sh
; but now moved to pio so everything is in python and cross-platform. The logic is as follows:
; 1. create a dummy modbus_entity_parameters.hpp file so the first pass compiles
; 2. compile the EMS-ESPcode with the EMSESP_MODBUS flag set
; 3. run the entity_dump test command and generate the dump_entities.csv file
; 4. use the dump_entities.csv file with update_modbus_registers.py script to generate the modbus_entity_parameters.hpp file
; 5. clean up everything and start again with the EMSESP_STANDALONE flag set
; 6. run the entity_dump test command again to create the real dump_entities.csv file
; 7. create the Modbus-Entity-Registers.md file
; 8. create the dump_telegrams.csv file
;
; To run this in pio use the steps
; pio run -e build_modbus
; pio run -e build_standalone -t clean -t build
# builds the modbus_entity_parameters.hpp header file # builds the modbus_entity_parameters.hpp header file
# pio run -e build_modbus -t build # pio run -e build_modbus
[env:build_modbus] [env:build_modbus]
extends = env:native extends = env:native
targets = build targets = build
@@ -287,8 +307,7 @@ custom_test_command = entity_dump
custom_output_file = dump_entities.csv custom_output_file = dump_entities.csv
custom_post_script = scripts/build_modbus_entity_parameters_post.py custom_post_script = scripts/build_modbus_entity_parameters_post.py
; builds the real dump_entities.csv and dump_telegrams.csv files ; builds the real dump_entities.csv and dump_telegrams.csv files, and also the Modbus-Entity-Registers.md file
; and the Modbus-Entity-Registers.md file
; to be run after build_modbus with: pio run -e build_standalone -t clean -t build ; to be run after build_modbus with: pio run -e build_standalone -t clean -t build
[env:build_standalone] [env:build_standalone]
extends = env:native extends = env:native

View File

@@ -110,7 +110,7 @@ def build_webUI(*args, **kwargs):
env.Exit(1) env.Exit(1)
env.Exit(0) env.Exit(0)
# Create custom target that only runs the script # 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,

View File

@@ -0,0 +1,64 @@
#!/bin/sh
# Builds the dump_*.csv files, modbus headers and modbus documentation.
# Run as `sh scripts/generate_csv_and_headers.sh` from the root of the repository.
##
## IMPORTANT NOTE!
## This script is not used anymore. It is kept for reference only.
## It has been replaced with two pio targets: build_modbus and build_standalone.
##
# create a dummy modbus_entity_parameters.hpp so the first pass compiles
cat >./src/core/modbus_entity_parameters.hpp <<EOL
#include "modbus.h"
#include "emsdevice.h"
/*
* This file is auto-generated. Do not modify.
*/
// clang-format off
namespace emsesp {
using dt = EMSdevice::DeviceType;
#define REGISTER_MAPPING(device_type, device_value_tag_type, long_name, modbus_register_offset, modbus_register_count) \\
{ device_type, device_value_tag_type, long_name[0], modbus_register_offset, modbus_register_count }
// IMPORTANT: This list MUST be ordered by keys "device_type", "device_value_tag_type" and "modbus_register_offset" in this order.
const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_mappings = {};
} // namespace emsesp
// clang-format on
EOL
# First generate Modbus entity parameters
# build the modbus_entity_parameters.hpp header file
make clean
make -s ARGS=-DEMSESP_MODBUS
rm -f ./src/core/modbus_entity_parameters.hpp ./docs/dump_entities.csv
echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_entities.csv
cat ./docs/dump_entities.csv | python3 ./scripts/update_modbus_registers.py > ./src/core/modbus_entity_parameters.hpp
# regenerate dump_entities.csv but without the Modbus entity parameters
make clean
make -s ARGS=-DEMSESP_STANDALONE
rm -f ./docs/dump_entities.csv
echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_entities.csv
# generate Modbus doc - Modbus-Entity-Registers.md used in the emsesp.org documentation
rm -f ./docs/Modbus-Entity-Registers.md
cat ./docs/dump_entities.csv | python3 ./scripts/generate-modbus-register-doc.py > ./docs/Modbus-Entity-Registers.md
# dump_telegrams.csv
rm -f ./docs/dump_telegrams.csv
echo "test telegram_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_telegrams.csv
ls -al ./src/core/modbus_entity_parameters.hpp
ls -al ./docs/Modbus-Entity-Registers.md
ls -al ./docs/dump_entities.csv
ls -al ./docs/dump_telegrams.csv