Merge pull request #3109 from proddy/core3

Core3 updates #3
This commit is contained in:
Proddy
2026-06-07 17:14:05 +02:00
committed by GitHub
45 changed files with 821 additions and 818 deletions

View File

@@ -35,10 +35,10 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"mime-types": "^3.0.2", "mime-types": "^3.0.2",
"preact": "^10.29.2", "preact": "^10.29.2",
"react": "^19.2.6", "react": "^19.2.7",
"react-dom": "^19.2.6", "react-dom": "^19.2.7",
"react-icons": "^5.6.0", "react-icons": "^5.6.0",
"react-router": "^7.16.0", "react-router": "^7.17.0",
"react-toastify": "^11.1.0", "react-toastify": "^11.1.0",
"typesafe-i18n": "^5.27.1", "typesafe-i18n": "^5.27.1",
"typescript": "^6.0.3" "typescript": "^6.0.3"
@@ -47,18 +47,18 @@
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@preact/preset-vite": "^2.10.5", "@preact/preset-vite": "^2.10.5",
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.9.1", "@types/node": "^25.9.2",
"@types/react": "^19.2.15", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"concurrently": "^10.0.0", "concurrently": "^10.0.3",
"eslint": "^10.4.0", "eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"terser": "^5.48.0", "terser": "^5.48.0",
"typescript-eslint": "^8.60.0", "typescript-eslint": "^8.60.1",
"vite": "^8.0.14", "vite": "^8.0.16",
"vite-plugin-imagemin": "^0.6.1" "vite-plugin-imagemin": "^0.6.1"
}, },
"packageManager": "pnpm@10.34.1+sha512.b58fbde6dca66a929538021581f648b4570b6ca19b18e7cbd7f2c07a7b24454155388dacdf08f2af3678e88a6d1fe04f9d609df24bf51735a060ea041b374ab7" "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
} }

662
interface/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
allowBuilds:
cwebp-bin: false
esbuild: false
gifsicle: false
jpegtran-bin: false
mozjpeg: false
optipng-bin: false
pngquant-bin: false
minimumReleaseAgeExclude:
- '@types/node@25.9.2'
- '@types/react@19.2.17'

View File

@@ -7,6 +7,7 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import HelpOutlineIcon from '@mui/icons-material/HelpOutlined'; import HelpOutlineIcon from '@mui/icons-material/HelpOutlined';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess'; import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore'; import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
import { import {
@@ -75,24 +76,25 @@ const Dashboard = memo(() => {
{ {
immediate: false immediate: false
} }
); )
.onSuccess(() => {
toast.success(LL.WRITE_CMD_SENT());
})
.onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
const deviceValueDialogSave = async (devicevalue: DeviceValue) => { const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
if (!selectedDashboardItem) { if (!selectedDashboardItem) {
return; return;
} }
const id = selectedDashboardItem.parentNode.id; // this is the parent ID // skip if we're executing an immediate schedule as this is handled in DevicesDialog::doAction()
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v }) if (devicevalue.v !== undefined) {
.then(() => { const id = selectedDashboardItem.parentNode.id; // this is the parent ID
toast.success(LL.WRITE_CMD_SENT()); await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v });
}) }
.catch((error: Error) => { setDeviceValueDialogOpen(false);
toast.error(error.message); setSelectedDashboardItem(undefined);
})
.finally(() => {
setDeviceValueDialogOpen(false);
setSelectedDashboardItem(undefined);
});
}; };
const dashboard_theme = useTheme({ const dashboard_theme = useTheme({
@@ -210,7 +212,12 @@ const Dashboard = memo(() => {
(parseInt(id.slice(0, 2), 16) & mask) === mask; (parseInt(id.slice(0, 2), 16) & mask) === mask;
const editDashboardValue = (di: DashboardItem) => { const editDashboardValue = (di: DashboardItem) => {
if (me.admin && di.dv?.c) { // don't execute on parent nodes
if (!me.admin || di.id <= 99) {
return;
}
if (di.dv?.c) {
setSelectedDashboardItem(di); setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true); setDeviceValueDialogOpen(true);
} }
@@ -321,7 +328,19 @@ const Dashboard = memo(() => {
<Cell> <Cell>
{me.admin && {me.admin &&
di.dv?.c && di.dv?.c &&
!hasMask(di.dv.id, DeviceEntityMask.DV_READONLY) && ( !hasMask(di.dv.id, DeviceEntityMask.DV_READONLY) &&
(di.dv.v === '' || di.dv.v === undefined ? (
<IconButton
size="small"
aria-label={LL.RUN_COMMAND()}
onClick={() => editDashboardValue(di)}
>
<PlayArrowIcon
color="primary"
sx={{ fontSize: 16 }}
/>
</IconButton>
) : (
<IconButton <IconButton
size="small" size="small"
aria-label={ aria-label={
@@ -334,7 +353,7 @@ const Dashboard = memo(() => {
sx={{ fontSize: 16 }} sx={{ fontSize: 16 }}
/> />
</IconButton> </IconButton>
)} ))}
</Cell> </Cell>
</> </>
) : ( ) : (

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
@@ -18,7 +19,9 @@ import {
Typography Typography
} from '@mui/material'; } from '@mui/material';
import { callAction } from '@/api/app';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import type Schema from 'async-validator'; import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components'; import { ValidatedTextField } from 'components';
@@ -61,13 +64,29 @@ const DevicesDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const save = async () => { const { send: executeSchedule } = useRequest(
(id: string) => callAction({ action: 'executeSchedule', param: id }),
{ immediate: false }
)
.onSuccess(() => {
toast.success(LL.EXECUTE_SCHEDULE_SENT());
})
.onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
const doAction = async () => {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, editItem); if (editItem.v === undefined && editItem.c !== undefined) {
onSave(editItem); await executeSchedule(editItem.c);
} else {
await validate(validator, editItem);
}
} catch (error) { } catch (error) {
setFieldErrors((error as ValidationError).fieldErrors); setFieldErrors((error as ValidationError).fieldErrors);
} finally {
onSave(editItem);
} }
}; };
@@ -100,9 +119,14 @@ const DevicesDialog = ({
return undefined; return undefined;
}; };
const isCommand = selectedItem.v === '' && selectedItem.c; const isCommand =
(selectedItem.v === '' || selectedItem.v === undefined) &&
Boolean(selectedItem.c);
const isSchedulerImmediate = selectedItem.v === undefined;
const dialogTitle = isCommand const dialogTitle = isCommand
? LL.RUN_COMMAND() ? isSchedulerImmediate
? LL.EXECUTE() + ' ' + LL.SCHEDULE(0)
: LL.RUN_COMMAND()
: writeable : writeable
? LL.CHANGE_VALUE() ? LL.CHANGE_VALUE()
: LL.VALUE(0); : LL.VALUE(0);
@@ -118,67 +142,69 @@ const DevicesDialog = ({
<Typography sx={{ mb: 2 }} color="warning" variant="body2"> <Typography sx={{ mb: 2 }} color="warning" variant="body2">
{editItem.id.slice(2)} {editItem.id.slice(2)}
</Typography> </Typography>
<Grid container> {!isSchedulerImmediate && (
<Grid size={12}> <Grid container>
{editItem.l ? ( <Grid size={12}>
<TextField {editItem.l ? (
name="v" <TextField
value={editItem.v} name="v"
aria-label={valueLabel} value={editItem.v}
disabled={!writeable} aria-label={valueLabel}
sx={{ width: '30ch' }} disabled={!writeable}
select sx={{ width: '30ch' }}
onChange={updateFormValue} select
> onChange={updateFormValue}
{editItem.l.map((val) => ( >
<MenuItem value={val} key={val}> {editItem.l.map((val) => (
{val} <MenuItem value={val} key={val}>
</MenuItem> {val}
))} </MenuItem>
</TextField> ))}
) : editItem.s || editItem.u !== DeviceValueUOM.NONE ? ( </TextField>
<ValidatedTextField ) : editItem.s || editItem.u !== DeviceValueUOM.NONE ? (
fieldErrors={fieldErrors || {}} <ValidatedTextField
name="v" fieldErrors={fieldErrors || {}}
label={valueLabel} name="v"
value={numberValue(Math.round((editItem.v as number) * 10) / 10)} label={valueLabel}
autoFocus value={numberValue(Math.round((editItem.v as number) * 10) / 10)}
disabled={!writeable} autoFocus
type="number" disabled={!writeable}
sx={{ width: '30ch' }} type="number"
onChange={updateFormValue} sx={{ width: '30ch' }}
slotProps={{ onChange={updateFormValue}
htmlInput: editItem.s slotProps={{
? { min: editItem.m, max: editItem.x, step: editItem.s } htmlInput: editItem.s
: {}, ? { min: editItem.m, max: editItem.x, step: editItem.s }
input: { : {},
startAdornment: ( input: {
<InputAdornment position="start"> startAdornment: (
{setUom(editItem.u)} <InputAdornment position="start">
</InputAdornment> {setUom(editItem.u)}
) </InputAdornment>
} )
}} }
/> }}
) : ( />
<ValidatedTextField ) : (
fieldErrors={fieldErrors || {}} <ValidatedTextField
name="v" fieldErrors={fieldErrors || {}}
label={valueLabel} name="v"
value={editItem.v} label={valueLabel}
disabled={!writeable} value={editItem.v}
sx={{ width: '30ch' }} disabled={!writeable}
multiline={!editItem.u} sx={{ width: '30ch' }}
onChange={updateFormValue} multiline={!editItem.u}
/> onChange={updateFormValue}
/>
)}
</Grid>
{writeable && helperText && (
<Grid>
<FormHelperText>{helperText}</FormHelperText>
</Grid>
)} )}
</Grid> </Grid>
{writeable && helperText && ( )}
<Grid>
<FormHelperText>{helperText}</FormHelperText>
</Grid>
)}
</Grid>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@@ -202,7 +228,7 @@ const DevicesDialog = ({
<Button <Button
startIcon={<WarningIcon color="warning" />} startIcon={<WarningIcon color="warning" />}
variant="outlined" variant="outlined"
onClick={save} onClick={doAction}
color="primary" color="primary"
> >
{buttonLabel} {buttonLabel}

View File

@@ -134,9 +134,13 @@ const SchedulerDialog = ({
const { send: executeSchedule } = useRequest( const { send: executeSchedule } = useRequest(
(id: string) => callAction({ action: 'executeSchedule', param: id }), (id: string) => callAction({ action: 'executeSchedule', param: id }),
{ immediate: false } { immediate: false }
).onError((error) => { )
toast.error(String(error.error?.message || 'An error occurred')); .onSuccess(() => {
}); toast.success(LL.EXECUTE_SCHEDULE_SENT());
})
.onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
const execute = async () => { const execute = async () => {
await executeSchedule(editItem.name); await executeSchedule(editItem.name);

View File

@@ -1,4 +1,5 @@
export interface Settings { export interface Settings {
system_name: string;
locale: string; locale: string;
tx_mode: number; tx_mode: number;
ems_bus_id: number; ems_bus_id: number;

View File

@@ -189,7 +189,19 @@ const ApplicationSettings = () => {
return ( return (
<> <>
<Typography sx={{ pb: 1 }} variant="h6" color="primary"> <Typography variant="h6" color="primary">
{LL.SYSTEM(0)}&nbsp;
{LL.CUSTOMIZATIONS()}
</Typography>
<TextField
name="system_name"
label={LL.SYSTEM_NAME()}
value={data.system_name}
variant="outlined"
onChange={updateFormValue}
margin="normal"
/>
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.SERVICES()} {LL.SERVICES()}
</Typography> </Typography>
<Typography color="secondary">API</Typography> <Typography color="secondary">API</Typography>

View File

@@ -145,9 +145,21 @@ const SystemMonitor = () => {
{LL.PLEASE_WAIT()}&hellip; {LL.PLEASE_WAIT()}&hellip;
</Typography> </Typography>
{isUploading && ( {isUploading && (
<Box sx={{ width: '100%', pl: 2, pr: 2, py: 2 }}> <>
<LinearProgressWithLabel value={progressValue} /> <Box sx={{ width: '100%', pl: 2, pr: 2, py: 2 }}>
</Box> <LinearProgressWithLabel value={progressValue} />
</Box>
<Button
sx={{ ml: 2, mt: 2 }}
startIcon={<CancelIcon />}
variant="outlined"
color="secondary"
onClick={onCancel}
>
{LL.CANCEL()}
</Button>
</>
)} )}
</> </>
)} )}

View File

@@ -2,6 +2,9 @@ import { memo } from 'react';
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material'; import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
import { readSettings } from 'api/app';
import { useRequest } from 'alova/client';
import { PROJECT_NAME } from 'env'; import { PROJECT_NAME } from 'env';
import { DRAWER_WIDTH } from './Layout'; import { DRAWER_WIDTH } from './Layout';
@@ -24,12 +27,28 @@ interface LayoutDrawerProps {
} }
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => { const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
const { data: settings } = useRequest(readSettings);
const system_name = settings?.system_name;
const drawer = ( const drawer = (
<> <>
<Toolbar disableGutters> <Toolbar disableGutters>
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} /> <LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
<Typography variant="h6">{PROJECT_NAME}</Typography> <Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}
>
<Typography>{PROJECT_NAME}</Typography>
{system_name && (
<Typography color="secondary" variant="body2">
{system_name}
</Typography>
)}
</Box>
</Box> </Box>
<Divider absolute /> <Divider absolute />
</Toolbar> </Toolbar>

View File

@@ -363,7 +363,9 @@ const cz: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor po instalaci nové verze.', UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor po instalaci nové verze.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.', UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.',
WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?', WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?',
TEST_EMAIL_SUCCESSFUL: 'Test email byl úspěšně odeslán' TEST_EMAIL_SUCCESSFUL: 'Test email byl úspěšně odeslán',
SYSTEM_NAME: 'Název systému',
EXECUTE_SCHEDULE_SENT: 'Plán byl úspěšně proveden'
}; };
export default cz; export default cz;

View File

@@ -363,7 +363,9 @@ const de: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Für diese Aktualisierung ist ein Werksreset erforderlich. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.', UPGRADE_IMPORTANT_MESSAGES_1: 'Für diese Aktualisierung ist ein Werksreset erforderlich. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.', UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.',
WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und Einstellungen erstellen. Alle Passwörter werden in dieser Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?', WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und Einstellungen erstellen. Alle Passwörter werden in dieser Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?',
TEST_EMAIL_SUCCESSFUL: 'Test email erfolgreich gesendet' TEST_EMAIL_SUCCESSFUL: 'Test email erfolgreich gesendet',
SYSTEM_NAME: 'Systemname',
EXECUTE_SCHEDULE_SENT: 'Zeitplan erfolgreich ausgeführt'
}; };
export default de; export default de;

View File

@@ -363,7 +363,9 @@ const en: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file after the new version is installed.', UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file after the new version is installed.',
UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.', UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.',
WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?', WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?',
TEST_EMAIL_SUCCESSFUL: 'Test email sent successfully' TEST_EMAIL_SUCCESSFUL: 'Test email sent successfully',
SYSTEM_NAME: 'System Name',
EXECUTE_SCHEDULE_SENT: 'Schedule executed successfully'
}; };
export default en; export default en;

View File

@@ -363,7 +363,9 @@ const fr: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système avant de continuer, et de la charger après l\'installation de la nouvelle version.', UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système avant de continuer, et de la charger après l\'installation de la nouvelle version.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.', UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.',
WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?', WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?',
TEST_EMAIL_SUCCESSFUL: 'Test email envoyé avec succès' TEST_EMAIL_SUCCESSFUL: 'Test email envoyé avec succès',
SYSTEM_NAME: 'Nom du système',
EXECUTE_SCHEDULE_SENT: 'Planlegger exécuté avec succès'
}; };
export default fr; export default fr;

View File

@@ -363,7 +363,9 @@ const it: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file dopo l\'installazione della nuova versione.', UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file dopo l\'installazione della nuova versione.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.', UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.',
WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?', WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?',
TEST_EMAIL_SUCCESSFUL: 'Test email inviata con successo' TEST_EMAIL_SUCCESSFUL: 'Test email inviata con successo',
SYSTEM_NAME: 'Nome del sistema',
EXECUTE_SCHEDULE_SENT: 'Programma eseguito con successo'
}; };
export default it; export default it;

View File

@@ -363,7 +363,9 @@ const nl: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file na de installatie van de nieuwe versie.', UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file na de installatie van de nieuwe versie.',
UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.', UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.',
WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?', WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?',
TEST_EMAIL_SUCCESSFUL: 'Test email verzonden succesvol' TEST_EMAIL_SUCCESSFUL: 'Test email verzonden succesvol',
SYSTEM_NAME: 'Systeemnaam',
EXECUTE_SCHEDULE_SENT: 'Planlegger uitgevoerd succesvol'
}; };
export default nl; export default nl;

View File

@@ -363,7 +363,9 @@ const no: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen etter at den nye versjonen er installert.', UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen etter at den nye versjonen er installert.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.', UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.',
WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?', WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?',
TEST_EMAIL_SUCCESSFUL: 'Test email sendt suksessfullt' TEST_EMAIL_SUCCESSFUL: 'Test email sendt suksessfullt',
SYSTEM_NAME: 'Systemnavn',
EXECUTE_SCHEDULE_SENT: 'Planlegger utført suksessfullt'
}; };
export default no; export default no;

View File

@@ -363,7 +363,9 @@ const pl: BaseTranslation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik po zainstalowaniu nowej wersji.', UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik po zainstalowaniu nowej wersji.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.', UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.',
WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?', WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?',
TEST_EMAIL_SUCCESSFUL: 'Test email wysłany pomyślnie' TEST_EMAIL_SUCCESSFUL: 'Test email wysłany pomyślnie',
SYSTEM_NAME: 'Nazwa systemu',
EXECUTE_SCHEDULE_SENT: 'Harmonogram wykonany pomyślnie'
}; };
export default pl; export default pl;

View File

@@ -363,7 +363,9 @@ const sk: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor po instalácii novej verzie.', UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor po instalácii novej verzie.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.', UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.',
WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?', WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?',
TEST_EMAIL_SUCCESSFUL: 'Test email bol úspešne odoslaný' TEST_EMAIL_SUCCESSFUL: 'Test email bol úspešne odoslaný',
SYSTEM_NAME: 'Názov systému',
EXECUTE_SCHEDULE_SENT: 'Plán bol úspešne vykonaný'
}; };
export default sk; export default sk;

View File

@@ -363,7 +363,9 @@ const sv: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil efter att den nya versionen är installerad.', UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil efter att den nya versionen är installerad.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.', UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.',
WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?', WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?',
TEST_EMAIL_SUCCESSFUL: 'Test email skickad lyckades' TEST_EMAIL_SUCCESSFUL: 'Test email skickad lyckades',
SYSTEM_NAME: 'Systemnamn',
EXECUTE_SCHEDULE_SENT: 'Schema utfört'
}; };
export default sv; export default sv;

View File

@@ -363,7 +363,9 @@ const tr: Translation = {
UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.', UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Yeni bir büyük sürüme yükselteceksiniz. Değişiklikleri ChangeLogı okuduğunuzdan emin olun.', UPGRADE_IMPORTANT_MESSAGES_2: 'Yeni bir büyük sürüme yükselteceksiniz. Değişiklikleri ChangeLogı okuduğunuzdan emin olun.',
WARNING_SYSTEM_BACKUP: 'Bu, sistem yapılandırmanızı ve ayarlarınızın bir yedeklemesi oluşturacaktır. Tüm şifreler yedekleme dosyasında okunabilir olacaktır. Paylaşırken dikkatli olun! Devam etmek istediğinize emin misiniz?', WARNING_SYSTEM_BACKUP: 'Bu, sistem yapılandırmanızı ve ayarlarınızın bir yedeklemesi oluşturacaktır. Tüm şifreler yedekleme dosyasında okunabilir olacaktır. Paylaşırken dikkatli olun! Devam etmek istediğinize emin misiniz?',
TEST_EMAIL_SUCCESSFUL: 'Test email başarıyla gönderildi' TEST_EMAIL_SUCCESSFUL: 'Test email başarıyla gönderildi',
SYSTEM_NAME: 'Sistem Adı',
EXECUTE_SCHEDULE_SENT: 'Zamanlama başarıyla uygulandı'
}; };
export default tr; export default tr;

View File

@@ -15,5 +15,5 @@
"itty-router": "^5.0.23", "itty-router": "^5.0.23",
"prettier": "^3.8.3" "prettier": "^3.8.3"
}, },
"packageManager": "pnpm@10.34.1+sha512.b58fbde6dca66a929538021581f648b4570b6ca19b18e7cbd7f2c07a7b24454155388dacdf08f2af3678e88a6d1fe04f9d609df24bf51735a060ea041b374ab7" "packageManager": "pnpm@11.5.2+sha512.71c631e382066efc25625d5cf029075de07b61b37f6e27350fbd84b1bda5864c8c1967adc280776b45c30a715c0359a3be08fef42d5bb09e2b99029979692916"
} }

View File

@@ -15,6 +15,7 @@ const headers = {
// EMS-ESP Application Settings // EMS-ESP Application Settings
let settings = { let settings = {
system_name: 'standalone',
locale: 'en', locale: 'en',
tx_mode: 1, tx_mode: 1,
ems_bus_id: 11, ems_bus_id: 11,
@@ -229,6 +230,21 @@ let countWifiScanPoll = 0; // wifi network scan
let countHardwarePoll = 0; // for during an upload let countHardwarePoll = 0; // for during an upload
// DeviceTypes // DeviceTypes
const enum ScheduleFlag {
SCHEDULE_SUN = 1,
SCHEDULE_MON = 2,
SCHEDULE_TUE = 4,
SCHEDULE_WED = 8,
SCHEDULE_THU = 16,
SCHEDULE_FRI = 32,
SCHEDULE_SAT = 64,
SCHEDULE_DAY = 0,
SCHEDULE_TIMER = 128,
SCHEDULE_ONCHANGE = 129,
SCHEDULE_CONDITION = 130,
SCHEDULE_IMMEDIATE = 132
}
const enum DeviceType { const enum DeviceType {
SYSTEM = 0, SYSTEM = 0,
TEMPERATURESENSOR, TEMPERATURESENSOR,
@@ -4164,7 +4180,7 @@ let emsesp_schedule = {
{ {
id: 4, id: 4,
active: false, active: false,
flags: 1, flags: ScheduleFlag.SCHEDULE_TIMER,
time: '04:00', time: '04:00',
cmd: 'system/restart', cmd: 'system/restart',
value: '', value: '',
@@ -4173,7 +4189,7 @@ let emsesp_schedule = {
{ {
id: 5, id: 5,
active: false, active: false,
flags: 130, flags: ScheduleFlag.SCHEDULE_CONDITION,
time: 'system/network info/rssi < -70', time: 'system/network info/rssi < -70',
cmd: 'system/restart', cmd: 'system/restart',
value: '', value: '',
@@ -4182,7 +4198,7 @@ let emsesp_schedule = {
{ {
id: 6, id: 6,
active: false, active: false,
flags: 129, flags: ScheduleFlag.SCHEDULE_ONCHANGE,
time: 'boiler/outdoortemp', time: 'boiler/outdoortemp',
cmd: 'boiler/selflowtemp', cmd: 'boiler/selflowtemp',
value: '(custom/setpoint - boiler/outdoortemp) * 2.8 + 3', value: '(custom/setpoint - boiler/outdoortemp) * 2.8 + 3',
@@ -4191,11 +4207,11 @@ let emsesp_schedule = {
{ {
id: 7, id: 7,
active: false, active: false,
flags: 132, flags: ScheduleFlag.SCHEDULE_IMMEDIATE,
time: '', time: '',
cmd: 'system/message', cmd: 'system/message',
value: '"hello world"', value: '"hello world"',
name: '' // empty name: 'send_message'
} }
] ]
}; };
@@ -4756,9 +4772,13 @@ router
id: DeviceTypeUniqueID.SCHEDULER_UID * 100 + index, id: DeviceTypeUniqueID.SCHEDULER_UID * 100 + index,
dv: { dv: {
id: '00' + item.name, id: '00' + item.name,
v: item.active ? 'on' : 'off',
c: item.name, c: item.name,
l: ['off', 'on'] ...(item.flags === ScheduleFlag.SCHEDULE_IMMEDIATE
? {}
: {
v: item.active ? 'on' : 'off',
l: ['off', 'on']
})
} }
})); }));
dashboard_object = { dashboard_object = {

View File

@@ -1340,4 +1340,5 @@ sendmail
serialises serialises
SPIRAM SPIRAM
optimisations optimisations
IILE IILE
Sumr

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Regenerate test/test_api/test_api.h from the native-test-create build, then run the API tests.
Workflow:
1. Run `pio run -e native-test-create -t exec` and capture its output.
2. Extract everything between the START/END "CUT HERE" markers.
3. Write that block to test/test_api/test_api.h.
4. Run `pio run -e native-test -t exec`.
"""
import subprocess
import sys
from pathlib import Path
START_MARKER = "// ---------- START - CUT HERE ----------"
END_MARKER = "// ---------- END - CUT HERE ----------"
# project root is the parent of this script's "scripts" directory
PROJECT_ROOT = Path(__file__).resolve().parent.parent
OUTPUT_HEADER = PROJECT_ROOT / "test" / "test_api" / "test_api.h"
def run(cmd, capture):
"""Run a command in the project root. Streams to the console; optionally captures stdout."""
print(f"\n>>> {' '.join(cmd)}\n", flush=True)
if capture:
# capture stdout while still echoing it so the user sees progress
result = subprocess.run(cmd, cwd=PROJECT_ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
print(result.stdout, end="", flush=True)
return result.returncode, result.stdout
result = subprocess.run(cmd, cwd=PROJECT_ROOT)
return result.returncode, None
def extract_between_markers(output):
"""Return the text strictly between the START and END markers (markers excluded)."""
lines = output.splitlines()
start = end = None
for i, line in enumerate(lines):
if START_MARKER in line and start is None:
start = i
elif END_MARKER in line and start is not None:
end = i
break
if start is None or end is None:
return None
return "\n".join(lines[start + 1 : end]) + "\n"
def main():
# 1. build + exec the generator
code, output = run(["pio", "run", "-e", "native-test-create", "-t", "exec"], capture=True)
if code != 0:
print(f"\nERROR: 'native-test-create' exec failed with exit code {code}", file=sys.stderr)
return code
# 2. extract the header content
content = extract_between_markers(output)
if content is None:
print(f"\nERROR: could not find content between markers:\n {START_MARKER}\n {END_MARKER}", file=sys.stderr)
return 1
# 3. write it to test_api.h
OUTPUT_HEADER.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_HEADER.write_text(content, encoding="utf-8")
print(f"\nWrote {len(content.splitlines())} lines to {OUTPUT_HEADER}", flush=True)
# 4. build + exec the actual tests
code, _ = run(["pio", "run", "-e", "native-test", "-t", "exec"], capture=False)
if code != 0:
print(f"\nERROR: 'native-test' exec failed with exit code {code}", file=sys.stderr)
return code
if __name__ == "__main__":
sys.exit(main())

View File

@@ -5,7 +5,7 @@
cd interface cd interface
rm -rf node_modules rm -rf node_modules
corepack use pnpm@latest-10 corepack use pnpm@latest
pnpm update --latest pnpm update --latest
pnpm install pnpm install
pnpm format pnpm format
@@ -13,7 +13,7 @@ pnpm lint
cd ../mock-api cd ../mock-api
rm -rf node_modules rm -rf node_modules
corepack use pnpm@latest-10 corepack use pnpm@latest
pnpm update --latest pnpm update --latest
pnpm install pnpm install
pnpm format pnpm format

View File

@@ -26,30 +26,30 @@ std::vector<uint8_t> AnalogSensor::exclude_types_;
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
unsigned long AnalogSensor::edge[] = {0, 0, 0}; volatile unsigned long AnalogSensor::edge[] = {0, 0, 0};
unsigned long AnalogSensor::edgecnt[] = {0, 0, 0}; volatile unsigned long AnalogSensor::edgecnt[] = {0, 0, 0};
void IRAM_ATTR AnalogSensor::freqIrq0() { void IRAM_ATTR AnalogSensor::freqIrq0() {
portENTER_CRITICAL_ISR(&mux); portENTER_CRITICAL_ISR(&mux);
if (micros() - edge[0] > 10) { // limit to 100kHz if (micros() - edge[0] > 10) { // limit to 100kHz
edgecnt[0]++; edgecnt[0] = edgecnt[0] + 1;
edge[0] = micros(); edge[0] = micros();
} }
portEXIT_CRITICAL_ISR(&mux); portEXIT_CRITICAL_ISR(&mux);
} }
void IRAM_ATTR AnalogSensor::freqIrq1() { void IRAM_ATTR AnalogSensor::freqIrq1() {
portENTER_CRITICAL_ISR(&mux); portENTER_CRITICAL_ISR(&mux);
if (micros() - edge[1] > 10) { // limit to 100kHz if (micros() - edge[1] > 10) { // limit to 100kHz
edgecnt[1]++; edgecnt[1] = edgecnt[1] + 1;
edge[1] = micros(); edge[1] = micros();
} }
portEXIT_CRITICAL_ISR(&mux); portEXIT_CRITICAL_ISR(&mux);
} }
void IRAM_ATTR AnalogSensor::freqIrq2() { void IRAM_ATTR AnalogSensor::freqIrq2() {
portENTER_CRITICAL_ISR(&mux); portENTER_CRITICAL_ISR(&mux);
if (micros() - edge[2] > 10) { // limit to 100kHz if (micros() - edge[2] > 10) { // limit to 100kHz
edgecnt[2]++; edgecnt[2] = edgecnt[2] + 1;
edge[2] = micros(); edge[2] = micros();
} }
portEXIT_CRITICAL_ISR(&mux); portEXIT_CRITICAL_ISR(&mux);
} }
@@ -272,8 +272,10 @@ void AnalogSensor::reload(bool get_nvs) {
sensor.set_value(0); sensor.set_value(0);
publish_sensor(sensor); publish_sensor(sensor);
attachInterrupt(sensor.gpio(), index == 0 ? freqIrq0 : index == 1 ? freqIrq1 : freqIrq2, FALLING); attachInterrupt(sensor.gpio(), index == 0 ? freqIrq0 : index == 1 ? freqIrq1 : freqIrq2, FALLING);
lastedge[index] = edge[index] = micros(); unsigned long now = micros();
edgecnt[index] = 0; edge[index] = now;
lastedge[index] = now;
edgecnt[index] = 0;
} else if (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2) { } else if (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2) {
auto index = sensor.type() - AnalogType::CNT_0; auto index = sensor.type() - AnalogType::CNT_0;
LOG_DEBUG("Counter %d on GPIO %02d", index, sensor.gpio()); LOG_DEBUG("Counter %d on GPIO %02d", index, sensor.gpio());

View File

@@ -174,10 +174,10 @@ class AnalogSensor {
return sensors_.size(); return sensors_.size();
} }
bool update(uint8_t gpio, const char * name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system); bool update(uint8_t gpio, const char * name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system);
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
void store_counters(); void store_counters();
std::string get_metrics_prometheus(); std::string get_metrics_prometheus();
static const std::vector<uint8_t> & exclude_types() { static const std::vector<uint8_t> & exclude_types() {
return exclude_types_; return exclude_types_;
} }
@@ -210,8 +210,8 @@ class AnalogSensor {
static void IRAM_ATTR freqIrq0(); static void IRAM_ATTR freqIrq0();
static void IRAM_ATTR freqIrq1(); static void IRAM_ATTR freqIrq1();
static void IRAM_ATTR freqIrq2(); static void IRAM_ATTR freqIrq2();
static unsigned long edge[3]; static volatile unsigned long edge[3]; // written from freqIrqN() ISRs, read from the main measure() loop (partly outside the critical section)
static unsigned long edgecnt[3]; static volatile unsigned long edgecnt[3]; // written from freqIrqN() ISRs, read from the main measure() loop (partly outside the critical section)
unsigned long lastedge[3] = {0, 0, 0}; unsigned long lastedge[3] = {0, 0, 0};
#endif #endif
}; };

View File

@@ -21,6 +21,10 @@
// GENERAL SETTINGS // GENERAL SETTINGS
#ifndef EMSESP_DEFAULT_SYSTEM_NAME
#define EMSESP_DEFAULT_SYSTEM_NAME ""
#endif
#ifndef EMSESP_DEFAULT_LOCALE #ifndef EMSESP_DEFAULT_LOCALE
#define EMSESP_DEFAULT_LOCALE EMSESP_LOCALE_EN // English #define EMSESP_DEFAULT_LOCALE EMSESP_LOCALE_EN // English
#endif #endif

View File

@@ -347,8 +347,9 @@ std::string EMSdevice::to_string() {
} }
// returns string of EMS device version and productID // returns string of EMS device version and productID
// this is used in the MQTT Discovery config
std::string EMSdevice::to_string_version() { std::string EMSdevice::to_string_version() {
return "DeviceID:" + Helpers::hextoa(device_id_) + " ProductID:" + Helpers::itoa(product_id_) + " Version:" + version_; return "DeviceID " + Helpers::hextoa(device_id_) + ", ProductID " + Helpers::itoa(product_id_) + ", Version " + version_;
} }
// returns out brand + device name // returns out brand + device name

View File

@@ -49,34 +49,34 @@ class DeviceValue {
// also used with HA as uom // also used with HA as uom
// shows also the HA device class being used // shows also the HA device class being used
enum DeviceValueUOM : uint8_t { enum DeviceValueUOM : uint8_t {
NONE = 0, // 0 NONE = 0, // 0
DEGREES, // 1 - °C - temperature DEGREES, // 1 - °C - temperature
DEGREES_R, // 2 - °C (relative temperature) - temperature DEGREES_R, // 2 - °C (relative temperature) - temperature
PERCENT, // 3 - % - power factor PERCENT, // 3 - % - power factor
LMIN, // 4 - l/min - volume flow rate LMIN, // 4 - l/min - volume flow rate
KWH, // 5 - kWh - energy KWH, // 5 - kWh - energy
WH, // 6 - Wh - energy WH, // 6 - Wh - energy
HOURS, // 7 - h - duration HOURS, // 7 - h - duration
MINUTES, // 8 - m - duration MINUTES, // 8 - m - duration
UA, // 9 - µA - current UA, // 9 - µA - current
BAR, // 10 - bar - pressure BAR, // 10 - bar - pressure
KW, // 11 - kW - power KW, // 11 - kW - power
W, // 12 - W - power W, // 12 - W - power
KB, // 13 - kB - data size KB, // 13 - kB - data size
SECONDS, // 14 - s - duration SECONDS, // 14 - s - duration
DBM, // 15 - dBm - signal strength DBM, // 15 - dBm - signal strength
FAHRENHEIT, // 16 - °F - temperature FAHRENHEIT, // 16 - °F - temperature
MV, // 17 - mV - voltage MV, // 17 - mV - voltage
SQM, // 18 - m² - area SQM, // 18 - m² - area
M3, // 19 - m³ - volume M3, // 19 - m³ - volume
L, // 20 - L - volume L, // 20 - L - volume
KMIN, // 21 - K*min KMIN, // 21 - K*min
K, // 22 - K - temperature K, // 22 - K - temperature
VOLTS, // 23 - V - voltage VOLTS, // 23 - V - voltage
MBAR, // 24 - mbar - atmospheric pressure MBAR, // 24 - mbar - atmospheric pressure
LH, // 25 - l/h - volume flow rate LH, // 25 - l/h - volume flow rate
CTKWH, // 26 - ct/kWh - monetary CTKWH, // 26 - ct/kWh - monetary
HERTZ, // 27 - Hz - frequency HERTZ, // 27 - Hz - frequency
CONNECTIVITY, // 28 - used in HA - connectivity CONNECTIVITY, // 28 - used in HA - connectivity
TIMESTAMP, // 29 - used in HA - timestamp TIMESTAMP, // 29 - used in HA - timestamp
}; };

View File

@@ -545,8 +545,8 @@ void Mqtt::ha_status() {
JsonObject dev = doc["dev"].to<JsonObject>(); JsonObject dev = doc["dev"].to<JsonObject>();
dev["name"] = Mqtt::basename(); dev["name"] = Mqtt::basename();
dev["sw"] = "v" + std::string(EMSESP_APP_VERSION); dev["sw"] = "v" + std::string(EMSESP_APP_VERSION);
dev["mf"] = "EMS-ESP"; dev["mf"] = "EMS-ESP"; // manufacturer is EMS-ESP always
dev["mdl"] = "EMS-ESP"; dev["mdl"] = EMSESP::system_.system_name().empty() ? "EMS-ESP" : EMSESP::system_.system_name(); // use users custom system name if set
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
dev["cu"] = std::string("http://") + EMSESP::system_.get_ip_or_hostname().c_str(); dev["cu"] = std::string("http://") + EMSESP::system_.get_ip_or_hostname().c_str();
#endif #endif

View File

@@ -82,7 +82,7 @@ void Network::begin() {
WiFi.persistent(false); WiFi.persistent(false);
WiFi.setAutoReconnect(false); WiFi.setAutoReconnect(false);
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.disconnect(true, true); // wipe old settings in NVS WiFi.disconnect(true, true); // wipe old settings in NVS
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
WiFi.setHostname(hostname_.c_str()); // updates shared default_hostname buffer WiFi.setHostname(hostname_.c_str()); // updates shared default_hostname buffer
WiFi.enableSTA(true); // creates the STA netif WiFi.enableSTA(true); // creates the STA netif

View File

@@ -684,6 +684,7 @@ void System::store_settings(WebSettings & settings) {
enum_format_ = settings.enum_format; enum_format_ = settings.enum_format;
readonly_mode_ = settings.readonly_mode; readonly_mode_ = settings.readonly_mode;
locale_ = settings.locale; locale_ = settings.locale;
system_name_ = settings.system_name;
developer_mode_ = settings.developer_mode; developer_mode_ = settings.developer_mode;
} }
@@ -836,8 +837,9 @@ void System::send_info_mqtt() {
} }
_connection = connection; _connection = connection;
JsonDocument doc; JsonDocument doc;
// doc["event"] = "connected";
doc["version"] = EMSESP_APP_VERSION; doc["version"] = EMSESP_APP_VERSION;
doc["systemName"] = system_name_.isEmpty() ? "EMS-ESP" : system_name_;
// if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38) // if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38)
// https://github.com/emsesp/EMS-ESP32/issues/751 // https://github.com/emsesp/EMS-ESP32/issues/751
@@ -852,16 +854,6 @@ void System::send_info_mqtt() {
if (EMSESP::network_.ethernet_connected()) { if (EMSESP::network_.ethernet_connected()) {
doc["network"] = "ethernet"; doc["network"] = "ethernet";
doc["hostname"] = ETH.getHostname(); doc["hostname"] = ETH.getHostname();
/*
doc["MAC"] = ETH.macAddress();
doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
doc["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP());
doc["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP());
if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") {
doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
}
*/
} else if (EMSESP::network_.wifi_connected()) { } else if (EMSESP::network_.wifi_connected()) {
doc["network"] = "wifi"; doc["network"] = "wifi";
doc["hostname"] = WiFi.getHostname(); doc["hostname"] = WiFi.getHostname();
@@ -1532,8 +1524,8 @@ bool System::check_upgrade() {
return StateUpdateResult::UNCHANGED; return StateUpdateResult::UNCHANGED;
}); });
// Scheduler name is now mandatory, update FS // Scheduler name is now mandatory, update FS
uint8_t i = 0; uint8_t i = 0;
bool schedule_changed = false; bool schedule_changed = false;
EMSESP::webSchedulerService.update([&](WebScheduler & scheduler) { EMSESP::webSchedulerService.update([&](WebScheduler & scheduler) {
for (ScheduleItem & scheduleItem : scheduler.scheduleItems) { for (ScheduleItem & scheduleItem : scheduler.scheduleItems) {
if (scheduleItem.name[0] == '\0') { if (scheduleItem.name[0] == '\0') {
@@ -2970,7 +2962,7 @@ bool System::uploadFirmwareURL(const char * url) {
String scheme = saved_url.substring(0, 8); String scheme = saved_url.substring(0, 8);
scheme.toLowerCase(); scheme.toLowerCase();
const bool is_https = scheme.startsWith("https://"); const bool is_https = scheme.startsWith("https://");
const int scheme_len = is_https ? 8 : 7; // "https://" vs "http://" const int scheme_len = is_https ? 8 : 7; // "https://" vs "http://"
WiFiClient basic_client; WiFiClient basic_client;
@@ -2992,11 +2984,11 @@ bool System::uploadFirmwareURL(const char * url) {
ssl_client.setBufferSizes(16384, 1024); ssl_client.setBufferSizes(16384, 1024);
ssl_client.setSessionTimeout(120); ssl_client.setSessionTimeout(120);
} }
basic_client.setTimeout(15000); // socket-level read timeout basic_client.setTimeout(15000); // socket-level read timeout
ssl_client.setTimeout(15000); // Stream::readBytes timeout used by Update ssl_client.setTimeout(15000); // Stream::readBytes timeout used by Update
ssl_client.setClient(&basic_client, is_https); // enableSSL = false for plain HTTP ssl_client.setClient(&basic_client, is_https); // enableSSL = false for plain HTTP
const uint16_t port = is_https ? 443 : 80; const uint16_t port = is_https ? 443 : 80;
String url_remain = saved_url.substring(scheme_len); String url_remain = saved_url.substring(scheme_len);
int redirect_count = 0; int redirect_count = 0;
@@ -3149,6 +3141,18 @@ bool System::uploadFirmwareURL(const char * url) {
int last_pct = -1; int last_pct = -1;
while (total_read < (size_t)firmware_size) { while (total_read < (size_t)firmware_size) {
// a cancel is signalled by the WebUI dropping the status below UPLOADING (back to NORMAL)
// via the systemStatus action, which runs on the AsyncTCP task while we're blocked here
if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
LOG_WARNING("Firmware upload cancelled at %u of %d bytes", (unsigned)total_read, firmware_size);
Update.abort(); // release the OTA partition handle so a later attempt can start cleanly
ssl_client.stop(); // drop the connection
saved_url.clear(); // prevent it from downloading again
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL);
Shell::loop_all(); // flush log buffers so the cancel message shows in the console
return true; // not an error - don't trigger the failure/reset path in emsesp.cpp
}
// wait for some data or for the connection to drop // wait for some data or for the connection to drop
uint32_t wait_start = millis(); uint32_t wait_start = millis();
while (!stream->available()) { while (!stream->available()) {
@@ -3158,10 +3162,18 @@ bool System::uploadFirmwareURL(const char * url) {
if (millis() - wait_start > READ_TIMEOUT_MS) { if (millis() - wait_start > READ_TIMEOUT_MS) {
break; break;
} }
// also bail out promptly if a cancel arrives mid-stall
if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
break;
}
delay(1); delay(1);
} }
if (!stream->available()) { if (!stream->available()) {
// if the inner wait broke because of a cancel, loop back so the top-of-loop handler runs
if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
continue;
}
LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size); LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size);
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD); EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false; return false;

View File

@@ -265,6 +265,10 @@ class System {
return std::string(locale_.c_str()); return std::string(locale_.c_str());
} }
std::string system_name() {
return std::string(system_name_.c_str());
}
void healthcheck(uint8_t healthcheck) { void healthcheck(uint8_t healthcheck) {
healthcheck_ = healthcheck; healthcheck_ = healthcheck;
} }
@@ -358,7 +362,7 @@ class System {
static uint32_t heap_mem_; static uint32_t heap_mem_;
static uint32_t min_free_mem_; static uint32_t min_free_mem_;
uint8_t systemStatus_; // uses SYSTEM_STATUS enum volatile uint8_t systemStatus_; // uses SYSTEM_STATUS enum - written from the AsyncTCP task (e.g. cancel) and read from the main loop during OTA
void set_partition_install_date(); void set_partition_install_date();
@@ -410,6 +414,7 @@ class System {
// EMS-ESP settings // EMS-ESP settings
std::string hostname_; std::string hostname_;
String system_name_;
String locale_; String locale_;
bool low_clock_; bool low_clock_;
String board_profile_; String board_profile_;

View File

@@ -304,7 +304,7 @@ class Thermostat : public EMSdevice {
uint8_t instantstart; uint8_t instantstart;
uint8_t coolstart; uint8_t coolstart;
uint8_t coolondelay; uint8_t coolondelay;
uint8_t cooloffdelay; uint8_t cooloffdelay;
// HybridHP // HybridHP
uint8_t hybridStrategy_; // co2 = 1, cost = 2, temperature = 3, mix = 4 uint8_t hybridStrategy_; // co2 = 1, cost = 2, temperature = 3, mix = 4

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.9.0-dev.10" #define EMSESP_APP_VERSION "3.9.0-dev.11"

View File

@@ -478,8 +478,8 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
} }
} }
// show scheduler, with name, on/off, unless it's of type SCHEDULE_IMMEDIATE // show scheduler items
if (EMSESP::webSchedulerService.count_entities(true)) { if (EMSESP::webSchedulerService.count_entities()) {
JsonObject obj = nodes.add<JsonObject>(); JsonObject obj = nodes.add<JsonObject>();
obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID; // it's unique id obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID; // it's unique id
obj["t"] = EMSdevice::DeviceType::SCHEDULER; // device type number obj["t"] = EMSdevice::DeviceType::SCHEDULER; // device type number
@@ -488,19 +488,20 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
EMSESP::webSchedulerService.read([&](const WebScheduler & webScheduler) { EMSESP::webSchedulerService.read([&](const WebScheduler & webScheduler) {
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) { for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
// only add if we have a name and it's not of type SCHEDULE_IMMEDIATE - we don't need a u (UOM) for this JsonObject node = nodes.add<JsonObject>();
if (scheduleItem.name[0] != '\0' && scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) { node["id"] = (EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID * 100) + count++;
JsonObject node = nodes.add<JsonObject>();
node["id"] = (EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID * 100) + count++;
JsonObject dv = node["dv"].to<JsonObject>(); JsonObject dv = node["dv"].to<JsonObject>();
dv["id"] = std::string("00") + scheduleItem.name; dv["id"] = std::string("00") + scheduleItem.name;
dv["c"] = scheduleItem.name; dv["c"] = scheduleItem.name;
// for immediate schedules, we don't show the active/inactive state or on/off options
if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
char s[12]; char s[12];
dv["v"] = Helpers::render_boolean(s, scheduleItem.active, true); dv["v"] = Helpers::render_boolean(s, scheduleItem.active, true);
JsonArray l = dv["l"].to<JsonArray>(); JsonArray l = dv["l"].to<JsonArray>();
l.add(Helpers::render_boolean(s, false, true)); l.add(Helpers::render_boolean(s, false, true)); // False option
l.add(Helpers::render_boolean(s, true, true)); l.add(Helpers::render_boolean(s, true, true)); // True option
} }
} }
}); });

View File

@@ -334,15 +334,9 @@ void WebSchedulerService::publish(const bool force) {
} }
} }
// count number of entries, default: only named items // count number of scheduler entries
uint8_t WebSchedulerService::count_entities(bool cmd_only) { uint8_t WebSchedulerService::count_entities() {
uint8_t count = 0; return static_cast<uint8_t>(scheduleItems_ ? scheduleItems_->size() : 0);
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.name[0] != '\0' || !cmd_only) {
count++;
}
}
return count;
} }
// execute scheduled command // execute scheduled command

View File

@@ -54,7 +54,7 @@ namespace emsesp {
class ScheduleItem { class ScheduleItem {
public: public:
boolean active; boolean active;
uint8_t flags; // bit flags, see SCHEDULEFLAG_* defines uint8_t flags; // bit flags, see SCHEDULEFLAG_* defines
uint16_t elapsed_min; // total mins from 00:00 uint16_t elapsed_min; // total mins from 00:00
stringPSRAM time; // HH:MM stringPSRAM time; // HH:MM
stringPSRAM cmd; stringPSRAM cmd;
@@ -85,7 +85,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
void ha_reset() { void ha_reset() {
ha_configdone_ = false; ha_configdone_ = false;
} }
uint8_t count_entities(bool cmd_only = false); uint8_t count_entities();
bool onChange(const char * cmd); bool onChange(const char * cmd);
bool executeSchedule(const char * name); bool executeSchedule(const char * name);

View File

@@ -36,6 +36,7 @@ void WebSettings::read(WebSettings & settings, JsonObject root) {
root["version"] = settings.version; root["version"] = settings.version;
root["board_profile"] = settings.board_profile; root["board_profile"] = settings.board_profile;
root["platform"] = EMSESP_PLATFORM; root["platform"] = EMSESP_PLATFORM;
root["system_name"] = settings.system_name;
root["locale"] = settings.locale; root["locale"] = settings.locale;
root["tx_mode"] = settings.tx_mode; root["tx_mode"] = settings.tx_mode;
root["ems_bus_id"] = settings.ems_bus_id; root["ems_bus_id"] = settings.ems_bus_id;
@@ -284,6 +285,8 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
// //
// without checks or necessary restarts... // without checks or necessary restarts...
// //
settings.system_name = root["system_name"] | EMSESP_DEFAULT_SYSTEM_NAME;
settings.trace_raw = root["trace_raw"] | EMSESP_DEFAULT_TRACELOG_RAW; settings.trace_raw = root["trace_raw"] | EMSESP_DEFAULT_TRACELOG_RAW;
EMSESP::trace_raw(settings.trace_raw); EMSESP::trace_raw(settings.trace_raw);

View File

@@ -58,6 +58,7 @@ namespace emsesp {
class WebSettings { class WebSettings {
public: public:
String version = EMSESP_APP_VERSION; String version = EMSESP_APP_VERSION;
String system_name;
String locale; String locale;
uint8_t tx_mode; uint8_t tx_mode;
uint8_t ems_bus_id; uint8_t ems_bus_id;

View File

@@ -421,8 +421,8 @@ bool WebStatusService::refresh_versions_cache() {
#else #else
// detect scheme from EMSESP_VERSIONS_URL (case-insensitive). One code path for HTTP and HTTPS, // detect scheme from EMSESP_VERSIONS_URL (case-insensitive). One code path for HTTP and HTTPS,
// using ESP_SSLClient as a plain TCP passthrough when SSL is disabled. // using ESP_SSLClient as a plain TCP passthrough when SSL is disabled.
String url = EMSESP_VERSIONS_URL; String url = EMSESP_VERSIONS_URL;
String lower = url; String lower = url;
lower.toLowerCase(); lower.toLowerCase();
const bool is_https = lower.startsWith("https://"); const bool is_https = lower.startsWith("https://");
if (!is_https && !lower.startsWith("http://")) { if (!is_https && !lower.startsWith("http://")) {

View File

@@ -68,7 +68,9 @@ static TestStream stream;
// load the tests // load the tests
// this is generated from this file when compiled with -DEMSESP_UNITY_CREATE // this is generated from this file when compiled with -DEMSESP_UNITY_CREATE
// copy the output to the test_api.h file // copy the output to the test_api.h file
#ifndef EMSESP_UNITY_CREATE
#include "test_api.h" #include "test_api.h"
#endif
// Unity's setup call - is called before each test - empty for now // Unity's setup call - is called before each test - empty for now
void setUp() { void setUp() {

File diff suppressed because one or more lines are too long