Merge remote-tracking branch 'origin/v3.5.0' into dev

This commit is contained in:
proddy
2022-09-18 15:43:02 +02:00
135 changed files with 9144 additions and 4297 deletions

View File

@@ -31,7 +31,7 @@ jobs:
python -m pip install --upgrade pip
pip install -U platformio
platformio upgrade
platformio update
pio pkg update
- name: Build WebUI
run: |

View File

@@ -24,7 +24,7 @@ jobs:
python -m pip install --upgrade pip
pip install -U platformio
platformio upgrade
platformio update
pio pkg update
- name: Build WebUI
run: |

View File

@@ -1,5 +1,21 @@
# Changelog
# [3.5.0]
## Added
- Translations in Web UI and all device entity names to German. [#22](https://github.com/emsesp/EMS-ESP32/issues/22)
## Fixed
## Changed
- Discovery in HomeAssistant don't work with custom base topic. [#596](https://github.com/emsesp/EMS-ESP32/issues/596)
## **BREAKING CHANGES:**
- MQTT Discovery (Home Assistant) entity names are now prefixed with the hostname, e.g. `select.thermostat_hc1_mode` becomes `select.emsesp_thermostat_hc1_mode`. You will need to recreate any custom dashboards.
# [3.4.2]
## Added

6
esp32_partition_16M.csv Normal file
View File

@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, , 0x2000,
app0, app, ota_0, , 0x7F0000,
app1, app, ota_1, , 0x7F0000,
spiffs, data, spiffs, , 64K,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0x2000
4 app0 app ota_0 0x7F0000
5 app1 app ota_1 0x7F0000
6 spiffs data spiffs 64K

6
esp32_partition_4M.csv Normal file
View File

@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, , 0x2000,
app0, app, ota_0, , 0x1F0000,
app1, app, ota_1, , 0x1F0000,
spiffs, data, spiffs, , 64K,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0x2000
4 app0 app ota_0 0x1F0000
5 app1 app ota_1 0x1F0000
6 spiffs data spiffs 64K

View File

@@ -1,6 +0,0 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x10000, 0x1F0000,
app1, app, ota_1, 0x200000, 0x1F0000,
spiffs, data, spiffs, 0x3F0000, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xE000 0x2000
4 app0 app ota_0 0x10000 0x1F0000
5 app1 app ota_1 0x200000 0x1F0000
6 spiffs data spiffs 0x3F0000 0x10000

View File

@@ -0,0 +1,5 @@
{
"adapter": "react",
"baseLocale": "en",
"$schema": "https://unpkg.com/typesafe-i18n@5.13.0/schema/typesafe-i18n.json"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "EMS-ESP",
"version": "3.4.0",
"version": "3.5.0",
"private": true,
"proxy": "http://localhost:3080",
"dependencies": {
@@ -30,6 +30,7 @@
"react-router-dom": "^6.4.0",
"react-scripts": "5.0.1",
"sockette": "^2.0.6",
"typesafe-i18n": "^5.13.0",
"typescript": "^4.8.3"
},
"scripts": {
@@ -41,8 +42,9 @@
"build-hosted": "env-cmd -f .env.hosted npm run build",
"build-localhost": "PUBLIC_URL=/ react-app-rewired build",
"mock-api": "nodemon --watch ../mock-api ../mock-api/server.js",
"standalone": "npm-run-all -p start mock-api",
"lint": "eslint . --ext .ts,.tsx"
"standalone": "npm-run-all -p start typesafe-i18n mock-api",
"lint": "eslint . --ext .ts,.tsx",
"typesafe-i18n": "typesafe-i18n"
},
"eslintConfig": {
"extends": [
@@ -78,7 +80,7 @@
"max-len": [
1,
{
"code": 200
"code": 220
}
],
"arrow-parens": 1

View File

@@ -1,4 +1,4 @@
import { FC, createRef, createContext, useContext, RefObject } from 'react';
import { FC, createRef, createContext, useContext, useEffect, useState, RefObject } from 'react';
import { SnackbarProvider } from 'notistack';
import { IconButton } from '@mui/material';
@@ -9,6 +9,13 @@ import { FeaturesLoader } from './contexts/features';
import CustomTheme from './CustomTheme';
import AppRouting from './AppRouting';
import { localStorageDetector } from 'typesafe-i18n/detectors';
import TypesafeI18n from './i18n/i18n-react';
import { detectLocale } from './i18n/i18n-util';
import { loadLocaleAsync } from './i18n/i18n-util.async';
const detectedLocale = detectLocale(localStorageDetector);
const App: FC = () => {
const notistackRef: RefObject<any> = createRef();
@@ -20,24 +27,34 @@ const App: FC = () => {
const colorMode = useContext(ColorModeContext);
const [wasLoaded, setWasLoaded] = useState(false);
useEffect(() => {
loadLocaleAsync(detectedLocale).then(() => setWasLoaded(true));
}, []);
if (!wasLoaded) return null;
return (
<ColorModeContext.Provider value={colorMode}>
<CustomTheme>
<SnackbarProvider
maxSnack={3}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} size="small">
<CloseIcon />
</IconButton>
)}
>
<FeaturesLoader>
<AppRouting />
</FeaturesLoader>
</SnackbarProvider>
</CustomTheme>
<TypesafeI18n locale={detectedLocale}>
<CustomTheme>
<SnackbarProvider
maxSnack={3}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} size="small">
<CloseIcon />
</IconButton>
)}
>
<FeaturesLoader>
<AppRouting />
</FeaturesLoader>
</SnackbarProvider>
</CustomTheme>
</TypesafeI18n>
</ColorModeContext.Provider>
);
};

View File

@@ -2,6 +2,8 @@ import { FC, useContext, useEffect } from 'react';
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
import { useSnackbar, VariantType } from 'notistack';
import { useI18nContext } from './i18n/i18n-react';
import { Authentication, AuthenticationContext } from './contexts/authentication';
import { FeaturesContext } from './contexts/features';
import { RequireAuthenticated, RequireUnauthenticated } from './components';
@@ -41,13 +43,14 @@ export const RemoveTrailingSlashes = () => {
const AppRouting: FC = () => {
const { features } = useContext(FeaturesContext);
const { LL } = useI18nContext();
return (
<Authentication>
<RemoveTrailingSlashes />
<Routes>
<Route path="/unauthorized" element={<RootRedirect message="Please sign in to continue" signOut />} />
<Route path="/fileUpdated" element={<RootRedirect message="Upload successful" variant="success" />} />
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
{features.security && (
<Route
path="/"

View File

@@ -2,7 +2,7 @@ import { FC, useContext, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { useSnackbar } from 'notistack';
import { Box, Fab, Paper, Typography } from '@mui/material';
import { Box, Fab, Paper, Typography, Button } from '@mui/material';
import ForwardIcon from '@mui/icons-material/Forward';
import * as AuthenticationApi from './api/authentication';
@@ -16,6 +16,15 @@ import { SignInRequest } from './types';
import { ValidatedTextField } from './components';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from './validators';
import { I18nContext } from './i18n/i18n-react';
import type { Locales } from './i18n/i18n-types';
import { loadLocaleAsync } from './i18n/i18n-util.async';
import { ReactComponent as NLflag } from './i18n/NL.svg';
import { ReactComponent as DEflag } from './i18n/DE.svg';
import { ReactComponent as GBflag } from './i18n/GB.svg';
import { ReactComponent as SEflag } from './i18n/SE.svg';
const SignIn: FC = () => {
const authenticationContext = useContext(AuthenticationContext);
const { enqueueSnackbar } = useSnackbar();
@@ -31,6 +40,9 @@ const SignIn: FC = () => {
const validateAndSignIn = async () => {
setProcessing(true);
SIGN_IN_REQUEST_VALIDATOR.messages({
required: '%s ' + LL.IS_REQUIRED()
});
try {
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
signIn();
@@ -47,10 +59,10 @@ const SignIn: FC = () => {
} catch (error: unknown) {
if (error instanceof AxiosError) {
if (error.response?.status === 401) {
enqueueSnackbar('Invalid login details', { variant: 'warning' });
enqueueSnackbar(LL.INVALID_LOGIN(), { variant: 'warning' });
}
} else {
enqueueSnackbar(extractErrorMessage(error, 'Unexpected error, please try again'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.ERROR()), { variant: 'error' });
}
setProcessing(false);
}
@@ -58,6 +70,14 @@ const SignIn: FC = () => {
const submitOnEnter = onEnterCallback(signIn);
const { LL, setLocale, locale } = useContext(I18nContext);
const selectLocale = async (loc: Locales) => {
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
return (
<Box
display="flex"
@@ -81,11 +101,37 @@ const SignIn: FC = () => {
})}
>
<Typography variant="h4">{PROJECT_NAME}</Typography>
<Box
sx={{
'& button, & a, & .MuiCard-root': {
mt: 0.5,
mx: 0.5
}
}}
>
<Button size="small" variant={locale === 'en' ? 'contained' : 'outlined'} onClick={() => selectLocale('en')}>
<GBflag style={{ width: 24 }} />
&nbsp;EN
</Button>
<Button size="small" variant={locale === 'de' ? 'contained' : 'outlined'} onClick={() => selectLocale('de')}>
<DEflag style={{ width: 24 }} />
&nbsp;DE
</Button>
<Button size="small" variant={locale === 'nl' ? 'contained' : 'outlined'} onClick={() => selectLocale('nl')}>
<NLflag style={{ width: 24 }} />
&nbsp;NL
</Button>
<Button size="small" variant={locale === 'se' ? 'contained' : 'outlined'} onClick={() => selectLocale('se')}>
<SEflag style={{ width: 24 }} />
&nbsp;SE
</Button>
</Box>
<ValidatedTextField
fieldErrors={fieldErrors}
disabled={processing}
name="username"
label="Username"
label={LL.USERNAME()}
value={signInRequest.username}
onChange={updateLoginRequestValue}
margin="normal"
@@ -97,7 +143,7 @@ const SignIn: FC = () => {
disabled={processing}
type="password"
name="password"
label="Password"
label={LL.PASSWORD()}
value={signInRequest.password}
onChange={updateLoginRequestValue}
onKeyDown={submitOnEnter}
@@ -107,7 +153,7 @@ const SignIn: FC = () => {
/>
<Fab variant="extended" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}>
<ForwardIcon sx={{ mr: 1 }} />
Sign In
{LL.SIGN_IN()}
</Fab>
</Paper>
</Box>

View File

@@ -1,12 +1,35 @@
import { FC, useState, useContext } from 'react';
import { FC, useState, useContext, ChangeEventHandler } from 'react';
import { Box, Button, Divider, IconButton, Popover, Typography, Avatar, styled, TypographyProps } from '@mui/material';
import {
Box,
Button,
Divider,
IconButton,
Popover,
Typography,
Avatar,
styled,
TypographyProps,
MenuItem,
TextField
} from '@mui/material';
import PersonIcon from '@mui/icons-material/Person';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import { AuthenticatedContext } from '../../contexts/authentication';
import { I18nContext } from '../../i18n/i18n-react';
import type { Locales } from '../../i18n/i18n-types';
import { loadLocaleAsync } from '../../i18n/i18n-util.async';
import { ReactComponent as NLflag } from '../../i18n/NL.svg';
import { ReactComponent as DEflag } from '../../i18n/DE.svg';
import { ReactComponent as GBflag } from '../../i18n/GB.svg';
import { ReactComponent as SEflag } from '../../i18n/SE.svg';
import { ReactComponent as PLflag } from '../../i18n/PL.svg';
import { ReactComponent as NOflag } from '../../i18n/NO.svg';
const ItemTypography = styled(Typography)<TypographyProps>({
maxWidth: '250px',
whiteSpace: 'nowrap',
@@ -23,6 +46,15 @@ const LayoutAuthMenu: FC = () => {
setAnchorEl(event.currentTarget);
};
const { locale, LL, setLocale } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
const handleClose = () => {
setAnchorEl(null);
};
@@ -32,7 +64,48 @@ const LayoutAuthMenu: FC = () => {
return (
<>
<IconButton id="open-auth-menu" sx={{ padding: 0 }} aria-describedby={id} color="inherit" onClick={handleClick}>
<TextField
name="locale"
InputProps={{ style: { fontSize: 10 } }}
variant="outlined"
value={locale}
onChange={onLocaleSelected}
size="small"
select
>
<MenuItem key="en" value="en">
<GBflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;EN
</MenuItem>
<MenuItem key="de" value="de">
<DEflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;DE
</MenuItem>
<MenuItem key="nl" value="nl">
<NLflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NL
</MenuItem>
<MenuItem key="se" value="se">
<SEflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;SE
</MenuItem>
<MenuItem key="pl" value="pl" disabled>
<PLflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;PL
</MenuItem>
<MenuItem key="no" value="no" disabled>
<NOflag style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;NO
</MenuItem>
</TextField>
<IconButton
id="open-auth-menu"
sx={{ ml: 1, padding: 0 }}
aria-describedby={id}
color="inherit"
onClick={handleClick}
>
<AccountCircleIcon />
</IconButton>
<Popover
@@ -56,13 +129,15 @@ const LayoutAuthMenu: FC = () => {
</Avatar>
<Box pl={2}>
<ItemTypography variant="h6">{me.username}</ItemTypography>
<ItemTypography variant="body1">{me.admin ? 'Admin User' : 'Guest User'}</ItemTypography>
<ItemTypography variant="body1">
{me.admin ? LL.ADMIN() + ' ' + LL.USER() : LL.GUEST() + ' ' + LL.USER()}
</ItemTypography>
</Box>
</Box>
<Divider />
<Box p={1.5}>
<Button variant="outlined" fullWidth color="primary" onClick={() => signOut(true)}>
Sign Out
{LL.SIGN_OUT()}
</Button>
</Box>
</Popover>

View File

@@ -15,9 +15,12 @@ import ProjectMenu from '../../project/ProjectMenu';
import LayoutMenuItem from './LayoutMenuItem';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
const LayoutMenu: FC = () => {
const { features } = useContext(FeaturesContext);
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
return (
<>
@@ -28,12 +31,17 @@ const LayoutMenu: FC = () => {
</List>
)}
<List disablePadding component="nav">
<LayoutMenuItem icon={SettingsEthernetIcon} label="Network Connection" to="/network" />
<LayoutMenuItem icon={SettingsInputAntennaIcon} label="Access Point" to="/ap" />
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="Network Time" to="/ntp" />}
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK()} to="/network" />
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT()} to="/ap" />
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />}
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
<LayoutMenuItem icon={LockIcon} label="Security" to="/security" disabled={!authenticatedContext.me.admin} />
<LayoutMenuItem icon={SettingsIcon} label="System" to="/system" />
<LayoutMenuItem
icon={LockIcon}
label={LL.SECURITY()}
to="/security"
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={SettingsIcon} label={LL.SYSTEM()} to="/system" />
</List>
</>
);

View File

@@ -5,6 +5,8 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import { MessageBox } from '..';
import { useI18nContext } from '../../i18n/i18n-react';
interface FormLoaderProps {
message?: string;
errorMessage?: string;
@@ -12,12 +14,14 @@ interface FormLoaderProps {
}
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, onRetry, message = 'Loading…' }) => {
const { LL } = useI18nContext();
if (errorMessage) {
return (
<MessageBox my={2} level="error" message={errorMessage}>
{onRetry && (
<Button startIcon={<RefreshIcon />} variant="contained" color="error" onClick={onRetry}>
Retry
{LL.RETRY()}
</Button>
)}
</MessageBox>

View File

@@ -2,23 +2,29 @@ import { FC } from 'react';
import { CircularProgress, Box, Typography, Theme } from '@mui/material';
import { useI18nContext } from '../../i18n/i18n-react';
interface LoadingSpinnerProps {
height?: number | string;
}
const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => (
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column" padding={2} height={height}>
<CircularProgress
sx={(theme: Theme) => ({
margin: theme.spacing(4),
color: theme.palette.text.secondary
})}
size={100}
/>
<Typography variant="h4" color="textSecondary">
Loading&hellip;
</Typography>
</Box>
);
const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => {
const { LL } = useI18nContext();
return (
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column" padding={2} height={height}>
<CircularProgress
sx={(theme: Theme) => ({
margin: theme.spacing(4),
color: theme.palette.text.secondary
})}
size={100}
/>
<Typography variant="h4" color="textSecondary">
{LL.LOADING()}&hellip;
</Typography>
</Box>
);
};
export default LoadingSpinner;

View File

@@ -6,6 +6,8 @@ import { Box, Button, LinearProgress, Theme, Typography, useTheme } from '@mui/m
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CancelIcon from '@mui/icons-material/Cancel';
import { useI18nContext } from '../../i18n/i18n-react';
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
const getBorderColor = (theme: Theme, props: DropzoneState) => {
@@ -41,14 +43,16 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
const { getRootProps, getInputProps } = dropzoneState;
const theme = useTheme();
const { LL } = useI18nContext();
const progressText = () => {
if (uploading) {
if (progress?.lengthComputable) {
return `Uploading: ${progressPercentage(progress)}%`;
return LL.UPLOADING() + `: ${progressPercentage(progress)}%`;
}
return 'Uploading\u2026';
return LL.UPLOADING() + `\u2026`;
}
return 'Drop file or click here';
return LL.UPLOAD_DROP_TEXT();
};
return (
@@ -81,7 +85,7 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, uploading, prog
/>
</Box>
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}>
Cancel
{LL.CANCEL()}
</Button>
</Fragment>
)}

View File

@@ -5,11 +5,15 @@ import { useSnackbar } from 'notistack';
import { extractErrorMessage } from '../../utils';
import { FileUploadConfig } from '../../api/endpoints';
import { useI18nContext } from '../../i18n/i18n-react';
interface MediaUploadOptions {
upload: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
}
const useFileUpload = ({ upload }: MediaUploadOptions) => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [uploading, setUploading] = useState<boolean>(false);
const [uploadProgress, setUploadProgress] = useState<ProgressEvent>();
@@ -42,13 +46,13 @@ const useFileUpload = ({ upload }: MediaUploadOptions) => {
cancelToken: cancelToken.token
});
resetUploadingStates();
enqueueSnackbar('File uploaded', { variant: 'success' });
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.SUCCESSFUL(), { variant: 'success' });
} catch (error: unknown) {
if (axios.isCancel(error)) {
enqueueSnackbar('Upload aborted', { variant: 'warning' });
enqueueSnackbar(LL.UPLOAD() + ' ' + LL.ABORTED(), { variant: 'warning' });
} else {
resetUploadingStates();
enqueueSnackbar(extractErrorMessage(error, 'Upload failed'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.UPLOAD() + ' ' + LL.FAILED()), { variant: 'error' });
}
}
};

View File

@@ -2,6 +2,8 @@ import { FC, useCallback, useContext, useEffect, useState } from 'react';
import { useSnackbar } from 'notistack';
import { useNavigate } from 'react-router-dom';
import { useI18nContext } from '../../i18n/i18n-react';
import * as AuthenticationApi from '../../api/authentication';
import { ACCESS_TOKEN } from '../../api/endpoints';
import { RequiredChildrenProps } from '../../utils';
@@ -12,6 +14,8 @@ import { AuthenticationContext } from './context';
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const { features } = useContext(FeaturesContext);
const { LL } = useI18nContext();
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
@@ -23,7 +27,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
setMe(decodedMe);
enqueueSnackbar(`Logged in as ${decodedMe.username}`, { variant: 'success' });
enqueueSnackbar(LL.LOGGED_IN({ name: decodedMe.username }), { variant: 'success' });
} catch (error: unknown) {
setMe(undefined);
throw new Error('Failed to parse JWT');

View File

@@ -19,6 +19,8 @@ import { APProvisionMode, APSettings } from '../../types';
import { numberValue, updateValue, useRest } from '../../utils';
import * as APApi from '../../api/ap';
import { useI18nContext } from '../../i18n/i18n-react';
export const isAPEnabled = ({ provision_mode }: APSettings) => {
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
};
@@ -29,6 +31,8 @@ const APSettingsForm: FC = () => {
update: APApi.updateAPSettings
});
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValue(setData);
@@ -53,7 +57,7 @@ const APSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="provision_mode"
label="Provide Access Point&hellip;"
label={LL.AP_PROVIDE() + '...'}
value={data.provision_mode}
fullWidth
select
@@ -61,16 +65,16 @@ const APSettingsForm: FC = () => {
onChange={updateFormValue}
margin="normal"
>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>{LL.AP_PROVIDE_TEXT_1()}</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>{LL.AP_PROVIDE_TEXT_2()}</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>{LL.AP_PROVIDE_TEXT_3()}</MenuItem>
</ValidatedTextField>
{isAPEnabled(data) && (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="ssid"
label="Access Point SSID"
label={LL.ACCESS_POINT() + ' SSID'}
fullWidth
variant="outlined"
value={data.ssid}
@@ -80,7 +84,7 @@ const APSettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Access Point Password"
label={LL.ACCESS_POINT() + ' ' + LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -90,7 +94,7 @@ const APSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="channel"
label="Preferred Channel"
label={LL.AP_PREFERRED_CHANNEL()}
value={numberValue(data.channel)}
fullWidth
select
@@ -107,7 +111,7 @@ const APSettingsForm: FC = () => {
</ValidatedTextField>
<BlockFormControlLabel
control={<Checkbox name="ssid_hidden" checked={data.ssid_hidden} onChange={updateFormValue} />}
label="Hide SSID"
label={LL.AP_HIDE_SSID()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -168,7 +172,7 @@ const APSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -176,7 +180,7 @@ const APSettingsForm: FC = () => {
};
return (
<SectionContent title="Access Point Settings" titleGutter>
<SectionContent title={LL.ACCESS_POINT() + ' ' + LL.SETTINGS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -11,6 +11,8 @@ import { APNetworkStatus, APStatus } from '../../types';
import { ButtonRow, FormLoader, SectionContent } from '../../components';
import { useRest } from '../../utils';
import { useI18nContext } from '../../i18n/i18n-react';
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
switch (status) {
case APNetworkStatus.ACTIVE:
@@ -24,24 +26,26 @@ export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
}
};
export const apStatus = ({ status }: APStatus) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return 'Active';
case APNetworkStatus.INACTIVE:
return 'Inactive';
case APNetworkStatus.LINGERING:
return 'Lingering until idle';
default:
return 'Unknown';
}
};
const APStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const apStatus = ({ status }: APStatus) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return LL.ACTIVE();
case APNetworkStatus.INACTIVE:
return LL.INACTIVE();
case APNetworkStatus.LINGERING:
return 'Lingering until idle';
default:
return LL.UNKNOWN();
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -56,7 +60,7 @@ const APStatusForm: FC = () => {
<SettingsInputAntennaIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={apStatus(data)} />
<ListItemText primary={LL.STATUS()} secondary={apStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -87,7 +91,7 @@ const APStatusForm: FC = () => {
</List>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
@@ -95,7 +99,7 @@ const APStatusForm: FC = () => {
};
return (
<SectionContent title="Access Point Status" titleGutter>
<SectionContent title={LL.ACCESS_POINT() + ' ' + LL.STATUS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -8,8 +8,12 @@ import APStatusForm from './APStatusForm';
import APSettingsForm from './APSettingsForm';
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
import { useI18nContext } from '../../i18n/i18n-react';
const AccessPoint: FC = () => {
useLayoutTitle('Access Point');
const { LL } = useI18nContext();
useLayoutTitle(LL.ACCESS_POINT());
const authenticatedContext = useContext(AuthenticatedContext);
@@ -18,8 +22,12 @@ const AccessPoint: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="Access Point Status" />
<Tab value="settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={LL.ACCESS_POINT() + ' ' + LL.STATUS()} />
<Tab
value="settings"
label={LL.ACCESS_POINT() + ' ' + LL.SETTINGS()}
disabled={!authenticatedContext.me.admin}
/>
</RouterTabs>
<Routes>
<Route path="status" element={<APStatusForm />} />

View File

@@ -9,7 +9,11 @@ import { AuthenticatedContext } from '../../contexts/authentication';
import MqttStatusForm from './MqttStatusForm';
import MqttSettingsForm from './MqttSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const Mqtt: FC = () => {
const { LL } = useI18nContext();
useLayoutTitle('MQTT');
const authenticatedContext = useContext(AuthenticatedContext);
@@ -18,8 +22,8 @@ const Mqtt: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="MQTT Status" />
<Tab value="settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={'MQTT ' + LL.STATUS()} />
<Tab value="settings" label={'MQTT ' + LL.SETTINGS()} disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<MqttStatusForm />} />

View File

@@ -1,7 +1,7 @@
import { FC, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { Button, Checkbox, MenuItem, Grid, Typography } from '@mui/material';
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import { MQTT_SETTINGS_VALIDATOR, validate } from '../../validators';
@@ -17,12 +17,16 @@ import { MqttSettings } from '../../types';
import { numberValue, updateValue, useRest } from '../../utils';
import * as MqttApi from '../../api/mqtt';
import { useI18nContext } from '../../i18n/i18n-react';
const MqttSettingsForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<MqttSettings>({
read: MqttApi.readMqttSettings,
update: MqttApi.updateMqttSettings
});
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValue(setData);
@@ -46,7 +50,7 @@ const MqttSettingsForm: FC = () => {
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
label="Enable MQTT"
label={LL.ENABLE_MQTT()}
/>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={6}>
@@ -91,7 +95,7 @@ const MqttSettingsForm: FC = () => {
<Grid item xs={6}>
<ValidatedTextField
name="client_id"
label="Client ID (optional)"
label={'Client ID (' + LL.OPTIONAL() + ')'}
fullWidth
variant="outlined"
value={data.client_id}
@@ -104,7 +108,7 @@ const MqttSettingsForm: FC = () => {
<Grid item xs={6}>
<ValidatedTextField
name="username"
label="Username"
label={LL.USERNAME()}
fullWidth
variant="outlined"
value={data.username}
@@ -115,7 +119,7 @@ const MqttSettingsForm: FC = () => {
<Grid item xs={6}>
<ValidatedPasswordField
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -129,7 +133,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="keep_alive"
label="Keep Alive (seconds)"
label="Keep Alive"
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.keep_alive)}
@@ -149,7 +156,7 @@ const MqttSettingsForm: FC = () => {
margin="normal"
select
>
<MenuItem value={0}>0 (default)</MenuItem>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
</ValidatedTextField>
@@ -157,18 +164,18 @@ const MqttSettingsForm: FC = () => {
</Grid>
<BlockFormControlLabel
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />}
label="Set Clean Session"
label={LL.MQTT_CLEAN_SESSION()}
/>
<BlockFormControlLabel
control={<Checkbox name="mqtt_retain" checked={data.mqtt_retain} onChange={updateFormValue} />}
label="Always use Retain Flag"
label={LL.MQTT_RETAIN_FLAG()}
/>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Formatting
{LL.FORMATTING()}
</Typography>
<ValidatedTextField
name="nested_format"
label="Topic/Payload Format"
label={'Topic/Payload ' + LL.FORMAT()}
value={data.nested_format}
fullWidth
variant="outlined"
@@ -176,19 +183,19 @@ const MqttSettingsForm: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>Nested in a single topic</MenuItem>
<MenuItem value={2}>As individual topics</MenuItem>
<MenuItem value={1}>{LL.MQTT_NEST_1()}</MenuItem>
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
</ValidatedTextField>
<BlockFormControlLabel
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />}
label="Publish command output to a 'response' topic"
label={LL.MQTT_RESPONSE()}
/>
{!data.ha_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item>
<BlockFormControlLabel
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />}
label="Publish single value topics on change"
label={LL.MQTT_PUBLISH_TEXT_1()}
/>
</Grid>
{data.publish_single && (
@@ -197,7 +204,7 @@ const MqttSettingsForm: FC = () => {
control={
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} />
}
label="Publish to command topics (ioBroker)"
label={LL.MQTT_PUBLISH_TEXT_2()}
/>
</Grid>
)}
@@ -208,14 +215,14 @@ const MqttSettingsForm: FC = () => {
<Grid item>
<BlockFormControlLabel
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />}
label="Enable MQTT Discovery (Home Assistant, Domoticz)"
label={LL.MQTT_PUBLISH_TEXT_3()}
/>
</Grid>
{data.ha_enabled && (
<Grid item xs={6}>
<ValidatedTextField
name="discovery_prefix"
label="Prefix for the Discovery topics"
label={LL.MQTT_PUBLISH_TEXT_4()}
fullWidth
variant="outlined"
value={data.discovery_prefix}
@@ -227,14 +234,17 @@ const MqttSettingsForm: FC = () => {
</Grid>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Publish Intervals (in seconds, 0=automatic)
{LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto)
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_boiler"
label="Boilers and Heat Pumps"
label={LL.MQTT_INT_BOILER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_boiler)}
@@ -247,7 +257,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_thermostat"
label="Thermostats"
label={LL.MQTT_INT_THERMOSTATS()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_thermostat)}
@@ -260,7 +273,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_solar"
label="Solar Modules"
label={LL.MQTT_INT_SOLAR()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_solar)}
@@ -273,7 +289,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_mixer"
label="Mixer Modules"
label={LL.MQTT_INT_MIXER()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_mixer)}
@@ -286,7 +305,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_sensor"
label="Temperature Sensors"
label={LL.TEMP_SENSORS()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_sensor)}
@@ -299,7 +321,10 @@ const MqttSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="publish_time_other"
label="Default"
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
label={LL.DEFAULT()}
fullWidth
variant="outlined"
value={numberValue(data.publish_time_other)}
@@ -318,7 +343,7 @@ const MqttSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -326,7 +351,7 @@ const MqttSettingsForm: FC = () => {
};
return (
<SectionContent title="MQTT Settings" titleGutter>
<SectionContent title={'MQTT ' + LL.SETTINGS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -11,6 +11,8 @@ import { MqttStatus, MqttDisconnectReason } from '../../types';
import * as MqttApi from '../../api/mqtt';
import { useRest } from '../../utils';
import { useI18nContext } from '../../i18n/i18n-react';
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatus, theme: Theme) => {
if (!enabled) {
return theme.palette.info.main;
@@ -29,44 +31,46 @@ export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatus, theme: Theme) =
return theme.palette.error.main;
};
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
if (!enabled) {
return 'Not enabled';
}
if (connected) {
return 'Connected';
}
return 'Disconnected';
};
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
switch (disconnect_reason) {
case MqttDisconnectReason.TCP_DISCONNECTED:
return 'TCP disconnected';
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
return 'Unacceptable protocol version';
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
return 'Client ID rejected';
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
return 'Server unavailable';
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
return 'Malformed credentials';
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
return 'Not authorized';
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
return 'Device out of memory';
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
return 'Server fingerprint invalid';
default:
return 'Unknown';
}
};
const MqttStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<MqttStatus>({ read: MqttApi.readMqttStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const mqttStatus = ({ enabled, connected }: MqttStatus) => {
if (!enabled) {
return LL.NOT_ENABLED();
}
if (connected) {
return LL.CONNECTED();
}
return LL.DISCONNECTED();
};
const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
switch (disconnect_reason) {
case MqttDisconnectReason.TCP_DISCONNECTED:
return 'TCP disconnected';
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
return 'Unacceptable protocol version';
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
return 'Client ID rejected';
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
return 'Server unavailable';
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
return 'Malformed credentials';
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
return 'Not authorized';
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
return 'Device out of memory';
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
return 'Server fingerprint invalid';
default:
return 'Unknown';
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -89,7 +93,7 @@ const MqttStatusForm: FC = () => {
<SpeakerNotesOffIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="MQTT Publish Errors" secondary={data.mqtt_fails} />
<ListItemText primary={'MQTT Publish ' + LL.ERRORS()} secondary={data.mqtt_fails} />
</ListItem>
</>
);
@@ -102,7 +106,7 @@ const MqttStatusForm: FC = () => {
<ReportIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
<ListItemText primary={LL.DISCONNECT_REASON()} secondary={disconnectReason(data)} />
</ListItem>
<Divider variant="inset" component="li" />
</>
@@ -118,14 +122,14 @@ const MqttStatusForm: FC = () => {
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={mqttStatus(data)} />
<ListItemText primary={LL.STATUS()} secondary={mqttStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{data.enabled && renderConnectionStatus()}
</List>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
@@ -133,7 +137,7 @@ const MqttStatusForm: FC = () => {
};
return (
<SectionContent title="MQTT Status" titleGutter>
<SectionContent title={'MQTT ' + LL.STATUS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -11,12 +11,16 @@ import NetworkStatusForm from './NetworkStatusForm';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import NetworkSettingsForm from './NetworkSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const NetworkConnection: FC = () => {
useLayoutTitle('Network Connection');
const { LL } = useI18nContext();
useLayoutTitle(LL.NETWORK());
const { routerTab } = useRouterTab();
const authenticatedContext = useContext(AuthenticatedContext);
const navigate = useNavigate();
const { routerTab } = useRouterTab();
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
@@ -41,9 +45,9 @@ const NetworkConnection: FC = () => {
}}
>
<RouterTabs value={routerTab}>
<Tab value="status" label="Network Status" />
<Tab value="scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
<Tab value="settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={LL.NETWORK() + ' ' + LL.STATUS()} />
<Tab value="scan" label={LL.NETWORK_SCAN()} disabled={!authenticatedContext.me.admin} />
<Tab value="settings" label={LL.NETWORK() + ' ' + LL.SETTINGS()} disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<NetworkStatusForm />} />

View File

@@ -10,7 +10,8 @@ import {
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
Typography
Typography,
InputAdornment
} from '@mui/material';
import LockOpenIcon from '@mui/icons-material/LockOpen';
@@ -36,7 +37,11 @@ import { ValidateFieldsError } from 'async-validator';
import { validate } from '../../validators';
import { createNetworkSettingsValidator } from '../../validators/network';
import { useI18nContext } from '../../i18n/i18n-react';
const WiFiSettingsForm: FC = () => {
const { LL } = useI18nContext();
const { selectedNetwork, deselectNetwork } = useContext(WiFiConnectionContext);
const [initialized, setInitialized] = useState(false);
@@ -111,7 +116,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="ssid"
label="SSID (leave blank to disable WiFi)"
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'}
fullWidth
variant="outlined"
value={data.ssid}
@@ -123,7 +128,7 @@ const WiFiSettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -135,7 +140,10 @@ const WiFiSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="tx_power"
label="WiFi Tx Power (dBm)"
label={'WiFi Tx ' + LL.POWER()}
InputProps={{
endAdornment: <InputAdornment position="end">dBm</InputAdornment>
}}
fullWidth
variant="outlined"
value={numberValue(data.tx_power)}
@@ -146,21 +154,21 @@ const WiFiSettingsForm: FC = () => {
<BlockFormControlLabel
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />}
label="Disable WiFi Sleep Mode"
label={LL.NETWORK_DISABLE_SLEEP()}
/>
<BlockFormControlLabel
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />}
label="Use Lower WiFi Bandwidth"
label={LL.NETWORK_LOW_BAND()}
/>
<BlockFormControlLabel
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />}
label="Enable mDNS Service"
label={LL.NETWORK_USE_DNS()}
/>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
General
{LL.GENERAL_OPTIONS()}
</Typography>
<ValidatedTextField
@@ -176,12 +184,12 @@ const WiFiSettingsForm: FC = () => {
<BlockFormControlLabel
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />}
label="Enable IPv6 support"
label={LL.NETWORK_ENABLE_IPV6()}
/>
<BlockFormControlLabel
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />}
label="Use Fixed IP address"
label={LL.NETWORK_FIXED_IP()}
/>
{data.static_ip_config && (
<>
@@ -246,7 +254,7 @@ const WiFiSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -254,7 +262,7 @@ const WiFiSettingsForm: FC = () => {
};
return (
<SectionContent title="Network Settings" titleGutter>
<SectionContent title={LL.NETWORK() + ' ' + LL.SETTINGS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -14,6 +14,8 @@ import { NetworkConnectionStatus, NetworkStatus } from '../../types';
import * as NetworkApi from '../../api/network';
import { useRest } from '../../utils';
import { useI18nContext } from '../../i18n/i18n-react';
const isConnected = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
@@ -35,29 +37,6 @@ const networkStatusHighlight = ({ status }: NetworkStatus, theme: Theme) => {
}
};
const networkStatus = ({ status }: NetworkStatus) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return 'Inactive';
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
return 'Idle';
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return 'No SSID Available';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
return 'Connected (WiFi)';
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return 'Connected (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return 'Connection Failed';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return 'Connection Lost';
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
return 'Disconnected';
default:
return 'Unknown';
}
};
export const isWiFi = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
export const isEthernet = ({ status }: NetworkStatus) => status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
@@ -81,8 +60,33 @@ const IPs = (status: NetworkStatus) => {
const NetworkStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<NetworkStatus>({ read: NetworkApi.readNetworkStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const networkStatus = ({ status }: NetworkStatus) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return LL.INACTIVE();
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
return LL.IDLE();
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return 'No SSID Available';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
return LL.CONNECTED() + ' (WiFi)';
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return LL.CONNECTED() + ' (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return LL.CONNECTED() + ' ' + LL.FAILED();
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return LL.CONNECTED() + ' ' + LL.LOST();
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
return LL.DISCONNECTED();
default:
return LL.UNKNOWN();
}
};
const content = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage} />;
@@ -162,7 +166,7 @@ const NetworkStatusForm: FC = () => {
</List>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</>
@@ -170,7 +174,7 @@ const NetworkStatusForm: FC = () => {
};
return (
<SectionContent title="Network Status" titleGutter>
<SectionContent title={LL.NETWORK() + ' ' + LL.STATUS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -12,6 +12,8 @@ import { ButtonRow, FormLoader, SectionContent } from '../../components';
import WiFiNetworkSelector from './WiFiNetworkSelector';
import { useI18nContext } from '../../i18n/i18n-react';
const NUM_POLLS = 10;
const POLLING_FREQUENCY = 500;
@@ -22,6 +24,8 @@ const compareNetworks = (network1: WiFiNetwork, network2: WiFiNetwork) => {
};
const WiFiNetworkScanner: FC = () => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const pollCount = useRef(0);
@@ -46,7 +50,7 @@ const WiFiNetworkScanner: FC = () => {
pollCount.current = completedPollCount;
setTimeout(pollNetworkList, POLLING_FREQUENCY);
} else {
finishedWithError('Device did not return network list in timely manner');
finishedWithError(LL.PROBLEM_LOADING());
}
} else {
const newNetworkList = response.data;
@@ -55,12 +59,12 @@ const WiFiNetworkScanner: FC = () => {
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
finishedWithError('Problem listing WiFi networks ' + error.response?.data.message);
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
} else {
finishedWithError('Problem listing WiFi networks');
finishedWithError(LL.PROBLEM_LOADING());
}
}
}, [finishedWithError]);
}, [finishedWithError, LL]);
const startNetworkScan = useCallback(async () => {
pollCount.current = 0;
@@ -71,12 +75,12 @@ const WiFiNetworkScanner: FC = () => {
setTimeout(pollNetworkList, POLLING_FREQUENCY);
} catch (error: unknown) {
if (error instanceof AxiosError) {
finishedWithError('Problem scanning for WiFi networks ' + error.response?.data.message);
finishedWithError(LL.PROBLEM_LOADING() + ' ' + error.response?.data.message);
} else {
finishedWithError('Problem scanning for WiFi networks');
finishedWithError(LL.PROBLEM_LOADING());
}
}
}, [finishedWithError, pollNetworkList]);
}, [finishedWithError, pollNetworkList, LL]);
useEffect(() => {
startNetworkScan();
@@ -84,13 +88,13 @@ const WiFiNetworkScanner: FC = () => {
const renderNetworkScanner = () => {
if (!networkList) {
return <FormLoader message="Scanning&hellip;" errorMessage={errorMessage} />;
return <FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />;
}
return <WiFiNetworkSelector networkList={networkList} />;
};
return (
<SectionContent title="Network Scanner">
<SectionContent title={LL.NETWORK_SCANNER()}>
{renderNetworkScanner()}
<ButtonRow>
<Button
@@ -100,7 +104,7 @@ const WiFiNetworkScanner: FC = () => {
onClick={startNetworkScan}
disabled={!errorMessage && !networkList}
>
Scan again&hellip;
{LL.SCAN_AGAIN()}&hellip;
</Button>
</ButtonRow>
</SectionContent>

View File

@@ -12,6 +12,8 @@ import { WiFiEncryptionType, WiFiNetwork, WiFiNetworkList } from '../../types';
import { WiFiConnectionContext } from './WiFiConnectionContext';
import { useI18nContext } from '../../i18n/i18n-react';
interface WiFiNetworkSelectorProps {
networkList: WiFiNetworkList;
}
@@ -39,6 +41,8 @@ export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
};
const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
const { LL } = useI18nContext();
const wifiConnectionContext = useContext(WiFiConnectionContext);
const renderNetwork = (network: WiFiNetwork) => {
@@ -61,7 +65,7 @@ const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
};
if (networkList.networks.length === 0) {
return <MessageBox mt={2} mb={1} message="No WiFi networks found" level="info" />;
return <MessageBox mt={2} mb={1} message={LL.NETWORK_NO_WIFI()} level="info" />;
}
return <List>{networkList.networks.map(renderNetwork)}</List>;

View File

@@ -12,12 +12,16 @@ import * as NTPApi from '../../api/ntp';
import { selectedTimeZone, timeZoneSelectItems, TIME_ZONES } from './TZ';
import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp';
import { useI18nContext } from '../../i18n/i18n-react';
const NTPSettingsForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<NTPSettings>({
read: NTPApi.readNTPSettings,
update: NTPApi.updateNTPSettings
});
const { LL } = useI18nContext();
const updateFormValue = updateValue(setData);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -49,7 +53,7 @@ const NTPSettingsForm: FC = () => {
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
label="Enable NTP"
label={LL.ENABLE_NTP()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -64,7 +68,7 @@ const NTPSettingsForm: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="tz_label"
label="Time zone"
label={LL.TIME_ZONE()}
fullWidth
variant="outlined"
value={selectedTimeZone(data.tz_label, data.tz_format)}
@@ -72,7 +76,7 @@ const NTPSettingsForm: FC = () => {
margin="normal"
select
>
<MenuItem disabled>Time zone...</MenuItem>
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
{timeZoneSelectItems()}
</ValidatedTextField>
<ButtonRow>
@@ -84,7 +88,7 @@ const NTPSettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -92,7 +96,7 @@ const NTPSettingsForm: FC = () => {
};
return (
<SectionContent title="NTP Settings" titleGutter>
<SectionContent title={'NTP ' + LL.SETTINGS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -16,7 +16,8 @@ import {
ListItemText,
TextField,
Theme,
useTheme
useTheme,
Typography
} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
@@ -31,6 +32,8 @@ import { ButtonRow, FormLoader, SectionContent } from '../../components';
import { extractErrorMessage, formatDateTime, formatLocalDateTime, useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
export const isNtpActive = ({ status }: NTPStatus) => status === NTPSyncStatus.NTP_ACTIVE;
export const isNtpEnabled = ({ status }: NTPStatus) => status !== NTPSyncStatus.NTP_DISABLED;
@@ -47,19 +50,6 @@ export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
}
};
export const ntpStatus = ({ status }: NTPStatus) => {
switch (status) {
case NTPSyncStatus.NTP_DISABLED:
return 'Disabled';
case NTPSyncStatus.NTP_INACTIVE:
return 'Inactive';
case NTPSyncStatus.NTP_ACTIVE:
return 'Active';
default:
return 'Unknown';
}
};
const NTPStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<NTPStatus>({ read: NTPApi.readNTPStatus });
const [localTime, setLocalTime] = useState<string>('');
@@ -68,6 +58,8 @@ const NTPStatusForm: FC = () => {
const { enqueueSnackbar } = useSnackbar();
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value);
const openSetTime = () => {
@@ -77,59 +69,71 @@ const NTPStatusForm: FC = () => {
const theme = useTheme();
const ntpStatus = ({ status }: NTPStatus) => {
switch (status) {
case NTPSyncStatus.NTP_DISABLED:
return LL.DISABLED();
case NTPSyncStatus.NTP_INACTIVE:
return LL.INACTIVE();
case NTPSyncStatus.NTP_ACTIVE:
return LL.ACTIVE();
default:
return LL.UNKNOWN();
}
};
const configureTime = async () => {
setProcessing(true);
try {
await NTPApi.updateTime({
local_time: formatLocalDateTime(new Date(localTime))
});
enqueueSnackbar('Time set', { variant: 'success' });
enqueueSnackbar(LL.TIME_SET(), { variant: 'success' });
setSettingTime(false);
loadData();
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating time'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setProcessing(false);
}
};
const renderSetTimeDialog = () => {
return (
<Dialog open={settingTime} onClose={() => setSettingTime(false)}>
<DialogTitle>Set Time</DialogTitle>
<DialogContent dividers>
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
<TextField
label="Local Time"
type="datetime-local"
value={localTime}
onChange={updateLocalTime}
disabled={processing}
variant="outlined"
fullWidth
InputLabelProps={{
shrink: true
}}
/>
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary">
Cancel
</Button>
<Button
startIcon={<AccessTimeIcon />}
variant="outlined"
onClick={configureTime}
disabled={processing}
color="primary"
autoFocus
>
Set Time
</Button>
</DialogActions>
</Dialog>
);
};
const renderSetTimeDialog = () => (
<Dialog open={settingTime} onClose={() => setSettingTime(false)}>
<DialogTitle>{LL.SET_TIME()}</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
</Box>
<TextField
label={LL.LOCAL_TIME()}
type="datetime-local"
value={localTime}
onChange={updateLocalTime}
disabled={processing}
fullWidth
InputLabelProps={{
shrink: true
}}
/>
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary">
{LL.CANCEL()}
</Button>
<Button
startIcon={<AccessTimeIcon />}
variant="outlined"
onClick={configureTime}
disabled={processing}
color="primary"
autoFocus
>
{LL.SET_TIME()}
</Button>
</DialogActions>
</Dialog>
);
const content = () => {
if (!data) {
@@ -145,7 +149,7 @@ const NTPStatusForm: FC = () => {
<UpdateIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={ntpStatus(data)} />
<ListItemText primary={LL.STATUS()} secondary={ntpStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
{isNtpEnabled(data) && (
@@ -167,7 +171,7 @@ const NTPStatusForm: FC = () => {
<AccessTimeIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
<ListItemText primary={LL.LOCAL_TIME()} secondary={formatDateTime(data.local_time)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -176,7 +180,7 @@ const NTPStatusForm: FC = () => {
<SwapVerticalCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
<ListItemText primary={LL.UTC_TIME()} secondary={formatDateTime(data.utc_time)} />
</ListItem>
<Divider variant="inset" component="li" />
</List>
@@ -184,7 +188,7 @@ const NTPStatusForm: FC = () => {
<Box flexGrow={1}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
@@ -192,7 +196,7 @@ const NTPStatusForm: FC = () => {
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}>
Set Time
{LL.SET_TIME()}
</Button>
</ButtonRow>
</Box>
@@ -204,7 +208,7 @@ const NTPStatusForm: FC = () => {
};
return (
<SectionContent title="NTP Status" titleGutter>
<SectionContent title={'NTP ' + LL.STATUS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -9,8 +9,11 @@ import { AuthenticatedContext } from '../../contexts/authentication';
import NTPStatusForm from './NTPStatusForm';
import NTPSettingsForm from './NTPSettingsForm';
import { useI18nContext } from '../../i18n/i18n-react';
const NetworkTime: FC = () => {
useLayoutTitle('Network Time');
const { LL } = useI18nContext();
useLayoutTitle("NTP");
const authenticatedContext = useContext(AuthenticatedContext);
const { routerTab } = useRouterTab();
@@ -18,8 +21,8 @@ const NetworkTime: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="NTP Status" />
<Tab value="settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
<Tab value="status" label={'NTP ' + LL.STATUS()} />
<Tab value="settings" label={'NTP ' + LL.SETTINGS()} disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<NTPStatusForm />} />

View File

@@ -19,6 +19,8 @@ import { MessageBox } from '../../components';
import * as SecurityApi from '../../api/security';
import { Token } from '../../types';
import { useI18nContext } from '../../i18n/i18n-react';
interface GenerateTokenProps {
username?: string;
onClose: () => void;
@@ -28,15 +30,17 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const [token, setToken] = useState<Token>();
const open = !!username;
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const getToken = useCallback(async () => {
try {
setToken((await SecurityApi.generateToken(username)).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem generating token'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
}, [username, enqueueSnackbar]);
}, [username, enqueueSnackbar, LL]);
useEffect(() => {
if (open) {
@@ -46,16 +50,11 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open={!!username} fullWidth maxWidth="sm">
<DialogTitle id="generate-token-dialog-title">Access Token for {username}</DialogTitle>
<DialogTitle id="generate-token-dialog-title">{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
<DialogContent dividers>
{token ? (
<>
<MessageBox
message="The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the
'Authorization' header or in the 'access_token' URL query parameter."
level="info"
my={2}
/>
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
<Box mt={2} mb={2}>
<TextField label="Token" multiline value={token.token} fullWidth contentEditable={false} />
</Box>
@@ -63,13 +62,13 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
) : (
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">Generating token&hellip;</Typography>
<Typography variant="h6">{LL.GENERATING_TOKEN()}&hellip;</Typography>
</Box>
)}
</DialogContent>
<DialogActions>
<Button startIcon={<CloseIcon />} variant="outlined" onClick={onClose} color="secondary">
Close
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>

View File

@@ -20,6 +20,8 @@ import { createUserValidator } from '../../validators';
import { useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
import GenerateToken from './GenerateToken';
import UserForm from './UserForm';
@@ -34,9 +36,11 @@ const ManageUsersForm: FC = () => {
const [generatingToken, setGeneratingToken] = useState<string>();
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const table_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 120px;
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 120px 120px;
`,
BaseRow: `
font-size: 14px;
@@ -136,8 +140,8 @@ const ManageUsersForm: FC = () => {
<>
<Header>
<HeaderRow>
<HeaderCell resize>USERNAME</HeaderCell>
<HeaderCell stiff>IS ADMIN</HeaderCell>
<HeaderCell resize>{LL.USERNAME()}</HeaderCell>
<HeaderCell stiff>{LL.IS_ADMIN()}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -169,9 +173,7 @@ const ManageUsersForm: FC = () => {
)}
</Table>
{noAdminConfigured() && (
<MessageBox level="warning" message="You must have at least one admin user configured" my={2} />
)}
{noAdminConfigured() && <MessageBox level="warning" message={LL.USER_WARNING()} my={2} />}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
@@ -183,14 +185,14 @@ const ManageUsersForm: FC = () => {
type="submit"
onClick={onSubmit}
>
Save
{LL.SAVE()}
</Button>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow>
<Button startIcon={<PersonAddIcon />} variant="outlined" color="secondary" onClick={createUser}>
Add
{LL.ADD()}
</Button>
</ButtonRow>
</Box>
@@ -210,7 +212,7 @@ const ManageUsersForm: FC = () => {
};
return (
<SectionContent title="Manage Users" titleGutter>
<SectionContent title={LL.MANAGE_USERS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -8,16 +8,19 @@ import { RouterTabs, useRouterTab, useLayoutTitle } from '../../components';
import SecuritySettingsForm from './SecuritySettingsForm';
import ManageUsersForm from './ManageUsersForm';
import { useI18nContext } from '../../i18n/i18n-react';
const Security: FC = () => {
useLayoutTitle('Security');
const { LL } = useI18nContext();
useLayoutTitle(LL.SECURITY());
const { routerTab } = useRouterTab();
return (
<>
<RouterTabs value={routerTab}>
<Tab value="users" label="Manage Users" />
<Tab value="settings" label="Security Settings" />
<Tab value="users" label={LL.MANAGE_USERS()} />
<Tab value="settings" label={LL.SECURITY() + ' ' + LL.SETTINGS()} />
</RouterTabs>
<Routes>
<Route path="users" element={<ManageUsersForm />} />

View File

@@ -11,7 +11,11 @@ import { SECURITY_SETTINGS_VALIDATOR, validate } from '../../validators';
import { updateValue, useRest } from '../../utils';
import { AuthenticatedContext } from '../../contexts/authentication';
import { useI18nContext } from '../../i18n/i18n-react';
const SecuritySettingsForm: FC = () => {
const { LL } = useI18nContext();
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<SecuritySettings>({
read: SecurityApi.readSecuritySettings,
@@ -42,7 +46,7 @@ const SecuritySettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="jwt_secret"
label="su Password"
label={"su " + LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.jwt_secret}
@@ -51,7 +55,7 @@ const SecuritySettingsForm: FC = () => {
/>
<MessageBox
level="info"
message="The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console."
message={LL.SU_TEXT()}
mt={1}
/>
<ButtonRow>
@@ -63,7 +67,7 @@ const SecuritySettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -71,7 +75,7 @@ const SecuritySettingsForm: FC = () => {
};
return (
<SectionContent title="Security Settings" titleGutter>
<SectionContent title={LL.SECURITY() + " " + LL.SETTINGS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -11,6 +11,8 @@ import { User } from '../../types';
import { updateValue } from '../../utils';
import { validate } from '../../validators';
import { useI18nContext } from '../../i18n/i18n-react';
interface UserFormProps {
creating: boolean;
validator: Schema;
@@ -23,6 +25,8 @@ interface UserFormProps {
}
const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => {
const { LL } = useI18nContext();
const updateFormValue = updateValue(setUser);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const open = !!user;
@@ -49,12 +53,14 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
<Dialog onClose={onCancelEditing} open={!!user} fullWidth maxWidth="sm">
{user && (
<>
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
<DialogTitle id="user-form-dialog-title">
{creating ? LL.ADD() : LL.MODIFY()}&nbsp;{LL.USER()}
</DialogTitle>
<DialogContent dividers>
<ValidatedTextField
fieldErrors={fieldErrors}
name="username"
label="Username"
label={LL.USERNAME()}
fullWidth
variant="outlined"
value={user.username}
@@ -65,7 +71,7 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={user.password}
@@ -74,12 +80,12 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
/>
<BlockFormControlLabel
control={<Checkbox name="admin" checked={user.admin} onChange={updateFormValue} />}
label="is Admin?"
label={LL.IS_ADMIN()}
/>
</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onCancelEditing} color="secondary">
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<PersonAddIcon />}
@@ -88,7 +94,7 @@ const UserForm: FC<UserFormProps> = ({ creating, validator, user, setUser, onDon
color="primary"
autoFocus
>
Add
{LL.ADD()}
</Button>
</DialogActions>
</>

View File

@@ -14,6 +14,8 @@ import { extractErrorMessage } from '../../utils';
import * as EMSESP from '../../project/api';
import { useI18nContext } from '../../i18n/i18n-react';
interface UploadFileProps {
uploadGeneralFile: (file: File, config?: FileUploadConfig) => AxiosPromise<void>;
}
@@ -23,6 +25,8 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
const { enqueueSnackbar } = useSnackbar();
const { LL } = useI18nContext();
const saveFile = (json: any, endpoint: string) => {
const a = document.createElement('a');
const filename = 'emsesp_' + endpoint + '.json';
@@ -35,19 +39,19 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
enqueueSnackbar('File downloaded', { variant: 'info' });
enqueueSnackbar(LL.DOWNLOAD_SUCCESSFUL(), { variant: 'info' });
};
const downloadSettings = async () => {
try {
const response = await EMSESP.getSettings();
if (response.status !== 200) {
enqueueSnackbar('Unable to get settings', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
} else {
saveFile(response.data, 'settings');
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
@@ -55,47 +59,45 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
try {
const response = await EMSESP.getCustomizations();
if (response.status !== 200) {
enqueueSnackbar('Unable to get customizations', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
} else {
saveFile(response.data, 'customizations');
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
return (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
Upload
</Typography>
{!uploading && (
<Box mb={2} color="warning.main">
<Typography variant="body2">
Upload a new firmware (.bin) file, settings or customizations (.json) file below.
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()}
</Typography>
</Box>
<Box mb={2} color="warning.main">
<Typography variant="body2">{LL.UPLOAD_TEXT()} </Typography>
</Box>
</>
)}
<SingleUpload onDrop={uploadFile} onCancel={cancelUpload} uploading={uploading} progress={uploadProgress} />
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
Download
</Typography>
{!uploading && (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD()}
</Typography>
<Box color="warning.main">
<Typography mb={1} variant="body2">
Download the application settings. Be careful when sharing your settings as this file contains passwords
and other sensitive system information.
{LL.DOWNLOAD_SETTINGS_TEXT()}
</Typography>
</Box>
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => downloadSettings()}>
settings
{LL.SETTINGS()}
</Button>
<Box color="warning.main">
<Typography mt={2} mb={1} variant="body2">
Download the entity customizations.
{LL.DOWNLOAD_CUSTOMIZATION_TEXT()}{' '}
</Typography>
</Box>
<Button
@@ -104,7 +106,7 @@ const GeneralFileUpload: FC<UploadFileProps> = ({ uploadGeneralFile }) => {
color="primary"
onClick={() => downloadCustomizations()}
>
customizations
{LL.CUSTOMIZATION()}
</Button>
</>
)}

View File

@@ -12,6 +12,7 @@ import {
ValidatedPasswordField,
ValidatedTextField
} from '../../components';
import { OTASettings } from '../../types';
import { numberValue, updateValue, useRest } from '../../utils';
@@ -19,12 +20,16 @@ import { ValidateFieldsError } from 'async-validator';
import { validate } from '../../validators';
import { OTA_SETTINGS_VALIDATOR } from '../../validators/system';
import { useI18nContext } from '../../i18n/i18n-react';
const OTASettingsForm: FC = () => {
const { loadData, saving, data, setData, saveData, errorMessage } = useRest<OTASettings>({
read: SystemApi.readOTASettings,
update: SystemApi.updateOTASettings
});
const { LL } = useI18nContext();
const updateFormValue = updateValue(setData);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -48,7 +53,7 @@ const OTASettingsForm: FC = () => {
<>
<BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />}
label="Enable OTA Updates"
label={LL.ENABLE_OTA()}
/>
<ValidatedTextField
fieldErrors={fieldErrors}
@@ -64,7 +69,7 @@ const OTASettingsForm: FC = () => {
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Password"
label={LL.PASSWORD()}
fullWidth
variant="outlined"
value={data.password}
@@ -80,7 +85,7 @@ const OTASettingsForm: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</>
@@ -88,7 +93,7 @@ const OTASettingsForm: FC = () => {
};
return (
<SectionContent title="OTA Settings" titleGutter>
<SectionContent title={'OTA ' + LL.SETTINGS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -4,6 +4,8 @@ import { FC, useRef, useState } from 'react';
import * as SystemApi from '../../api/system';
import { FormLoader } from '../../components';
import { useI18nContext } from '../../i18n/i18n-react';
const RESTART_TIMEOUT = 2 * 60 * 1000;
const POLL_TIMEOUT = 2000;
const POLL_INTERVAL = 5000;
@@ -12,6 +14,8 @@ const RestartMonitor: FC = () => {
const [failed, setFailed] = useState<boolean>(false);
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const { LL } = useI18nContext();
const timeoutAt = useRef(new Date().getTime() + RESTART_TIMEOUT);
const poll = useRef(async () => {
try {
@@ -32,12 +36,7 @@ const RestartMonitor: FC = () => {
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
return (
<FormLoader
message="EMS-ESP is restarting, please wait&hellip;"
errorMessage={failed ? 'Timed out waiting for device to restart.' : undefined}
/>
);
return <FormLoader message={LL.APPLICATION_RESTARTING() + '...'} errorMessage={failed ? 'Timed out' : undefined} />;
};
export default RestartMonitor;

View File

@@ -12,8 +12,13 @@ import OTASettingsForm from './OTASettingsForm';
import SystemLog from './SystemLog';
import { useI18nContext } from '../../i18n/i18n-react';
const System: FC = () => {
useLayoutTitle('System');
const { LL } = useI18nContext();
useLayoutTitle(LL.SYSTEM());
const { me } = useContext(AuthenticatedContext);
const { features } = useContext(FeaturesContext);
@@ -22,11 +27,11 @@ const System: FC = () => {
return (
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="System Status" />
<Tab value="log" label="System Log" />
<Tab value="status" label={LL.SYSTEM() + ' ' + LL.STATUS()} />
<Tab value="log" label={LL.SYSTEM() + ' ' + LL.LOG()} />
{features.ota && <Tab value="ota" label="OTA Settings" disabled={!me.admin} />}
{features.upload_firmware && <Tab value="upload" label="Upload/Download" disabled={!me.admin} />}
{features.ota && <Tab value="ota" label={"OTA " + LL.SETTINGS()} disabled={!me.admin} />}
{features.upload_firmware && <Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />}
</RouterTabs>
<Routes>
<Route path="status" element={<SystemStatusForm />} />

View File

@@ -15,6 +15,9 @@ import DownloadIcon from '@mui/icons-material/GetApp';
import { useSnackbar } from 'notistack';
import { EVENT_SOURCE_ROOT } from '../../api/endpoints';
import { useI18nContext } from '../../i18n/i18n-react';
export const LOG_EVENTSOURCE_URL = EVENT_SOURCE_ROOT + 'log';
const useWindowSize = () => {
@@ -63,6 +66,8 @@ const levelLabel = (level: LogLevel) => {
const SystemLog: FC = () => {
useWindowSize();
const { LL } = useI18nContext();
const { loadData, data, setData } = useRest<LogSettings>({
read: SystemApi.readLogSettings
});
@@ -104,10 +109,10 @@ const SystemLog: FC = () => {
compact: data.compact
});
if (response.status !== 200) {
enqueueSnackbar('Problem applying log settings', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem applying log settings'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
}
};
@@ -159,9 +164,9 @@ const SystemLog: FC = () => {
try {
setLogEntries((await SystemApi.readLogEntries()).data);
} catch (error: unknown) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch log'));
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, []);
}, [LL]);
useEffect(() => {
fetchLog();
@@ -214,7 +219,7 @@ const SystemLog: FC = () => {
</ValidatedTextField>
</Grid>
<Grid item xs={3}>
<FormLabel>Buffer size</FormLabel>
<FormLabel>{LL.BUFFER_SIZE()}</FormLabel>
<Slider
value={data.max_messages}
valueLabelDisplay="auto"
@@ -235,12 +240,12 @@ const SystemLog: FC = () => {
<Grid item>
<BlockFormControlLabel
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />}
label="Compact"
label={LL.COMPACT()}
/>
</Grid>
<Grid item>
<Button startIcon={<DownloadIcon />} variant="outlined" color="secondary" onClick={onDownload}>
Export
{LL.EXPORT()}
</Button>
</Grid>
</Grid>
@@ -273,7 +278,7 @@ const SystemLog: FC = () => {
};
return (
<SectionContent title="System Log" titleGutter id="log-window">
<SectionContent title={LL.SYSTEM() + ' ' + LL.LOG()} titleGutter id="log-window">
{content()}
</SectionContent>
);

View File

@@ -39,6 +39,8 @@ import { AuthenticatedContext } from '../../contexts/authentication';
import axios from 'axios';
import { useI18nContext } from '../../i18n/i18n-react';
export const VERSIONCHECK_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/latest';
export const VERSIONCHECK_DEV_ENDPOINT = 'https://api.github.com/repos/emsesp/EMS-ESP32/releases/tags/latest';
export const uploadURL = window.location.origin + '/system/upload';
@@ -48,6 +50,8 @@ function formatNumber(num: number) {
}
const SystemStatusForm: FC = () => {
const { LL } = useI18nContext();
const { loadData, data, errorMessage } = useRest<SystemStatus>({ read: SystemApi.readSystemStatus });
const { me } = useContext(AuthenticatedContext);
@@ -80,9 +84,9 @@ const SystemStatusForm: FC = () => {
setProcessing(true);
try {
await SystemApi.restart();
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
enqueueSnackbar(LL.APPLICATION_RESTARTING(), { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
} finally {
setConfirmRestart(false);
setProcessing(false);
@@ -92,7 +96,7 @@ const SystemStatusForm: FC = () => {
const renderRestartDialog = () => (
<Dialog open={confirmRestart} onClose={() => setConfirmRestart(false)}>
<DialogTitle>Restart</DialogTitle>
<DialogContent dividers>Are you sure you want to restart EMS-ESP?</DialogContent>
<DialogContent dividers>{LL.RESTART_TEXT()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
@@ -100,7 +104,7 @@ const SystemStatusForm: FC = () => {
onClick={() => setConfirmRestart(false)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
@@ -110,7 +114,7 @@ const SystemStatusForm: FC = () => {
color="primary"
autoFocus
>
Restart
{LL.RESTART()}
</Button>
</DialogActions>
</Dialog>
@@ -119,16 +123,12 @@ const SystemStatusForm: FC = () => {
const renderVersionDialog = () => {
return (
<Dialog open={showingVersion} onClose={() => setShowingVersion(false)}>
<DialogTitle>Version Check</DialogTitle>
<DialogTitle>{LL.VERSION_CHECK()}</DialogTitle>
<DialogContent dividers>
<MessageBox
my={0}
level="info"
message={'You are currently running EMS-ESP version ' + data?.emsesp_version}
/>
<MessageBox my={0} level="info" message={LL.SYSTEM_VERSION_RUNNING() + ' ' + data?.emsesp_version} />
{latestVersion && (
<Box mt={2} mb={2}>
The latest <u>official</u> version is <b>{latestVersion.version}</b>&nbsp;(
{LL.THE_LATEST()}&nbsp;<u>official</u> version is <b>{latestVersion.version}</b>&nbsp;(
<Link target="_blank" href={latestVersion.changelog} color="primary">
{'release notes'}
</Link>
@@ -142,7 +142,7 @@ const SystemStatusForm: FC = () => {
{latestDevVersion && (
<Box mt={2} mb={2}>
The latest <u>development</u> version is&nbsp;<b>{latestDevVersion.version}</b>
{LL.THE_LATEST()}&nbsp;<u>development</u> version is&nbsp;<b>{latestDevVersion.version}</b>
&nbsp;(
<Link target="_blank" href={latestDevVersion.changelog} color="primary">
{'release notes'}
@@ -157,17 +157,17 @@ const SystemStatusForm: FC = () => {
<Box color="warning.main" p={0} pl={0} pr={0} mt={4} mb={0}>
<Typography variant="body2">
Use&nbsp;
{LL.USE()}&nbsp;
<Link target="_blank" href={uploadURL} color="primary">
{'UPLOAD'}
</Link>
&nbsp;to apply the new firmware
&nbsp;{LL.SYSTEM_APPLY_FIRMWARE()}
</Typography>
</Box>
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setShowingVersion(false)} color="secondary">
Close
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
@@ -178,9 +178,9 @@ const SystemStatusForm: FC = () => {
setProcessing(true);
try {
await SystemApi.factoryReset();
enqueueSnackbar('Device has been factory reset and will now restart', { variant: 'info' });
enqueueSnackbar(LL.SYSTEM_FACTORY_TEXT(), { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem factory resetting the device'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setConfirmFactoryReset(false);
setProcessing(false);
@@ -189,8 +189,8 @@ const SystemStatusForm: FC = () => {
const renderFactoryResetDialog = () => (
<Dialog open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}>
<DialogTitle>Factory Reset</DialogTitle>
<DialogContent dividers>Are you sure you want to reset the device to its factory defaults?</DialogContent>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
@@ -198,7 +198,7 @@ const SystemStatusForm: FC = () => {
onClick={() => setConfirmFactoryReset(false)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
@@ -208,7 +208,7 @@ const SystemStatusForm: FC = () => {
autoFocus
color="error"
>
Factory Reset
{LL.FACTORY_RESET()}
</Button>
</DialogActions>
</Dialog>
@@ -231,7 +231,7 @@ const SystemStatusForm: FC = () => {
<ListItemText primary="EMS-ESP Version" secondary={'v' + data.emsesp_version} />
{latestVersion && (
<Button color="primary" onClick={() => setShowingVersion(true)}>
Version Check
{LL.VERSION_CHECK()}
</Button>
)}
</ListItem>
@@ -242,7 +242,7 @@ const SystemStatusForm: FC = () => {
<DevicesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Device (Platform / SDK)" secondary={data.esp_platform + ' / ' + data.sdk_version} />
<ListItemText primary={LL.PLATFORM()} secondary={data.esp_platform + ' / ' + data.sdk_version} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -251,7 +251,7 @@ const SystemStatusForm: FC = () => {
<TimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="System Uptime" secondary={data.uptime} />
<ListItemText primary={LL.UPTIME()} secondary={data.uptime} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -260,7 +260,7 @@ const SystemStatusForm: FC = () => {
<ShowChartIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="CPU Frequency" secondary={data.cpu_freq_mhz + ' MHz'} />
<ListItemText primary={LL.CPU_FREQ()} secondary={data.cpu_freq_mhz + ' MHz'} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -270,7 +270,7 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Heap (Free / Max Alloc)"
primary={LL.HEAP()}
secondary={
formatNumber(data.free_heap) +
' / ' +
@@ -290,7 +290,7 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="PSRAM (Size / Free)"
primary={LL.PSRAM()}
secondary={formatNumber(data.psram_size) + ' / ' + formatNumber(data.free_psram) + ' bytes'}
/>
</ListItem>
@@ -304,7 +304,7 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Flash Chip (Size / Speed)"
primary={LL.FLASH()}
secondary={
formatNumber(data.flash_chip_size) + ' bytes / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz'
}
@@ -318,7 +318,7 @@ const SystemStatusForm: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="File System (Used / Total)"
primary={LL.FILESYSTEM()}
secondary={
formatNumber(data.fs_used) +
' / ' +
@@ -335,7 +335,7 @@ const SystemStatusForm: FC = () => {
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</ButtonRow>
</Box>
@@ -348,7 +348,7 @@ const SystemStatusForm: FC = () => {
color="primary"
onClick={() => setConfirmRestart(true)}
>
Restart
{LL.RESTART()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
@@ -356,7 +356,7 @@ const SystemStatusForm: FC = () => {
onClick={() => setConfirmFactoryReset(true)}
color="error"
>
Factory reset
{LL.FACTORY_RESET()}
</Button>
</ButtonRow>
</Box>
@@ -370,7 +370,7 @@ const SystemStatusForm: FC = () => {
};
return (
<SectionContent title="System Status" titleGutter>
<SectionContent title={LL.SYSTEM() + ' ' + LL.STATUS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -7,9 +7,13 @@ import { FileUploadConfig } from '../../api/endpoints';
import GeneralFileUpload from './GeneralFileUpload';
import RestartMonitor from './RestartMonitor';
import { useI18nContext } from '../../i18n/i18n-react';
const UploadFileForm: FC = () => {
const [restarting, setRestarting] = useState<boolean>();
const { LL } = useI18nContext();
const uploadFile = useRef(async (file: File, config?: FileUploadConfig) => {
const response = await SystemApi.uploadFile(file, config);
setRestarting(true);
@@ -17,7 +21,7 @@ const UploadFileForm: FC = () => {
});
return (
<SectionContent title="Upload/Download" titleGutter>
<SectionContent title={LL.UPLOAD_DOWNLOAD()} titleGutter>
{restarting ? <RestartMonitor /> : <GeneralFileUpload uploadGeneralFile={uploadFile.current} />}
</SectionContent>
);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.331h512v341.337H0z"/><path d="M0 85.331h512v113.775H0z"/><path fill="#FFDA44" d="M0 312.882h512v113.775H0z"/></svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512V426.67H0z"/><path fill="#D80027" d="M288 85.33h-64v138.666H0v64h224v138.666h64V287.996h224v-64H288z"/><g fill="#0052B4"><path d="M393.785 315.358 512 381.034v-65.676zM311.652 315.358 512 426.662v-31.474l-143.693-79.83zM458.634 426.662l-146.982-81.664v81.664z"/></g><path fill="#FFF" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><path fill="#D80027" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><g fill="#0052B4"><path d="M90.341 315.356 0 365.546v-50.19zM200.348 329.51v97.151H25.491z"/></g><path fill="#D80027" d="M143.693 315.358 0 395.188v31.474l200.348-111.304z"/><g fill="#0052B4"><path d="M118.215 196.634 0 130.958v65.676zM200.348 196.634 0 85.33v31.474l143.693 79.83zM53.366 85.33l146.982 81.664V85.33z"/></g><path fill="#FFF" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><path fill="#D80027" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><g fill="#0052B4"><path d="M421.659 196.636 512 146.446v50.19zM311.652 182.482V85.331h174.857z"/></g><path fill="#D80027" d="M368.307 196.634 512 116.804V85.33L311.652 196.634z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.5 513 342"><path fill="#FFF" d="M0 85.5h513v342H0z"/><path fill="#cd1f2a" d="M0 85.5h513v114H0z"/><path fill="#1d4185" d="M0 312h513v114H0z"/></svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.334h512v341.337H0z"/><path fill="#FFF" d="M512 295.883H202.195v130.783H122.435V295.883H0V216.111h122.435V85.329H202.195v130.782H512V277.329z"/><path fill="#2E52B2" d="M512 234.666v42.663H183.652v149.337h-42.674V277.329H0v-42.663h140.978V85.329h42.674v149.337z"/></svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><g fill="#FFF"><path d="M0 85.337h512v341.326H0z"/><path d="M0 85.337h512V256H0z"/></g><path fill="#D80027" d="M0 256h512v170.663H0z"/></svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#0052B4" d="M0 85.333h512V426.67H0z"/><path fill="#FFDA44" d="M192 85.33h-64v138.666H0v64h128v138.666h64V287.996h320v-64H192z"/></svg>

After

Width:  |  Height:  |  Size: 217 B

View File

@@ -0,0 +1,265 @@
import type { Translation } from '../i18n-types';
const de: Translation = {
LANGUAGE: 'Sprache',
RETRY: 'Neuer Versuch',
LOADING: 'Laden',
IS_REQUIRED: 'ist erforderlich',
SIGN_IN: 'Einloggen',
SIGN_OUT: 'Ausloggen',
USERNAME: 'Nutzername',
PASSWORD: 'Passwort',
DASHBOARD: 'Kontrollzentrum',
SETTINGS: 'Einstellungen',
SAVED: 'gespeichert',
HELP: 'Hilfe',
LOGGED_IN: 'Eingeloggt als {name}',
PLEASE_SIGNIN: 'Bitte einloggen, um fortzufahren',
UPLOAD_SUCCESSFUL: 'Hochladen erfolgreich',
DOWNLOAD_SUCCESSFUL: 'Herunterladen erfolgreich',
INVALID_LOGIN: 'Ungültige Login Daten',
NETWORK: 'Netzwerk',
SECURITY: 'Sicherheit',
ONOFF_CAP: 'AN/AUS',
ONOFF: 'an/aus',
TYPE: 'Typ',
DESCRIPTION: 'Bezeichnung',
ENTITIES: 'Entitäten',
REFRESH: 'Aktualisieren',
EXPORT: 'Exportieren',
DEVICE_DETAILS: 'Geräte Details',
BRAND: 'Marke',
ENTITY_NAME: 'Entitätsname',
VALUE: 'Wert',
SHOW_FAV: 'nur Favoriten anzeigen',
DEVICE_SENSOR_DATA: 'Geräte- und Sensordaten',
DEVICES_SENSORS: 'Geräte & Sensoren',
ATTACHED_SENSORS: 'Angeschlossene EMS-ESP Sensoren',
RUN_COMMAND: 'Befehl ausführen',
CHANGE_VALUE: 'Wert ändern',
CANCEL: 'Abbrechen',
RESET: 'Zurücksetzen',
SEND: 'Senden',
SAVE: 'Speichern',
REMOVE: 'Entfernen',
PROBLEM_UPDATING: 'Problem beim Aktualisieren',
PROBLEM_LOADING: 'Problem beim Laden',
ACCESS_DENIED: 'Zugriff abgelehnt',
ANALOG_SENSOR: 'Analogsensor',
ANALOG_SENSORS: 'Analogsensoren',
UPDATED: 'Aktualisiert',
UPDATE: 'Aktualisieren',
REMOVED: 'Entfernt',
DELETION: 'Löschung',
OFFSET: 'Addition',
FACTOR: 'Faktor',
FREQ: 'Frequenz',
STARTVALUE: 'Startwert',
WARN_GPIO: 'Warnung: Vorsicht bei der korrekten Wahl des GPIO!',
EDIT: 'Editiere',
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperatursensoren',
WRITE_COMMAND: 'Befehl schreiben {cmd}',
EMS_BUS_WARNING:
'EMS-Bus getrennt. Wenn diese Warnung nach einigen Sekunden immer noch besteht, überprüfen Sie bitte die Einstellungen und das Board-Profil',
EMS_BUS_SCANNING: 'Suche nach EMS Geräten...',
CONNECTED: 'Verbunden',
TX_ISSUES: 'Tx-Probleme - versuchen Sie einen anderen Tx-Modus',
DISCONNECTED: 'Getrennt',
EMS_SCAN: 'Möchten Sie wirklich eine vollständige Gerätesuche des EMS-Busses starten?',
EMS_BUS_STATUS: 'EMS-Busstatus',
ACTIVE_DEVICES: 'Aktive Geräte und Sensoren',
DEVICE: 'Gerät',
SUCCESS: 'ERFOLG',
FAIL: 'FEHLER',
QUALITY: 'QUALITÄT',
SCAN_DEVICES: 'Nach neuen Geräten suchen',
EMS_BUS_STATUS_TITLE: 'EMS-Bus- und Aktivitätsstatus',
SCAN: 'Suche',
STATUS_NAMES: [
'EMS-Telegramme empfangen (Rx)',
'EMS-Telegramme gelesen (Tx)',
'EMS-Telegramme geschrieben (Tx)',
'Temperatursensoren gelesen',
'Analogsensoren gelesen',
'MQTT-Nachrichten gesendet',
'API-Aufrufe',
'Syslog-Mitteilungen'
],
NUM_DEVICES: '{num} Gerät{{e}}',
NUM_TEMP_SENSORS: '{num} Temperatursensor{{en}}',
NUM_ANALOG_SENSORS: '{num} Analogsensor{{en}}',
NUM_DAYS: '{num} Tag{{e}}',
NUM_SECONDS: '{num} Sekunde{{n}}',
NUM_HOURS: '{num} Stunde{{n}}',
NUM_MINUTES: '{num} Minute{{n}}',
APPLICATION_SETTINGS: 'Anwendungseinstellungen',
CUSTOMIZATION: 'Anpassungen',
APPLICATION_RESTARTING: 'EMS-ESP startet neu',
BOARD_PROFILE_TEXT:
'Wählen Sie ein vorkonfiguriertes Platinenprofil aus der Liste unten aus oder wählen Sie "Custom", um Ihre eigenen Hardwareeinstellungen zu konfigurieren',
BOARD_PROFILE: 'Platinenprofil',
BUTTON: 'Taste',
TEMPERATURE: 'Temperatur',
DISABLED: 'deaktiviert',
GENERAL_OPTIONS: 'Allgemeine Optionen',
LANGUAGE_ENTITIES: 'Sprache (für Geräteentitäten)',
HIDE_LED: 'LED ausblenden',
ENABLE_TELNET: 'Aktiviere Telnet Konsole',
ENABLE_ANALOG: 'Aktiviere Analogsensorsen',
CONVERT_FAHRENHEIT: 'Konvertiere Temperaturwerte in Fahrenheit',
BYPASS_TOKEN: 'Zugriffstoken-Autorisierung bei API-Aufrufen umgehen',
READONLY: 'Nur-Lese-Modus aktivieren (blockiert alle ausgehenden EMS Tx Write-Befehle)',
UNDERCLOCK_CPU: 'CPU-Geschwindigkeit untertakten',
ENABLE_SHOWER_TIMER: 'Duschtimer aktivieren',
ENABLE_SHOWER_ALERT: 'Duschalarm aktivieren',
TRIGGER_TIME: 'Auslösezeit',
COLD_SHOT_DURATION: 'Kaltschussdauer',
FORMATTING_OPTIONS: 'Formatierungsoptionen',
BOOLEAN_FORMAT_DASHBOARD: 'Boolsches Format für Web',
BOOLEAN_FORMAT_API: 'Boolesches Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Parasitäre Stomversorgung',
LOGGING: 'Protokollierung',
LOG_HEX: 'EMS-Telegramme hexadezimal protokollieren',
ENABLE_SYSLOG: 'Syslog aktivieren',
MARK_INTERVAL: 'Intervallmarke',
SECONDS: 'Sekunden',
MINUTES: 'Minuten',
HOURS: 'Stunden',
RESTART: 'Neu starten',
RESTART_TEXT: 'EMS-ESP muss neu gestartet werden, um geänderte Systemeinstellungen zu übernehmen',
COMMAND: 'Befehl',
CUSTOMIZATIONS_RESTART: 'Alle Anpassungen wurden entfernt. Neustart...',
CUSTOMIZATIONS_FULL: 'Ausgewählte Entitäten haben das Limit überschritten. Bitte stapelweise speichern',
CUSTOMIZATIONS_SAVED: 'Anpassungen gespeichert',
CUSTOMIZATIONS_HELP_1: 'Wählen Sie ein Gerät aus und passen Sie die Entitäten mithilfe der Optionen an',
CUSTOMIZATIONS_HELP_2: 'als Favorit markieren',
CUSTOMIZATIONS_HELP_3: 'Schreibaktion deaktivieren',
CUSTOMIZATIONS_HELP_4: 'von MQTT und API ausschließen',
CUSTOMIZATIONS_HELP_5: 'Aus dem Kontrollzentrum ausblenden',
SELECT_DEVICE: 'Wählen Sie ein Gerät aus',
SET_ALL: 'setzen Sie alle',
OPTIONS: 'Optionen',
NAME: 'Name',
CUSTOMIZATIONS_RESET:
'Möchten Sie wirklich alle Anpassungen entfernen, einschließlich der benutzerdefinierten Einstellungen der Temperatur- und Analogsensoren?',
DEVICE_ENTITIES: 'Geräteentitäten',
USER_CUSTOMIZATION: 'Benutzeranpassung',
SUPPORT_INFORMATION: 'Unterstützende Informationen',
CLICK_HERE: 'Hier klicken',
HELP_INFORMATION_1: 'EMS-ESP Konfigurationsanweisungen und mehr finden Sie im Online-Wiki',
HELP_INFORMATION_2: 'Für einen Live-Community-Chat besuchen Sie unseren Discord-Server',
HELP_INFORMATION_3: 'Um neue Funktionen anzufragen oder Fehler zu melden, eröffnen Sie ein Issue auf Github',
HELP_INFORMATION_4: 'Bitte laden Sie die System-Details und hängen Sie sie an das Support-Issue an. ',
HELP_INFORMATION_5:
'EMS-ESP ist ein freies Open-Source Projekt. Bitte unterstützen Sie die zukünftige Entwicklung mit einem "Star" auf Github!',
SUPPORT_INFO: 'Support Info',
UPLOAD: 'Hochladen',
DOWNLOAD: 'Herunterladen',
ABORTED: 'abgebrochen',
FAILED: 'gescheitert',
SUCCESSFUL: 'erfolgreich',
SYSTEM: 'System',
LOG: 'Log',
STATUS: 'Status',
UPLOAD_DOWNLOAD: 'Hoch-/Herunterladen',
SYSTEM_VERSION_RUNNING: 'Sie verwenden die Version',
SYSTEM_APPLY_FIRMWARE: 'um die neue Firmware anzuwenden',
CLOSE: 'Schließen',
USE: 'Verwenden',
FACTORY_RESET: 'Werkseinstellung',
SYSTEM_FACTORY_TEXT: 'EMS-ESP wurde auf Werkseinstellung gesetzt und startet als Zugangspunkt neu',
SYSTEM_FACTORY_TEXT_DIALOG: 'Sind Sie sicher alle Einstellungen auf Werkseinstellung zu setzen?',
VERSION_CHECK: 'Versionsprüfung',
THE_LATEST: 'Die neueste',
PLATFORM: 'Platform (Platform / SDK)',
UPTIME: 'System Betriebszeit',
CPU_FREQ: 'CPU Frequenz',
HEAP: 'RAM Speicher (Frei / Max Belegt)',
PSRAM: 'PSRAM (Größe / Frei)',
FLASH: 'Flash Speicher (Größe / Geschwindigkeit)',
FILESYSTEM: 'Dateisystem (Genutzt / Gesamt)',
BUFFER_SIZE: 'Puffergröße',
COMPACT: 'Kompakte Darstellung',
ENABLE_OTA: 'OTA Updates verwenden',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Herunterladen der individuellen Entitätsanpassungen',
DOWNLOAD_SETTINGS_TEXT:
'Herunterladen der Anwendungseinstellungen. Vorsicht beim Teilen der Einstellungen, da sie Passwörter und andere sensitive Einstellungen enthalten',
UPLOAD_TEXT: 'Hochladen von neuer Firmware (.bin), Geräte- oder Entitätseinstellungen (.json)',
UPLOADING: 'Hochladen',
UPLOAD_DROP_TEXT: 'Klicken Sie hier, oder ziehen eine Datei hierher',
ERROR: 'Unerwarteter Fehler, bitter versuchen Sie es erneut',
TIME_SET: 'Zeit gesetzt',
MANAGE_USERS: 'Nutzerverwaltung',
IS_ADMIN: 'ist Admin',
USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren',
ADD: 'Hinzufügen',
ACCESS_TOKEN_FOR: 'Zugangs-Token für',
ACCESS_TOKEN_TEXT:
'Dieses Token ist für REST API Aufrufe bestimmt, die eine Authentifizierung benötigen. Es kann entweder als Bearer Token im `Authorization-Header` oder in der Access_Token URL verwendet werden.',
GENERATING_TOKEN: 'Erzeuge Token',
USER: 'Nutzer',
MODIFY: 'Ändern',
SU_TEXT:
'Das su (super user) Passwort wird zum Signieren der Authentifikations-Tokens verwendet und ermöglicht Admin-Berechtigung in der Konsole.',
NOT_ENABLED: 'Nicht aktiviert',
ERRORS: 'Fehler',
DISCONNECT_REASON: 'Grund der Verbindungsunterbrechung',
ENABLE_MQTT: 'MQTT aktivieren',
OPTIONAL: 'Optional',
FORMATTING: 'Formattierung',
FORMAT: 'Format',
MQTT_NEST_1: 'Als Nester in in einem Gesamttopic',
MQTT_NEST_2: 'Als einzelne Topics',
MQTT_RESPONSE: 'Veröffentliche die Kommandoantwort als `response` Topic',
MQTT_PUBLISH_TEXT_1: 'Veröffentliche einzelne Werte bei Veränderung als eigene Topics',
MQTT_PUBLISH_TEXT_2: 'Veröffentliche als Kommando-Topic (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Aktiviere `MQTT Discovery` (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix für die `Discovery`-Topics',
MQTT_PUBLISH_INTERVALS: 'Veröffentlichungs-Intervalle',
MQTT_INT_BOILER: 'Boiler und Wärmepumpen',
MQTT_INT_THERMOSTATS: 'Thermostate',
MQTT_INT_SOLAR: 'Solarmodule',
MQTT_INT_MIXER: 'Mischermodule',
DEFAULT: 'Standard',
MQTT_CLEAN_SESSION: 'Setze `Clean Session`',
MQTT_RETAIN_FLAG: 'Setze `Retain flag` immer',
INACTIVE: 'Inaktiv',
ACTIVE: 'Aktiv',
UNKNOWN: 'Unbekannt',
SET_TIME: 'Zeiteinstellung',
SET_TIME_TEXT: 'Geben Sie das lokale Datum und die Zeit ein',
LOCAL_TIME: 'Lokalzeit',
UTC_TIME: 'UTC Zeit',
ENABLE_NTP: 'Aktiviere NTP',
TIME_ZONE: 'Zeitzone',
ACCESS_POINT: 'Zugangspunkt',
AP_PROVIDE: 'Aktiviere Zugangspunkt',
AP_PROVIDE_TEXT_1: 'Immer',
AP_PROVIDE_TEXT_2: 'Wenn WiFi nicht verbunden',
AP_PROVIDE_TEXT_3: 'Niemals',
AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal',
AP_HIDE_SSID: 'Verstecke SSID',
NETWORK_SCAN: 'Suche nach WiFi Netzwerken',
IDLE: 'Leerlauf',
LOST: 'Verloren',
SCANNING: 'Suche',
SCAN_AGAIN: 'Erneute Suche',
NETWORK_SCANNER: 'Netzwerk Suche',
NETWORK_NO_WIFI: 'Keine WiFi Netzwerke gefunden',
NETWORK_BLANK_SSID: 'Freilassen um WiFi zu deaktivieren',
POWER: 'Leistung',
NETWORK_DISABLE_SLEEP: 'Deaktiviere WiFi Schlafmodus',
NETWORK_LOW_BAND: 'Verwende niedrige WiFi Bandbreite',
NETWORK_USE_DNS: 'Aktiviere mDNS Service',
NETWORK_ENABLE_IPV6: 'Aktiviere IPv6 Unterstützung',
NETWORK_FIXED_IP: 'Feste IP Addresse',
ADMIN: 'Administrator',
GUEST: 'Gast',
NEW: 'Neuer',
RENAME: 'Ändere'
};
export default de;

View File

@@ -0,0 +1,265 @@
import type { BaseTranslation } from '../i18n-types';
const en: BaseTranslation = {
LANGUAGE: 'Language',
RETRY: 'Retry',
LOADING: 'Loading',
IS_REQUIRED: 'is required',
SIGN_IN: 'Sign In',
SIGN_OUT: 'Sign Out',
USERNAME: 'Username',
PASSWORD: 'Password',
DASHBOARD: 'Dashboard',
SETTINGS: 'Settings',
SAVED: 'saved',
HELP: 'Help',
LOGGED_IN: 'Logged in as {name}',
PLEASE_SIGNIN: 'Please sign in to continue',
UPLOAD_SUCCESSFUL: 'Upload successful',
DOWNLOAD_SUCCESSFUL: 'Download successful',
INVALID_LOGIN: 'Invalid login details',
NETWORK: 'Network',
SECURITY: 'Security',
ONOFF_CAP: 'ON/OFF',
ONOFF: 'on/off',
TYPE: 'Type',
DESCRIPTION: 'Description',
ENTITIES: 'Entities',
REFRESH: 'Refresh',
EXPORT: 'Export',
DEVICE_DETAILS: 'Device Details',
BRAND: 'Brand',
ENTITY_NAME: 'Entity Name',
VALUE: 'Value',
SHOW_FAV: 'only show favorites',
DEVICE_SENSOR_DATA: 'Device and Sensor Data',
DEVICES_SENSORS: 'Devices & Sensors',
ATTACHED_SENSORS: 'Attached EMS-ESP Sensors',
RUN_COMMAND: 'Call Command',
CHANGE_VALUE: 'Change Value',
CANCEL: 'Cancel',
RESET: 'Reset',
SEND: 'Send',
SAVE: 'Save',
REMOVE: 'Remove',
PROBLEM_UPDATING: 'Problem updating',
PROBLEM_LOADING: 'Problem loading',
ACCESS_DENIED: 'Access Denied',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analog Sensors',
UPDATED: 'Updated',
UPDATE: 'Update',
REMOVED: 'Removed',
DELETION: 'Deletion',
OFFSET: 'Offset',
FACTOR: 'Factor',
FREQ: 'Frequency',
STARTVALUE: 'Start value',
WARN_GPIO: 'Warning: be careful when assigning a GPIO!',
EDIT: 'Edit',
TEMP_SENSOR: 'Temperature Sensor',
TEMP_SENSORS: 'Temperature Sensors',
WRITE_COMMAND: 'Write command {cmd}',
EMS_BUS_WARNING:
'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
EMS_BUS_SCANNING: 'Scanning for EMS devices...',
CONNECTED: 'Connected',
TX_ISSUES: 'Tx issues - try a different Tx Mode',
DISCONNECTED: 'Disconnected',
EMS_SCAN: 'Are you sure you want to initiate a full device scan of the EMS bus?',
EMS_BUS_STATUS: 'EMS Bus Status',
ACTIVE_DEVICES: 'Active Devices & Sensors',
DEVICE: 'Device',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
QUALITY: 'QUALITY',
SCAN_DEVICES: 'Scan for new devices',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activity Status',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrams Received (Rx)',
'EMS Reads (Tx)',
'EMS Writes (Tx)',
'Temperature Sensor Reads',
'Analog Sensor Reads',
'MQTT Publishes',
'API Calls',
'Syslog Messages'
],
NUM_DEVICES: '{num} Device{{s}}',
NUM_TEMP_SENSORS: '{num} Temperature Sensor{{s}}',
NUM_ANALOG_SENSORS: '{num} Analog Sensor{{s}}',
NUM_DAYS: '{num} Day{{s}}',
NUM_SECONDS: '{num} Second{{s}}',
NUM_HOURS: '{num} Hour{{s}}',
NUM_MINUTES: '{num} Minute{{s}}',
APPLICATION_SETTINGS: 'Application Settings',
CUSTOMIZATION: 'Customization',
APPLICATION_RESTARTING: 'EMS-ESP is restarting',
BOARD_PROFILE_TEXT:
'Select a pre-configured interface board profile from the list below or choose Custom to configure your own hardware settings',
BOARD_PROFILE: 'Board Profile',
BUTTON: 'Button',
TEMPERATURE: 'Temperature',
DISABLED: 'disabled',
GENERAL_OPTIONS: 'General Options',
LANGUAGE_ENTITIES: 'Language (for device entities)',
HIDE_LED: 'Hide LED',
ENABLE_TELNET: 'Enable Telnet Console',
ENABLE_ANALOG: 'Enable Analog Sensors',
CONVERT_FAHRENHEIT: 'Convert temperature values to Fahrenheit',
BYPASS_TOKEN: 'Bypass Access Token authorization on API calls',
READONLY: 'Enable read-only mode (blocks all outgoing EMS Tx Write commands)',
UNDERCLOCK_CPU: 'Underclock CPU speed',
ENABLE_SHOWER_TIMER: 'Enable Shower Timer',
ENABLE_SHOWER_ALERT: 'Enable Shower Alert',
TRIGGER_TIME: 'Trigger Time',
COLD_SHOT_DURATION: 'Cold Shot Duration',
FORMATTING_OPTIONS: 'Formatting Options',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean Format Dashboard',
BOOLEAN_FORMAT_API: 'Boolean Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Enable parasite power',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrams in hexadecimal',
ENABLE_SYSLOG: 'Enable Syslog',
MARK_INTERVAL: 'Mark Interval',
SECONDS: 'seconds',
MINUTES: 'minutes',
RESTART: 'Restart',
HOURS: 'hours',
RESTART_TEXT: 'EMS-ESP needs to be restarted to apply changed system settings',
COMMAND: 'Command',
CUSTOMIZATIONS_RESTART: 'All customizations have been removed. Restarting...',
CUSTOMIZATIONS_FULL: 'Selected entities exceeded limit. Please save in batches',
CUSTOMIZATIONS_SAVED: 'Customizations saved',
CUSTOMIZATIONS_HELP_1: 'Select a device and customize the entities options or click to rename',
CUSTOMIZATIONS_HELP_2: 'mark as favorite',
CUSTOMIZATIONS_HELP_3: 'disable write action',
CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API',
CUSTOMIZATIONS_HELP_5: 'hide from Dashboard',
SELECT_DEVICE: 'Select a device',
SET_ALL: 'set all',
OPTIONS: 'Options',
NAME: 'Name',
CUSTOMIZATIONS_RESET:
'Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors?',
DEVICE_ENTITIES: 'Device Entities',
USER_CUSTOMIZATION: 'User Customization',
SUPPORT_INFORMATION: 'Support Information',
CLICK_HERE: 'Click Here',
HELP_INFORMATION_1: 'Visit the online wiki to get instructions on how to configure EMS-ESP',
HELP_INFORMATION_2: 'For live community chat join our Discord server',
HELP_INFORMATION_3: 'To request a feature or report a bug',
HELP_INFORMATION_4: 'remember to download and attach your system information for a faster response when reporting an issue',
HELP_INFORMATION_5:
"EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!",
SUPPORT_INFO: 'Support Info',
UPLOAD: 'Upload',
DOWNLOAD: 'Download',
ABORTED: 'aborted',
FAILED: 'failed',
SUCCESSFUL: 'successful',
SYSTEM: 'System',
LOG: 'Log',
STATUS: 'Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
SYSTEM_VERSION_RUNNING: 'You are currently running version',
SYSTEM_APPLY_FIRMWARE: 'to apply the new firmware',
CLOSE: 'Close',
USE: 'Use',
FACTORY_RESET: 'Factory Reset',
SYSTEM_FACTORY_TEXT: 'Device has been factory reset and will now restart',
SYSTEM_FACTORY_TEXT_DIALOG: 'Are you sure you want to reset the device to its factory defaults?',
VERSION_CHECK: 'Version Check',
THE_LATEST: 'The latest',
PLATFORM: 'Device (Platform / SDK)',
UPTIME: 'System Uptime',
CPU_FREQ: 'CPU Frequency',
HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size / Speed)',
FILESYSTEM: 'File System (Used / Total)',
BUFFER_SIZE: 'Buffer Size',
COMPACT: 'Compact',
ENABLE_OTA: 'Enable OTA Updates',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download the entity customizations',
DOWNLOAD_SETTINGS_TEXT:
'Download the application settings. Be careful when sharing your settings as this file contains passwords and other sensitive system information',
UPLOAD_TEXT: 'Upload a new firmware (.bin) file, settings or customizations (.json) file below',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Drop file or click here',
ERROR: 'Unexpected Error, please try again',
TIME_SET: 'Time set',
MANAGE_USERS: 'Manage Users',
IS_ADMIN: 'is Admin',
USER_WARNING: 'You must have at least one admin user configured',
ADD: 'Add',
ACCESS_TOKEN_FOR: 'Access Token for',
ACCESS_TOKEN_TEXT:
'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
GENERATING_TOKEN: 'Generating token',
USER: 'User',
MODIFY: 'Modify',
SU_TEXT:
'The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console.',
NOT_ENABLED: 'Not enabled',
ERRORS: 'Errors',
DISCONNECT_REASON: 'Disconnect Reason',
ENABLE_MQTT: 'Enable MQTT',
OPTIONAL: 'Optional',
FORMATTING: 'Formatting',
FORMAT: 'Format',
MQTT_NEST_1: 'Nested in a single topic',
MQTT_NEST_2: 'As individual topics',
MQTT_RESPONSE: 'Publish command output to a `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publish single value topics on change',
MQTT_PUBLISH_TEXT_2: 'Publish to command topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Enable MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix for the Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publish Intervals',
MQTT_INT_BOILER: 'Boilers and Heat Pumps',
MQTT_INT_THERMOSTATS: 'Thermostats',
MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules',
DEFAULT: 'Default',
MQTT_CLEAN_SESSION: 'Set Clean Session',
MQTT_RETAIN_FLAG: 'Always set Retain flag',
INACTIVE: 'Inactive',
ACTIVE: 'Active',
UNKNOWN: 'Unknown',
SET_TIME: 'Set Time',
SET_TIME_TEXT: 'Enter local date and time below to set the time',
LOCAL_TIME: 'Local Time',
UTC_TIME: 'UTC Time',
ENABLE_NTP: 'Enable NTP',
TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'always',
AP_PROVIDE_TEXT_2: 'when WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'never',
AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID',
NETWORK_SCAN: 'Scan WiFi Networks',
IDLE: 'Idle',
LOST: 'Lost',
SCANNING: 'Scanning',
SCAN_AGAIN: 'Scan again',
NETWORK_SCANNER: 'Network Scanner',
NETWORK_NO_WIFI: 'No WiFi networks found',
NETWORK_BLANK_SSID: 'leave blank to disable WiFi',
POWER: 'Power',
NETWORK_DISABLE_SLEEP: 'Disable WiFi Sleep Mode',
NETWORK_LOW_BAND: 'Use Lower WiFi Bandwidth',
NETWORK_USE_DNS: 'Enable mDNS Service',
NETWORK_ENABLE_IPV6: 'Enable IPv6 support',
NETWORK_FIXED_IP: 'Use Fixed IP address',
ADMIN: 'Admin',
GUEST: 'Guest',
NEW: 'New',
RENAME: 'Rename'
};
export default en;

View File

@@ -0,0 +1,11 @@
import type { FormattersInitializer } from 'typesafe-i18n';
import type { Locales, Formatters } from './i18n-types';
import { date } from 'typesafe-i18n/formatters';
export const initFormatters: FormattersInitializer<Locales, Formatters> = (locale: Locales) => {
const formatters: Formatters = {
weekday: date(locale, { weekday: 'long' })
};
return formatters;
};

View File

@@ -0,0 +1,16 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { useContext } from 'react'
import { initI18nReact } from 'typesafe-i18n/react'
import type { I18nContextType } from 'typesafe-i18n/react'
import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales } from './i18n-util'
const { component: TypesafeI18n, context: I18nContext } = initI18nReact<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
const useI18nContext = (): I18nContextType<Locales, Translations, TranslationFunctions> => useContext(I18nContext)
export { I18nContext, useI18nContext }
export default TypesafeI18n

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales, locales } from './i18n-util'
const localeTranslationLoaders = {
de: () => import('./de'),
en: () => import('./en'),
nl: () => import('./nl'),
no: () => import('./no'),
pl: () => import('./pl'),
se: () => import('./se'),
}
const updateDictionary = (locale: Locales, dictionary: Partial<Translations>) =>
loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
export const importLocaleAsync = async (locale: Locales) =>
(await localeTranslationLoaders[locale]()).default as unknown as Translations
export const loadLocaleAsync = async (locale: Locales): Promise<void> => {
updateDictionary(locale, await importLocaleAsync(locale))
loadFormatters(locale)
}
export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync))
export const loadFormatters = (locale: Locales): void =>
void (loadedFormatters[locale] = initFormatters(locale))

View File

@@ -0,0 +1,34 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales, locales } from './i18n-util'
import de from './de'
import en from './en'
import nl from './nl'
import no from './no'
import pl from './pl'
import se from './se'
const localeTranslations = {
de,
en,
nl,
no,
pl,
se,
}
export const loadLocale = (locale: Locales): void => {
if (loadedLocales[locale]) return
loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
loadFormatters(locale)
}
export const loadAllLocales = (): void => locales.forEach(loadLocale)
export const loadFormatters = (locale: Locales): void =>
void (loadedFormatters[locale] = initFormatters(locale))

View File

@@ -0,0 +1,37 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
import type { LocaleDetector } from 'typesafe-i18n/detectors'
import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'
export const baseLocale: Locales = 'en'
export const locales: Locales[] = [
'de',
'en',
'nl',
'no',
'pl',
'se'
]
export const isLocale = (locale: string) => locales.includes(locale as Locales)
export const loadedLocales = {} as Record<Locales, Translations>
export const loadedFormatters = {} as Record<Locales, Formatters>
export const i18nString = (locale: Locales) => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale])
export const i18nObject = (locale: Locales) =>
initI18nObject<Locales, Translations, TranslationFunctions, Formatters>(
locale,
loadedLocales[locale],
loadedFormatters[locale]
)
export const i18n = () => initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
export const detectLocale = (...detectors: LocaleDetector[]) => detectLocaleFn<Locales>(baseLocale, locales, ...detectors)

View File

@@ -0,0 +1,265 @@
import type { BaseTranslation } from '../i18n-types';
const nl: BaseTranslation = {
LANGUAGE: 'Taal',
RETRY: 'Opnieuw proberen',
LOADING: 'Laden',
IS_REQUIRED: 'is verplicht',
SIGN_IN: 'Inloggen',
SIGN_OUT: 'Uitloggen',
USERNAME: 'Gebruikersnaam',
PASSWORD: 'Wachtwoord',
DASHBOARD: 'Dashboard',
SETTINGS: 'Instellingen',
SAVED: 'opgeslagen',
HELP: 'Help',
LOGGED_IN: 'Ingelogd als {name}',
PLEASE_SIGNIN: 'Log in om verder te gaan',
UPLOAD_SUCCESSFUL: 'Upload successvol',
DOWNLOAD_SUCCESSFUL: 'Download successvol',
INVALID_LOGIN: 'Logingegevens fout',
NETWORK: 'Netwerk',
SECURITY: 'Beveiliging',
ONOFF_CAP: 'AAN/UIT',
ONOFF: 'aan/uit',
TYPE: 'Type',
DESCRIPTION: 'Beschrijving',
ENTITIES: 'Entiteiten',
REFRESH: 'Ververs',
EXPORT: 'Export',
BRAND: 'Merk',
ENTITY_NAME: 'Entiteit',
VALUE: 'Waarde',
SHOW_FAV: 'alleen favorieten weergeven',
DEVICE_SENSOR_DATA: 'Apparaat en Sensor data',
DEVICES_SENSORS: 'Apparaten & Sensoren',
ATTACHED_SENSORS: 'Aangesloten EMS-ESP sensoren',
RUN_COMMAND: 'Call commando',
CHANGE_VALUE: 'Wijzig waarde',
CANCEL: 'Annuleren',
RESET: 'Reset',
SEND: 'Verzenden',
SAVE: 'Opslaan',
REMOVE: 'Verwijderen',
PROBLEM_UPDATING: 'Probleem met updaten',
PROBLEM_LOADING: 'Probleem met laden',
ACCESS_DENIED: 'Toegang geweigerd',
ANALOG_SENSOR: 'Analoge sensor',
ANALOG_SENSORS: 'Analoge Sensoren',
UPDATED: 'Bijgewerkt',
UPDATE: 'Bijwerken',
REMOVED: 'Verwijderd',
DELETION: 'Verwijder',
OFFSET: 'Offset',
FACTOR: 'Factor',
FREQ: 'Frequentie',
STARTVALUE: 'Startwaarde',
WARN_GPIO: 'Waarschuwing: let op met het koppelen van de juiste GPIO pin!',
EDIT: 'Wijzigen',
TEMP_SENSOR: 'Temperatuur sensor',
TEMP_SENSORS: 'Temperatuur Sensoren',
WRITE_COMMAND: 'Schrijf commando {cmd}',
EMS_BUS_WARNING:
'EMS bus niet gevonden. Als deze waarschuwing blijft staan na een paar seconden dan loop de instellingen na en in het bijzonder het apparaat type profiel na.',
EMS_BUS_SCANNING: 'Scannen naar EMS apparaten...',
CONNECTED: 'Verbonden',
TX_ISSUES: 'Tx bus probleem. Probeer een andere Tx verzendmodus',
DISCONNECTED: 'Niet verbonden',
EMS_SCAN: 'Weet je zeker dat je een volledige EMS bus scan uit wilt voeren?',
EMS_BUS_STATUS: 'EMS busstatus',
ACTIVE_DEVICES: 'Actieve Apparaten & Sensoren',
DEVICE: 'Apparaat',
SUCCESS: 'SUCCESS',
FAIL: 'MISLUKT',
QUALITY: 'QUALITEIT',
SCAN_DEVICES: 'Scannen naar nieuwe apparaten',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activiteitenstatus',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrammen ontvangen (Rx)',
'EMS Leesopdrachten (Tx)',
'EMS Schrijfopdrachten (Tx)',
'Temperatuursensoren uitgelezen',
'Analoge sensoren uitgelezen',
'MQTT publicaties',
'API calls',
'Syslog berichten'
],
NUM_DEVICES: '{num} Apparaat{{en}}',
NUM_TEMP_SENSORS: '{num} Temperatuursensor{{en}}',
NUM_ANALOG_SENSORS: '{num} Analoge sensor{{en}}',
NUM_DAYS: '{num} Dag{{en}}',
NUM_SECONDS: '{num} Second{{en}}',
NUM_HOURS: '{num} Uur{{en}}',
NUM_MINUTES: '{num} Minuut{{en}}',
APPLICATION_SETTINGS: 'Applicatieinstellingen',
CUSTOMIZATION: 'Custom aanpassingen',
APPLICATION_RESTARTING: 'EMS-ESP herstarten',
BOARD_PROFILE_TEXT:
'Selecteer een vooraf ingesteld apparaat profiel uit de lijst of kies Eigen om zelf uw hardware te configureren',
BOARD_PROFILE: 'Apparaatprofiel',
BUTTON: 'Toets',
TEMPERATURE: 'Temperatuur',
DISABLED: 'Uitgeschakeld',
GENERAL_OPTIONS: 'Algemene Opties',
LANGUAGE_ENTITIES: 'Taal (voor apparaat entiteiten)',
HIDE_LED: 'Verberg LED',
ENABLE_TELNET: 'Activeer Telnet console',
ENABLE_ANALOG: 'Activeer analoge sensoren',
CONVERT_FAHRENHEIT: 'Converteer temperatuurwaarden naar Fahrenheit',
BYPASS_TOKEN: 'Bypass Access Token authorization on API calls',
READONLY: 'Activeer read-only modus (blokkeert alle outgaande EMS Tx schrijf commandos)',
UNDERCLOCK_CPU: 'Underclock CPU snelheid',
ENABLE_SHOWER_TIMER: 'Activeer Douche Timer (tijdmeting)',
ENABLE_SHOWER_ALERT: 'Activeer Douchemelding',
TRIGGER_TIME: 'Trigger tijd',
COLD_SHOT_DURATION: 'Tijd Shot koud water',
FORMATTING_OPTIONS: 'Formatteringsopties',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean formaat dashboard',
BOOLEAN_FORMAT_API: 'Boolean formaat API/MQTT',
ENUM_FORMAT: 'Enum formaat API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Activeer Dallas parasitaire modus',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrammen in hexadecimaal',
ENABLE_SYSLOG: 'Activeer Syslog',
MARK_INTERVAL: 'Markeringsinterval',
SECONDS: 'seconden',
MINUTES: 'minuten',
RESTART: 'Herstarten',
HOURS: 'uren',
RESTART_TEXT: 'EMS-ESP dient opnieuw gestart te worden om de wijzingen toe te passen',
COMMAND: 'Commando',
CUSTOMIZATIONS_RESTART: 'Alle custom profielen worden verwijderd. Herstarten...',
CUSTOMIZATIONS_FULL: 'Te veel entiteiten geselecteerd. Sla op in delen aub',
CUSTOMIZATIONS_SAVED: 'Custom aanpassingen opgeslagen',
CUSTOMIZATIONS_HELP_1: 'Selecteer een apparaat en pas de entiteiten aan door middel van de opties',
CUSTOMIZATIONS_HELP_2: 'Markeer as favoriet',
CUSTOMIZATIONS_HELP_3: 'Zet schrijfacties uit',
CUSTOMIZATIONS_HELP_4: 'Uitsluiten van MQTT en API',
CUSTOMIZATIONS_HELP_5: 'verberg van het Dashboard',
SELECT_DEVICE: 'Selecteer een apparaat',
SET_ALL: 'Alles aanzetten',
OPTIONS: 'Opties',
NAME: 'Naam',
CUSTOMIZATIONS_RESET:
'Weet je zeker dat je alle custom aanpassingen wilt verwijderen inclusief de custom instellingen voor analoge temperatuursensoren?',
DEVICE_ENTITIES: 'Apparaat Entiteiten',
USER_CUSTOMIZATION: 'Custom Instellingen',
SUPPORT_INFORMATION: 'Support Informatie',
CLICK_HERE: 'Klik Hier',
HELP_INFORMATION_1: 'Visit the online wiki to get instructions on how to configure EMS-ESP',
HELP_INFORMATION_2: 'For live community chat join our Discord server',
HELP_INFORMATION_3: 'To request a feature or report a bug',
HELP_INFORMATION_4:
'remember to download and attach your system information for a faster response when reporting an issue',
HELP_INFORMATION_5:
"EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!",
SUPPORT_INFO: 'Support Info',
UPLOAD: 'Upload',
DOWNLOAD: 'Download',
ABORTED: 'afgebroken',
FAILED: 'mislukt',
SUCCESSFUL: 'successvol',
SYSTEM: 'Systeem',
LOG: 'Log',
STATUS: 'Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
SYSTEM_VERSION_RUNNING: 'op dit moment draai je versie',
SYSTEM_APPLY_FIRMWARE: 'om de nieuwe firmware te activeren',
CLOSE: 'Sluiten',
USE: 'Gebruik',
FACTORY_RESET: 'Fabrieksinstellingen',
SYSTEM_FACTORY_TEXT: 'Gateway is gereset en start nu weer op met fabrieksinstellingen',
SYSTEM_FACTORY_TEXT_DIALOG: 'Weet je zeker dat je een reset naar fabrieksinstellingen uit wilt voeren?',
VERSION_CHECK: 'Versie Check',
THE_LATEST: 'De laatste',
PLATFORM: 'Apparaat (Platform / SDK)',
UPTIME: 'Systeem Uptime',
CPU_FREQ: 'CPU Frequency',
HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size / Speed)',
FILESYSTEM: 'File System (Used / Total)',
BUFFER_SIZE: 'Buffer Size',
COMPACT: 'Compact',
ENABLE_OTA: 'Acitveer OTA Updates',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download alle custom instellingen',
DOWNLOAD_SETTINGS_TEXT:
'Download de applicatie settings. Wees voorzichting met het delen van dit bestand want het bevat o.a. de wachtwoorden in plain text',
UPLOAD_TEXT: 'Upload een nieuwe firmware (.bin) file, instellingen of custom instellingen (.json) bestand hieronder',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Sleep bestand hierheen of klik hier',
ERROR: 'Onverwachte fout, probeer opnieuw',
TIME_SET: 'Tijd ingesteld',
MANAGE_USERS: 'Beheer Gebruikers',
IS_ADMIN: 'is Admin',
USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren',
ADD: 'Toevoegen',
ACCESS_TOKEN_FOR: 'Access Token voor',
ACCESS_TOKEN_TEXT:
'Het token hieronder wordt gebruikt voor de REST API calls die authorisatie nodig hebben. Het kan zowel als Bearer token in de Authorization header of in acccess_token URL query parameter gebruikt worden',
GENERATING_TOKEN: 'Token aan het genereren',
USER: 'Gebruiker',
MODIFY: 'Aanpassen',
SU_TEXT:
'Het su (super user) wachtwoord wordt gebruikt om authorisatie tokens te signeren en ook om admin privileges te activeren in de console.',
NOT_ENABLED: 'Niet geactiveerd',
ERRORS: 'Foutmeldingen',
DISCONNECT_REASON: 'Verbinding verbroken vanwege',
ENABLE_MQTT: 'Activeer MQTT',
OPTIONAL: 'Optioneel',
FORMATTING: 'Formatteren',
FORMAT: 'Formattering',
MQTT_NEST_1: 'Genest in 1 topic',
MQTT_NEST_2: 'Als individuele topics',
MQTT_RESPONSE: 'Publiceer commando output naar een `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publiceer enkele waarde topics on change',
MQTT_PUBLISH_TEXT_2: 'Publiceer naar commando topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Activeer MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix voor de Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publicatie intervallen',
MQTT_INT_BOILER: 'CV ketels en warmtepompen',
MQTT_INT_THERMOSTATS: 'Thermostaten',
MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules',
DEFAULT: 'Default',
MQTT_CLEAN_SESSION: 'Set Clean Session',
MQTT_RETAIN_FLAG: 'Always set Retain flag',
INACTIVE: 'Inactief',
ACTIVE: 'Actief',
UNKNOWN: 'Onbekend',
SET_TIME: 'Tijd instellen',
SET_TIME_TEXT: 'Geef de locale datum en tijd in',
LOCAL_TIME: 'Locale Tijd',
UTC_TIME: 'UTC Tijd',
ENABLE_NTP: 'Activeer NTP',
TIME_ZONE: 'Tijdzone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Activeer Access Point',
AP_PROVIDE_TEXT_1: 'altijd',
AP_PROVIDE_TEXT_2: 'als WiFi niet is verbonden',
AP_PROVIDE_TEXT_3: 'nooit',
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
AP_HIDE_SSID: 'SSID verbergen',
NETWORK_SCAN: 'Scan WiFi Networken',
IDLE: 'Idle',
LOST: 'Verloren',
SCANNING: 'Scannen',
SCAN_AGAIN: 'Opnieuw scannen',
NETWORK_SCANNER: 'Netwerk Scanner',
NETWORK_NO_WIFI: 'Geen WiFi networken gevonden',
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
POWER: 'Vermogen',
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
NETWORK_LOW_BAND: 'Lagere WiFi bandbreedte gebruiken',
NETWORK_USE_DNS: 'Activeer mDNS Service',
NETWORK_ENABLE_IPV6: 'Activeer IPv6 support',
NETWORK_FIXED_IP: 'Gebruik vast IP addres',
ADMIN: 'Admin',
GUEST: 'Gast',
NEW: 'Nieuwe',
RENAME: 'Hernoem'
};
export default nl;

View File

@@ -0,0 +1,265 @@
import type { BaseTranslation } from '../i18n-types';
const en: BaseTranslation = {
LANGUAGE: 'Language',
RETRY: 'Retry',
LOADING: 'Loading',
IS_REQUIRED: 'is required',
SIGN_IN: 'Sign In',
SIGN_OUT: 'Sign Out',
USERNAME: 'Username',
PASSWORD: 'Password',
DASHBOARD: 'Dashboard',
SETTINGS: 'Settings',
SAVED: 'saved',
HELP: 'Help',
LOGGED_IN: 'Logged in as {name}',
PLEASE_SIGNIN: 'Please sign in to continue',
UPLOAD_SUCCESSFUL: 'Upload successful',
DOWNLOAD_SUCCESSFUL: 'Download successful',
INVALID_LOGIN: 'Invalid login details',
NETWORK: 'Network',
SECURITY: 'Security',
ONOFF_CAP: 'ON/OFF',
ONOFF: 'on/off',
TYPE: 'Type',
DESCRIPTION: 'Description',
ENTITIES: 'Entities',
REFRESH: 'Refresh',
EXPORT: 'Export',
DEVICE_DETAILS: 'Device Details',
BRAND: 'Brand',
ENTITY_NAME: 'Entity Name',
VALUE: 'Value',
SHOW_FAV: 'only show favorites',
DEVICE_SENSOR_DATA: 'Device and Sensor Data',
DEVICES_SENSORS: 'Devices & Sensors',
ATTACHED_SENSORS: 'Attached EMS-ESP Sensors',
RUN_COMMAND: 'Call Command',
CHANGE_VALUE: 'Change Value',
CANCEL: 'Cancel',
RESET: 'Reset',
SEND: 'Send',
SAVE: 'Save',
REMOVE: 'Remove',
PROBLEM_UPDATING: 'Problem updating',
PROBLEM_LOADING: 'Problem loading',
ACCESS_DENIED: 'Access Denied',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analog Sensors',
UPDATED: 'Updated',
UPDATE: 'Update',
REMOVED: 'Removed',
DELETION: 'Deletion',
OFFSET: 'Offset',
FACTOR: 'Factor',
FREQ: 'Frequency',
STARTVALUE: 'Start value',
WARN_GPIO: 'Warning: be careful when assigning a GPIO!',
EDIT: 'Edit',
TEMP_SENSOR: 'Temperature Sensor',
TEMP_SENSORS: 'Temperature Sensors',
WRITE_COMMAND: 'Write command {cmd}',
EMS_BUS_WARNING:
'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
EMS_BUS_SCANNING: 'Scanning for EMS devices...',
CONNECTED: 'Connected',
TX_ISSUES: 'Tx issues - try a different Tx Mode',
DISCONNECTED: 'Disconnected',
EMS_SCAN: 'Are you sure you want to initiate a full device scan of the EMS bus?',
EMS_BUS_STATUS: 'EMS Bus Status',
ACTIVE_DEVICES: 'Active Devices & Sensors',
DEVICE: 'Device',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
QUALITY: 'QUALITY',
SCAN_DEVICES: 'Scan for new devices',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activity Status',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrams Received (Rx)',
'EMS Reads (Tx)',
'EMS Writes (Tx)',
'Temperature Sensor Reads',
'Analog Sensor Reads',
'MQTT Publishes',
'API Calls',
'Syslog Messages'
],
NUM_DEVICES: '{num} Device{{s}}',
NUM_TEMP_SENSORS: '{num} Temperature Sensor{{s}}',
NUM_ANALOG_SENSORS: '{num} Analog Sensor{{s}}',
NUM_DAYS: '{num} Day{{s}}',
NUM_SECONDS: '{num} Second{{s}}',
NUM_HOURS: '{num} Hour{{s}}',
NUM_MINUTES: '{num} Minute{{s}}',
APPLICATION_SETTINGS: 'Application Settings',
CUSTOMIZATION: 'Customization',
APPLICATION_RESTARTING: 'EMS-ESP is restarting',
BOARD_PROFILE_TEXT:
'Select a pre-configured interface board profile from the list below or choose Custom to configure your own hardware settings',
BOARD_PROFILE: 'Board Profile',
BUTTON: 'Button',
TEMPERATURE: 'Temperature',
DISABLED: 'disabled',
GENERAL_OPTIONS: 'General Options',
LANGUAGE_ENTITIES: 'Language (for device entities)',
HIDE_LED: 'Hide LED',
ENABLE_TELNET: 'Enable Telnet Console',
ENABLE_ANALOG: 'Enable Analog Sensors',
CONVERT_FAHRENHEIT: 'Convert temperature values to Fahrenheit',
BYPASS_TOKEN: 'Bypass Access Token authorization on API calls',
READONLY: 'Enable read-only mode (blocks all outgoing EMS Tx Write commands)',
UNDERCLOCK_CPU: 'Underclock CPU speed',
ENABLE_SHOWER_TIMER: 'Enable Shower Timer',
ENABLE_SHOWER_ALERT: 'Enable Shower Alert',
TRIGGER_TIME: 'Trigger Time',
COLD_SHOT_DURATION: 'Cold Shot Duration',
FORMATTING_OPTIONS: 'Formatting Options',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean Format Dashboard',
BOOLEAN_FORMAT_API: 'Boolean Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Enable parasite power',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrams in hexadecimal',
ENABLE_SYSLOG: 'Enable Syslog',
MARK_INTERVAL: 'Mark Interval',
SECONDS: 'seconds',
MINUTES: 'minutes',
RESTART: 'Restart',
HOURS: 'hours',
RESTART_TEXT: 'EMS-ESP needs to be restarted to apply changed system settings',
COMMAND: 'Command',
CUSTOMIZATIONS_RESTART: 'All customizations have been removed. Restarting...',
CUSTOMIZATIONS_FULL: 'Selected entities exceeded limit. Please save in batches',
CUSTOMIZATIONS_SAVED: 'Customizations saved',
CUSTOMIZATIONS_HELP_1: 'Select a device and customize the entities options or click to rename',
CUSTOMIZATIONS_HELP_2: 'mark as favorite',
CUSTOMIZATIONS_HELP_3: 'disable write action',
CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API',
CUSTOMIZATIONS_HELP_5: 'hide from Dashboard',
SELECT_DEVICE: 'Select a device',
SET_ALL: 'set all',
OPTIONS: 'Options',
NAME: 'Name',
CUSTOMIZATIONS_RESET:
'Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors?',
DEVICE_ENTITIES: 'Device Entities',
USER_CUSTOMIZATION: 'User Customization',
SUPPORT_INFORMATION: 'Support Information',
CLICK_HERE: 'Click Here',
HELP_INFORMATION_1: 'Visit the online wiki to get instructions on how to configure EMS-ESP',
HELP_INFORMATION_2: 'For live community chat join our Discord server',
HELP_INFORMATION_3: 'To request a feature or report a bug',
HELP_INFORMATION_4: 'remember to download and attach your system information for a faster response when reporting an issue',
HELP_INFORMATION_5:
"EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!",
SUPPORT_INFO: 'Support Info',
UPLOAD: 'Upload',
DOWNLOAD: 'Download',
ABORTED: 'aborted',
FAILED: 'failed',
SUCCESSFUL: 'successful',
SYSTEM: 'System',
LOG: 'Log',
STATUS: 'Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
SYSTEM_VERSION_RUNNING: 'You are currently running version',
SYSTEM_APPLY_FIRMWARE: 'to apply the new firmware',
CLOSE: 'Close',
USE: 'Use',
FACTORY_RESET: 'Factory Reset',
SYSTEM_FACTORY_TEXT: 'Device has been factory reset and will now restart',
SYSTEM_FACTORY_TEXT_DIALOG: 'Are you sure you want to reset the device to its factory defaults?',
VERSION_CHECK: 'Version Check',
THE_LATEST: 'The latest',
PLATFORM: 'Device (Platform / SDK)',
UPTIME: 'System Uptime',
CPU_FREQ: 'CPU Frequency',
HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size / Speed)',
FILESYSTEM: 'File System (Used / Total)',
BUFFER_SIZE: 'Buffer Size',
COMPACT: 'Compact',
ENABLE_OTA: 'Enable OTA Updates',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download the entity customizations',
DOWNLOAD_SETTINGS_TEXT:
'Download the application settings. Be careful when sharing your settings as this file contains passwords and other sensitive system information',
UPLOAD_TEXT: 'Upload a new firmware (.bin) file, settings or customizations (.json) file below',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Drop file or click here',
ERROR: 'Unexpected Error, please try again',
TIME_SET: 'Time set',
MANAGE_USERS: 'Manage Users',
IS_ADMIN: 'is Admin',
USER_WARNING: 'You must have at least one admin user configured',
ADD: 'Add',
ACCESS_TOKEN_FOR: 'Access Token for',
ACCESS_TOKEN_TEXT:
'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
GENERATING_TOKEN: 'Generating token',
USER: 'User',
MODIFY: 'Modify',
SU_TEXT:
'The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console.',
NOT_ENABLED: 'Not enabled',
ERRORS: 'Errors',
DISCONNECT_REASON: 'Disconnect Reason',
ENABLE_MQTT: 'Enable MQTT',
OPTIONAL: 'Optional',
FORMATTING: 'Formatting',
FORMAT: 'Format',
MQTT_NEST_1: 'Nested in a single topic',
MQTT_NEST_2: 'As individual topics',
MQTT_RESPONSE: 'Publish command output to a `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publish single value topics on change',
MQTT_PUBLISH_TEXT_2: 'Publish to command topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Enable MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix for the Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publish Intervals',
MQTT_INT_BOILER: 'Boilers and Heat Pumps',
MQTT_INT_THERMOSTATS: 'Thermostats',
MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules',
DEFAULT: 'Default',
MQTT_CLEAN_SESSION: 'Set Clean Session',
MQTT_RETAIN_FLAG: 'Always set Retain flag',
INACTIVE: 'Inactive',
ACTIVE: 'Active',
UNKNOWN: 'Unknown',
SET_TIME: 'Set Time',
SET_TIME_TEXT: 'Enter local date and time below to set the time',
LOCAL_TIME: 'Local Time',
UTC_TIME: 'UTC Time',
ENABLE_NTP: 'Enable NTP',
TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'always',
AP_PROVIDE_TEXT_2: 'when WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'never',
AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID',
NETWORK_SCAN: 'Scan WiFi Networks',
IDLE: 'Idle',
LOST: 'Lost',
SCANNING: 'Scanning',
SCAN_AGAIN: 'Scan again',
NETWORK_SCANNER: 'Network Scanner',
NETWORK_NO_WIFI: 'No WiFi networks found',
NETWORK_BLANK_SSID: 'leave blank to disable WiFi',
POWER: 'Power',
NETWORK_DISABLE_SLEEP: 'Disable WiFi Sleep Mode',
NETWORK_LOW_BAND: 'Use Lower WiFi Bandwidth',
NETWORK_USE_DNS: 'Enable mDNS Service',
NETWORK_ENABLE_IPV6: 'Enable IPv6 support',
NETWORK_FIXED_IP: 'Use Fixed IP address',
ADMIN: 'Admin',
GUEST: 'Guest',
NEW: 'New',
RENAME: 'Rename'
};
export default en;

View File

@@ -0,0 +1,265 @@
import type { BaseTranslation } from '../i18n-types';
const en: BaseTranslation = {
LANGUAGE: 'Language',
RETRY: 'Retry',
LOADING: 'Loading',
IS_REQUIRED: 'is required',
SIGN_IN: 'Sign In',
SIGN_OUT: 'Sign Out',
USERNAME: 'Username',
PASSWORD: 'Password',
DASHBOARD: 'Dashboard',
SETTINGS: 'Settings',
SAVED: 'saved',
HELP: 'Help',
LOGGED_IN: 'Logged in as {name}',
PLEASE_SIGNIN: 'Please sign in to continue',
UPLOAD_SUCCESSFUL: 'Upload successful',
DOWNLOAD_SUCCESSFUL: 'Download successful',
INVALID_LOGIN: 'Invalid login details',
NETWORK: 'Network',
SECURITY: 'Security',
ONOFF_CAP: 'ON/OFF',
ONOFF: 'on/off',
TYPE: 'Type',
DESCRIPTION: 'Description',
ENTITIES: 'Entities',
REFRESH: 'Refresh',
EXPORT: 'Export',
DEVICE_DETAILS: 'Device Details',
BRAND: 'Brand',
ENTITY_NAME: 'Entity Name',
VALUE: 'Value',
SHOW_FAV: 'only show favorites',
DEVICE_SENSOR_DATA: 'Device and Sensor Data',
DEVICES_SENSORS: 'Devices & Sensors',
ATTACHED_SENSORS: 'Attached EMS-ESP Sensors',
RUN_COMMAND: 'Call Command',
CHANGE_VALUE: 'Change Value',
CANCEL: 'Cancel',
RESET: 'Reset',
SEND: 'Send',
SAVE: 'Save',
REMOVE: 'Remove',
PROBLEM_UPDATING: 'Problem updating',
PROBLEM_LOADING: 'Problem loading',
ACCESS_DENIED: 'Access Denied',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analog Sensors',
UPDATED: 'Updated',
UPDATE: 'Update',
REMOVED: 'Removed',
DELETION: 'Deletion',
OFFSET: 'Offset',
FACTOR: 'Factor',
FREQ: 'Frequency',
STARTVALUE: 'Start value',
WARN_GPIO: 'Warning: be careful when assigning a GPIO!',
EDIT: 'Edit',
TEMP_SENSOR: 'Temperature Sensor',
TEMP_SENSORS: 'Temperature Sensors',
WRITE_COMMAND: 'Write command {cmd}',
EMS_BUS_WARNING:
'EMS bus disconnected. If this warning still persists after a few seconds please check settings and board profile',
EMS_BUS_SCANNING: 'Scanning for EMS devices...',
CONNECTED: 'Connected',
TX_ISSUES: 'Tx issues - try a different Tx Mode',
DISCONNECTED: 'Disconnected',
EMS_SCAN: 'Are you sure you want to initiate a full device scan of the EMS bus?',
EMS_BUS_STATUS: 'EMS Bus Status',
ACTIVE_DEVICES: 'Active Devices & Sensors',
DEVICE: 'Device',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
QUALITY: 'QUALITY',
SCAN_DEVICES: 'Scan for new devices',
EMS_BUS_STATUS_TITLE: 'EMS Bus & Activity Status',
SCAN: 'Scan',
STATUS_NAMES: [
'EMS Telegrams Received (Rx)',
'EMS Reads (Tx)',
'EMS Writes (Tx)',
'Temperature Sensor Reads',
'Analog Sensor Reads',
'MQTT Publishes',
'API Calls',
'Syslog Messages'
],
NUM_DEVICES: '{num} Device{{s}}',
NUM_TEMP_SENSORS: '{num} Temperature Sensor{{s}}',
NUM_ANALOG_SENSORS: '{num} Analog Sensor{{s}}',
NUM_DAYS: '{num} Day{{s}}',
NUM_SECONDS: '{num} Second{{s}}',
NUM_HOURS: '{num} Hour{{s}}',
NUM_MINUTES: '{num} Minute{{s}}',
APPLICATION_SETTINGS: 'Application Settings',
CUSTOMIZATION: 'Customization',
APPLICATION_RESTARTING: 'EMS-ESP is restarting',
BOARD_PROFILE_TEXT:
'Select a pre-configured interface board profile from the list below or choose Custom to configure your own hardware settings',
BOARD_PROFILE: 'Board Profile',
BUTTON: 'Button',
TEMPERATURE: 'Temperature',
DISABLED: 'disabled',
GENERAL_OPTIONS: 'General Options',
LANGUAGE_ENTITIES: 'Language (for device entities)',
HIDE_LED: 'Hide LED',
ENABLE_TELNET: 'Enable Telnet Console',
ENABLE_ANALOG: 'Enable Analog Sensors',
CONVERT_FAHRENHEIT: 'Convert temperature values to Fahrenheit',
BYPASS_TOKEN: 'Bypass Access Token authorization on API calls',
READONLY: 'Enable read-only mode (blocks all outgoing EMS Tx Write commands)',
UNDERCLOCK_CPU: 'Underclock CPU speed',
ENABLE_SHOWER_TIMER: 'Enable Shower Timer',
ENABLE_SHOWER_ALERT: 'Enable Shower Alert',
TRIGGER_TIME: 'Trigger Time',
COLD_SHOT_DURATION: 'Cold Shot Duration',
FORMATTING_OPTIONS: 'Formatting Options',
BOOLEAN_FORMAT_DASHBOARD: 'Boolean Format Dashboard',
BOOLEAN_FORMAT_API: 'Boolean Format API/MQTT',
ENUM_FORMAT: 'Enum Format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Enable parasite power',
LOGGING: 'Logging',
LOG_HEX: 'Log EMS telegrams in hexadecimal',
ENABLE_SYSLOG: 'Enable Syslog',
MARK_INTERVAL: 'Mark Interval',
SECONDS: 'seconds',
MINUTES: 'minutes',
RESTART: 'Restart',
HOURS: 'hours',
RESTART_TEXT: 'EMS-ESP needs to be restarted to apply changed system settings',
COMMAND: 'Command',
CUSTOMIZATIONS_RESTART: 'All customizations have been removed. Restarting...',
CUSTOMIZATIONS_FULL: 'Selected entities exceeded limit. Please save in batches',
CUSTOMIZATIONS_SAVED: 'Customizations saved',
CUSTOMIZATIONS_HELP_1: 'Select a device and customize the entities options or click to rename',
CUSTOMIZATIONS_HELP_2: 'mark as favorite',
CUSTOMIZATIONS_HELP_3: 'disable write action',
CUSTOMIZATIONS_HELP_4: 'exclude from MQTT and API',
CUSTOMIZATIONS_HELP_5: 'hide from Dashboard',
SELECT_DEVICE: 'Select a device',
SET_ALL: 'set all',
OPTIONS: 'Options',
NAME: 'Name',
CUSTOMIZATIONS_RESET:
'Are you sure you want remove all customizations including the custom settings of the Temperature and Analog sensors?',
DEVICE_ENTITIES: 'Device Entities',
USER_CUSTOMIZATION: 'User Customization',
SUPPORT_INFORMATION: 'Support Information',
CLICK_HERE: 'Click Here',
HELP_INFORMATION_1: 'Visit the online wiki to get instructions on how to configure EMS-ESP',
HELP_INFORMATION_2: 'For live community chat join our Discord server',
HELP_INFORMATION_3: 'To request a feature or report a bug',
HELP_INFORMATION_4: 'remember to download and attach your system information for a faster response when reporting an issue',
HELP_INFORMATION_5:
"EMS-ESP is a free and open-source project. Please support its future development by giving it a star on Github!",
SUPPORT_INFO: 'Support Info',
UPLOAD: 'Upload',
DOWNLOAD: 'Download',
ABORTED: 'aborted',
FAILED: 'failed',
SUCCESSFUL: 'successful',
SYSTEM: 'System',
LOG: 'Log',
STATUS: 'Status',
UPLOAD_DOWNLOAD: 'Upload/Download',
SYSTEM_VERSION_RUNNING: 'You are currently running version',
SYSTEM_APPLY_FIRMWARE: 'to apply the new firmware',
CLOSE: 'Close',
USE: 'Use',
FACTORY_RESET: 'Factory Reset',
SYSTEM_FACTORY_TEXT: 'Device has been factory reset and will now restart',
SYSTEM_FACTORY_TEXT_DIALOG: 'Are you sure you want to reset the device to its factory defaults?',
VERSION_CHECK: 'Version Check',
THE_LATEST: 'The latest',
PLATFORM: 'Device (Platform / SDK)',
UPTIME: 'System Uptime',
CPU_FREQ: 'CPU Frequency',
HEAP: 'Heap (Free / Max Alloc)',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size / Speed)',
FILESYSTEM: 'File System (Used / Total)',
BUFFER_SIZE: 'Buffer Size',
COMPACT: 'Compact',
ENABLE_OTA: 'Enable OTA Updates',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Download the entity customizations',
DOWNLOAD_SETTINGS_TEXT:
'Download the application settings. Be careful when sharing your settings as this file contains passwords and other sensitive system information',
UPLOAD_TEXT: 'Upload a new firmware (.bin) file, settings or customizations (.json) file below',
UPLOADING: 'Uploading',
UPLOAD_DROP_TEXT: 'Drop file or click here',
ERROR: 'Unexpected Error, please try again',
TIME_SET: 'Time set',
MANAGE_USERS: 'Manage Users',
IS_ADMIN: 'is Admin',
USER_WARNING: 'You must have at least one admin user configured',
ADD: 'Add',
ACCESS_TOKEN_FOR: 'Access Token for',
ACCESS_TOKEN_TEXT:
'The token below is used with REST API calls that require authorization. It can be passed either as a Bearer token in the Authorization header or in the access_token URL query parameter.',
GENERATING_TOKEN: 'Generating token',
USER: 'User',
MODIFY: 'Modify',
SU_TEXT:
'The su (super user) password is used to sign authentication tokens and also enable admin privileges within the Console.',
NOT_ENABLED: 'Not enabled',
ERRORS: 'Errors',
DISCONNECT_REASON: 'Disconnect Reason',
ENABLE_MQTT: 'Enable MQTT',
OPTIONAL: 'Optional',
FORMATTING: 'Formatting',
FORMAT: 'Format',
MQTT_NEST_1: 'Nested in a single topic',
MQTT_NEST_2: 'As individual topics',
MQTT_RESPONSE: 'Publish command output to a `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publish single value topics on change',
MQTT_PUBLISH_TEXT_2: 'Publish to command topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Enable MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix for the Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publish Intervals',
MQTT_INT_BOILER: 'Boilers and Heat Pumps',
MQTT_INT_THERMOSTATS: 'Thermostats',
MQTT_INT_SOLAR: 'Solar Modules',
MQTT_INT_MIXER: 'Mixer Modules',
DEFAULT: 'Default',
MQTT_CLEAN_SESSION: 'Set Clean Session',
MQTT_RETAIN_FLAG: 'Always set Retain flag',
INACTIVE: 'Inactive',
ACTIVE: 'Active',
UNKNOWN: 'Unknown',
SET_TIME: 'Set Time',
SET_TIME_TEXT: 'Enter local date and time below to set the time',
LOCAL_TIME: 'Local Time',
UTC_TIME: 'UTC Time',
ENABLE_NTP: 'Enable NTP',
TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'always',
AP_PROVIDE_TEXT_2: 'when WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'never',
AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID',
NETWORK_SCAN: 'Scan WiFi Networks',
IDLE: 'Idle',
LOST: 'Lost',
SCANNING: 'Scanning',
SCAN_AGAIN: 'Scan again',
NETWORK_SCANNER: 'Network Scanner',
NETWORK_NO_WIFI: 'No WiFi networks found',
NETWORK_BLANK_SSID: 'leave blank to disable WiFi',
POWER: 'Power',
NETWORK_DISABLE_SLEEP: 'Disable WiFi Sleep Mode',
NETWORK_LOW_BAND: 'Use Lower WiFi Bandwidth',
NETWORK_USE_DNS: 'Enable mDNS Service',
NETWORK_ENABLE_IPV6: 'Enable IPv6 support',
NETWORK_FIXED_IP: 'Use Fixed IP address',
ADMIN: 'Admin',
GUEST: 'Guest',
NEW: 'New',
RENAME: 'Rename'
};
export default en;

View File

@@ -0,0 +1,266 @@
import type { BaseTranslation } from '../i18n-types';
const se: BaseTranslation = {
LANGUAGE: 'Språk',
RETRY: 'Försök igen',
LOADING: 'Laddar',
IS_REQUIRED: 'Krävs',
SIGN_IN: 'Logga In',
SIGN_OUT: 'Logga Ut',
USERNAME: 'Användarnamn',
PASSWORD: 'Lösenord',
DASHBOARD: 'Kontrollpanel',
SETTINGS: 'Inställningar',
SAVED: 'Sparat',
HELP: 'Hjälp',
LOGGED_IN: 'Inloggad som {name}',
PLEASE_SIGNIN: 'Vänligen logga in för att fortsätta',
UPLOAD_SUCCESSFUL: 'Uppladdning lyckades',
DOWNLOAD_SUCCESSFUL: 'Nedladdning lyckades',
INVALID_LOGIN: 'Ogiltig login',
NETWORK: 'Nätverk',
SECURITY: 'Säkerhet',
ONOFF_CAP: 'PÅ/AV',
ONOFF: 'på/av',
TYPE: 'Typ',
DESCRIPTION: 'Beskrivning',
ENTITIES: 'Entiteter',
REFRESH: 'Uppdatera',
EXPORT: 'Export',
DEVICE_DETAILS: 'Enhetsdetaljer',
BRAND: 'Fabrikat',
ENTITY_NAME: 'Entitetsnamn',
VALUE: 'Värde',
SHOW_FAV: 'Visa enbart favoriter',
DEVICE_SENSOR_DATA: 'Enhets och Sensor-data',
DEVICES_SENSORS: 'Enheter & Sensorer',
ATTACHED_SENSORS: 'Anslutna EMS-ESP Sensorer',
RUN_COMMAND: 'Kör Kommando',
CHANGE_VALUE: 'Ändra Värde',
CANCEL: 'Avbryt',
RESET: 'Nollsäll',
SEND: 'Skicka',
SAVE: 'Spara',
REMOVE: 'Ta bort',
PROBLEM_UPDATING: 'Problem vid uppdatering',
PROBLEM_LOADING: 'Problem vid hämtning',
ACCESS_DENIED: 'Åtkomst Nekad',
ANALOG_SENSOR: 'Analog Sensor',
ANALOG_SENSORS: 'Analoga Sensorer',
UPDATED: 'Uppdaterad',
UPDATE: 'Uppdatera',
REMOVED: 'Raderad',
DELETION: 'Radering',
OFFSET: 'Kompensering',
FACTOR: 'Faktor',
FREQ: 'Frekvens',
STARTVALUE: 'Startvärde',
WARN_GPIO: 'Varning: Var försiktig vid aktivering av GPIO!',
EDIT: 'Ändra',
TEMP_SENSOR: 'Temperatursensor',
TEMP_SENSORS: 'Temperatursensorer',
WRITE_COMMAND: 'Skrivkommando {cmd}',
EMS_BUS_WARNING:
'EMS-buss nedkopplad. Om denna varning kvarstår efter några sekunder, kontrollera inställningar och enhets-profil.',
EMS_BUS_SCANNING: 'Söker efter EMS-enheter...',
CONNECTED: 'Ansluten',
TX_ISSUES: 'Sändfel - Prova ett annat TX-läge',
DISCONNECTED: 'Nedkopplad',
EMS_SCAN: 'Är du säker att du vill initiera en full genomsökning av EMS-bussen?',
EMS_BUS_STATUS: 'EMS-Bus Status',
ACTIVE_DEVICES: 'Aktiva Enheter & Sensorer',
DEVICE: 'Enhet',
SUCCESS: 'Lyckades',
FAIL: 'Misslyckades',
QUALITY: 'Kvalitet',
SCAN_DEVICES: 'Sök efter nya enheter',
EMS_BUS_STATUS_TITLE: 'EMS-buss & aktivitetsstatus',
SCAN: 'Sök',
STATUS_NAMES: [
'EMS-telegram Mottagna (Rx)',
'EMS-läsningar (Tx)',
'EMS-skrivningar (Tx)',
'Temperatursensor-läsningar',
'Analog Sensor-läsningar',
'MQTT-publiceringar',
'API-anrop',
'Syslog-meddelanden'
],
NUM_DEVICES: '{num} Enhet{{er}}',
NUM_TEMP_SENSORS: '{num} Temperatur-sensor{{er}}',
NUM_ANALOG_SENSORS: '{num} Analoga Sensor{{er}}',
NUM_DAYS: '{num} Dag{{ar}}',
NUM_SECONDS: '{num} Sekund{{er}}',
NUM_HOURS: '{num} Timmar',
NUM_MINUTES: '{num} Minut{{er}}',
APPLICATION_SETTINGS: 'Inställningar',
CUSTOMIZATION: 'Anpassa',
APPLICATION_RESTARTING: 'EMS-ESP startar om',
BOARD_PROFILE_TEXT:
'Välj en förkonfigurerad hårdvaruprofil från listan nedan eller välj Anpassad för att konfigurera dina egna hårdvaruinställningar',
BOARD_PROFILE: 'Hårdvaruprofil',
BUTTON: 'Knapp',
TEMPERATURE: 'Temperatur',
DISABLED: 'inaktiverad',
GENERAL_OPTIONS: 'Allmänna Inställningar',
LANGUAGE_ENTITIES: 'Språk (för entiteter)',
HIDE_LED: 'Inaktivera LED',
ENABLE_TELNET: 'Aktivera Telnet',
ENABLE_ANALOG: 'Aktivera Analoga Sensorer',
CONVERT_FAHRENHEIT: 'Konvertera temperatur till Fahrenheit',
BYPASS_TOKEN: 'Inaktivera Token-autensiering för API-anrop',
READONLY: 'Aktivera read-only (blockerar alla utgående skrivkommandon mot EMS-bussen)',
UNDERCLOCK_CPU: 'Nedklocka Processorhastighet',
ENABLE_SHOWER_TIMER: 'Aktivera Dusch-timer',
ENABLE_SHOWER_ALERT: 'Aktivera Dusch-warning',
TRIGGER_TIME: 'Aktiveringstid',
COLD_SHOT_DURATION: 'Längd på kalldusch',
FORMATTING_OPTIONS: 'Formatteringsalternativ',
BOOLEAN_FORMAT_DASHBOARD: 'Bool-format Kontrollpanel',
BOOLEAN_FORMAT_API: 'Bool-format API/MQTT',
ENUM_FORMAT: 'Enum-format API/MQTT',
INDEX: 'Index',
ENABLE_PARASITE: 'Aktivera parasitström',
LOGGING: 'Loggning',
LOG_HEX: 'Logga EMS-telegram i hexadecimal',
ENABLE_SYSLOG: 'Aktivera Syslog',
MARK_INTERVAL: 'Markerings-interval',
SECONDS: 'sekunder',
MINUTES: 'minuter',
RESTART: 'Starta om',
HOURS: 'timmar',
RESTART_TEXT: 'EMS-ESP kräver en omstart för att applicera förändrade systeminställningar',
COMMAND: 'Kommando',
CUSTOMIZATIONS_RESTART: 'Alla anpassningr har raderats. Startar om...',
CUSTOMIZATIONS_FULL: 'Antal valda enheter för högt. Vänligen spara i mindre antal åt gången.',
CUSTOMIZATIONS_SAVED: 'Anpassningar sparade',
CUSTOMIZATIONS_HELP_1: 'Välj en enhet och anpassa underenheter med hjälp av alternativen',
CUSTOMIZATIONS_HELP_2: 'markera som favorit',
CUSTOMIZATIONS_HELP_3: 'inaktivera skrivningar',
CUSTOMIZATIONS_HELP_4: 'exkludera från MQTT & API',
CUSTOMIZATIONS_HELP_5: 'göm från Kontrollpanel',
SELECT_DEVICE: 'Välj en enhet',
SET_ALL: 'ställ in alla',
OPTIONS: 'Alternativ',
NAME: 'Namn',
CUSTOMIZATIONS_RESET:
'Är du säker på att du vill ta bort alla anpassningar inklusive inställningar för Temperatur och Analoga sensorer?',
DEVICE_ENTITIES: 'Enhets-entiteter',
USER_CUSTOMIZATION: 'Användaranpassningar',
SUPPORT_INFORMATION: 'Supportinformation',
CLICK_HERE: 'Klicka Här',
HELP_INFORMATION_1: 'Besök Wikin för instruktioner för hur du kan konfigurera EMS-ESP',
HELP_INFORMATION_2: 'För community-support besök vår Discord-server',
HELP_INFORMATION_3: 'Önska en ny funktion eller rapportera en bugg',
HELP_INFORMATION_4:
'Bifoga din systeminformation för snabbare hantering när du rapporterar ett problem',
HELP_INFORMATION_5:
'EMS-ESP är gratis och är öppen källkod. Bidra till utveklingen genom att ge oss en stjärna på GitHub!',
SUPPORT_INFO: 'Supportinfo',
UPLOAD: 'Uppladdning',
DOWNLOAD: 'Nedladdning',
ABORTED: 'Avbruten',
FAILED: 'Misslyckades',
SUCCESSFUL: 'Lyckades',
SYSTEM: 'System',
LOG: 'Logg',
STATUS: 'Status',
UPLOAD_DOWNLOAD: 'Upp/Nedladdning',
SYSTEM_VERSION_RUNNING: 'Du använder version',
SYSTEM_APPLY_FIRMWARE: 'för att aktivera ny firmware',
CLOSE: 'Stäng',
USE: 'Använd',
FACTORY_RESET: 'Fabriksåterställning',
SYSTEM_FACTORY_TEXT: 'Enheten har blivit fabriksåterställd och startar nu om',
SYSTEM_FACTORY_TEXT_DIALOG: 'Är du säker att du vill fabriksåterställa enheten?',
VERSION_CHECK: 'Versionskontroll',
THE_LATEST: 'Den senaste',
PLATFORM: 'Enhet (Plattform / SDK)',
UPTIME: 'Systemets Uptid',
CPU_FREQ: 'CPU-frekvens',
HEAP: 'Heap (Ledigt / Max allokerat)',
PSRAM: 'PSRAM (Storlek / Ledigt)',
FLASH: 'Flash Chip (Storlek / Hastighet)',
FILESYSTEM: 'Filsystem (Använt / Totalt)',
BUFFER_SIZE: 'Bufferstorlek',
COMPACT: 'Komprimera',
ENABLE_OTA: 'Aktivera OTA-uppdateringar',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Ladda ner entitetsanpassningar',
DOWNLOAD_SETTINGS_TEXT:
'Ladda ner applikationsinställningar. Var försiktig om du delar dina iställlningar då de innehåller lösenord och annan känslig systeminformation',
UPLOAD_TEXT: 'Ladda upp ett nytt firmware (.bin), inställningar eller anpassningar (.json) nedan',
UPLOADING: 'Laddar upp',
UPLOAD_DROP_TEXT: 'Släpp fil eller klicka här',
ERROR: 'Okänt Fel, var god försök igen',
TIME_SET: 'Ställ in tid',
MANAGE_USERS: 'Användarhantering',
IS_ADMIN: 'Admin',
USER_WARNING: 'Du måste ha minst en admin konfigurerad',
ADD: 'Lägg till',
ACCESS_TOKEN_FOR: 'Access Token för',
ACCESS_TOKEN_TEXT:
'Nedan Token används med REST API-anrop som kräver auktorisering. Den kan skickas med antingen som en Bearer token i Authorization-headern eller i access_token URL query-parametern.',
GENERATING_TOKEN: 'Genererar token',
USER: 'Användare',
MODIFY: 'Ändra',
SU_TEXT:
'SU-användarens (super user) lösenord används för att signera autensierings-tokens samt för att aktivera administratörsprivilegier i Console-läge',
NOT_ENABLED: 'Ej aktiv',
ERRORS: 'fel',
DISCONNECT_REASON: 'Anledning till nedkoppling',
ENABLE_MQTT: 'Aktivera MQTT',
OPTIONAL: 'Valfritt',
FORMATTING: 'Formatering',
FORMAT: 'Format',
MQTT_NEST_1: 'Nestlad i en topic.',
MQTT_NEST_2: 'Som individuella topics',
MQTT_RESPONSE: 'Publish-kommando som ett `response` topic',
MQTT_PUBLISH_TEXT_1: 'Publicera single value topics vid värdeförändring',
MQTT_PUBLISH_TEXT_2: 'Publicera till kommando-topics (ioBroker)',
MQTT_PUBLISH_TEXT_3: 'Aktivera MQTT Discovery (Home Assistant, Domoticz)',
MQTT_PUBLISH_TEXT_4: 'Prefix för Discovery topics',
MQTT_PUBLISH_INTERVALS: 'Publiceringsintervall',
MQTT_INT_BOILER: 'Värmepump/panna',
MQTT_INT_THERMOSTATS: 'Termostater',
MQTT_INT_SOLAR: 'Solpaneler',
MQTT_INT_MIXER: 'Blandarventiler',
DEFAULT: 'Standard',
MQTT_CLEAN_SESSION: 'Sätt "Clean Session"-flaggan',
MQTT_RETAIN_FLAG: 'Sätt "Always Retain"-flaggan',
INACTIVE: 'Inaktiv',
ACTIVE: 'Aktiv',
UNKNOWN: 'Okänt',
SET_TIME: 'Ställ in klockan',
SET_TIME_TEXT: 'Ange lokal datum och tid nedan för att ställa in klockan',
LOCAL_TIME: 'Lokal Tid',
UTC_TIME: 'UTC-tid',
ENABLE_NTP: 'Aktivera NTP',
TIME_ZONE: 'Tidszon',
ACCESS_POINT: 'Accesspunkt',
AP_PROVIDE: 'Aktivera Accesspunkt',
AP_PROVIDE_TEXT_1: 'alltid',
AP_PROVIDE_TEXT_2: 'när WiFi är nedkopplat',
AP_PROVIDE_TEXT_3: 'aldrig',
AP_PREFERRED_CHANNEL: 'Föredragen kanal',
AP_HIDE_SSID: 'Göm SSID',
NETWORK_SCAN: 'Sök efter WiFi-nätverk',
IDLE: 'Vilande',
LOST: 'Förlorad',
SCANNING: 'Söker',
SCAN_AGAIN: 'Sök igen',
NETWORK_SCANNER: 'Nätverks-scanner',
NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades',
NETWORK_BLANK_SSID: 'lämna blankt för att inaktivera WiFi',
POWER: 'Effekt',
NETWORK_DISABLE_SLEEP: 'Inaktivera sömnläge',
NETWORK_LOW_BAND: 'Använd lägre bandbredd',
NETWORK_USE_DNS: 'Aktivera mDNS-tjänsten',
NETWORK_ENABLE_IPV6: 'Aktivera IPv6-support',
NETWORK_FIXED_IP: 'Använd statiskt IP',
ADMIN: 'Admin',
GUEST: 'Gäst',
NEW: 'Ny',
RENAME: 'Byt namn'
};
export default se;

View File

@@ -5,17 +5,21 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import DashboardStatus from './DashboardStatus';
import DashboardData from './DashboardData';
const Dashboard: FC = () => {
useLayoutTitle('Dashboard');
const { routerTab } = useRouterTab();
const { LL } = useI18nContext();
useLayoutTitle(LL.DASHBOARD());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="data" label="Devices &amp; Sensors" />
<Tab value="data" label={LL.DEVICES_SENSORS()} />
<Tab value="status" label="Status" />
</RouterTabs>
<Routes>

View File

@@ -49,8 +49,6 @@ import DeviceIcon from './DeviceIcon';
import { IconContext } from 'react-icons';
import { formatDurationMin, pluralize } from '../utils';
import { AuthenticatedContext } from '../contexts/authentication';
import { ButtonRow, ValidatedTextField, SectionContent, MessageBox } from '../components';
@@ -74,12 +72,24 @@ import {
DeviceEntityMask
} from './types';
import { useI18nContext } from '../i18n/i18n-react';
import parseMilliseconds from 'parse-ms';
const DashboardData: FC = () => {
const { me } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [coreData, setCoreData] = useState<CoreData>({ connected: true, devices: [], active_sensors: 0, analog_enabled: false });
const [coreData, setCoreData] = useState<CoreData>({
connected: true,
devices: [],
active_sensors: 0,
analog_enabled: false
});
const [deviceData, setDeviceData] = useState<DeviceData>({ label: '', data: [] });
const [sensorData, setSensorData] = useState<SensorData>({ sensors: [], analogs: [] });
const [deviceValue, setDeviceValue] = useState<DeviceValue>();
@@ -134,7 +144,7 @@ const DashboardData: FC = () => {
common_theme,
{
Table: `
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 80px 40px;
--data-table-library_grid-template-columns: 40px 100px repeat(1, minmax(0, 1fr)) 100px 40px;
`,
BaseRow: `
.td {
@@ -319,10 +329,10 @@ const DashboardData: FC = () => {
const handleDownloadCsv = () => {
const columns = [
{ accessor: (dv: any) => dv.id.slice(2), name: 'Entity' },
{ accessor: (dv: any) => dv.id.slice(2), name: LL.ENTITY_NAME() },
{
accessor: (dv: any) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v),
name: 'Value'
name: LL.VALUE()
},
{ accessor: (dv: any) => DeviceValueUOM_s[dv.u], name: 'UoM' }
];
@@ -355,9 +365,9 @@ const DashboardData: FC = () => {
try {
setCoreData((await EMSESP.readCoreData()).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Failed to fetch core data'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
}, [enqueueSnackbar]);
}, [enqueueSnackbar, LL]);
useEffect(() => {
fetchCoreData();
@@ -376,7 +386,7 @@ const DashboardData: FC = () => {
try {
setDeviceData((await EMSESP.readDeviceData({ id: unique_id })).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching device data'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
@@ -384,21 +394,38 @@ const DashboardData: FC = () => {
try {
setSensorData((await EMSESP.readSensorData()).data);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching sensor data'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
const isCmdOnly = (dv: DeviceValue) => dv.v === '' && dv.c;
const formatDurationMin = (duration_min: number) => {
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
let formatted = '';
if (days) {
formatted += LL.NUM_DAYS({ num: days }) + ' ';
}
if (hours) {
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
}
if (minutes) {
formatted += LL.NUM_MINUTES({ num: minutes });
}
return formatted;
};
function formatValue(value: any, uom: number) {
if (value === undefined) {
return '';
}
switch (uom) {
case DeviceValueUOM.HOURS:
return value ? formatDurationMin(value * 60) : '0 hours';
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
case DeviceValueUOM.MINUTES:
return value ? formatDurationMin(value) : '0 minutes';
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
case DeviceValueUOM.SECONDS:
return LL.NUM_SECONDS({ num: value });
case DeviceValueUOM.NONE:
if (typeof value === 'number') {
return new Intl.NumberFormat().format(value);
@@ -414,13 +441,24 @@ const DashboardData: FC = () => {
' ' +
DeviceValueUOM_s[uom]
);
case DeviceValueUOM.SECONDS:
return pluralize(value, DeviceValueUOM_s[uom]);
default:
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
}
}
const setUom = (uom: number) => {
switch (uom) {
case DeviceValueUOM.HOURS:
return LL.HOURS();
case DeviceValueUOM.MINUTES:
return LL.MINUTES();
case DeviceValueUOM.SECONDS:
return LL.SECONDS();
default:
return DeviceValueUOM_s[uom];
}
};
const sendDeviceValue = async () => {
if (deviceValue) {
try {
@@ -429,15 +467,15 @@ const DashboardData: FC = () => {
devicevalue: deviceValue
});
if (response.status === 204) {
enqueueSnackbar('Write command failed', { variant: 'error' });
enqueueSnackbar(LL.WRITE_COMMAND({ cmd: 'failed' }), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Write access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Write command sent', { variant: 'success' });
enqueueSnackbar(LL.WRITE_COMMAND({ cmd: 'send' }), { variant: 'success' });
}
setDeviceValue(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem writing value'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
refreshData();
setDeviceValue(undefined);
@@ -449,7 +487,7 @@ const DashboardData: FC = () => {
if (deviceValue) {
return (
<Dialog open={deviceValue !== undefined} onClose={() => setDeviceValue(undefined)}>
<DialogTitle>{isCmdOnly(deviceValue) ? 'Run Command' : 'Change Value'}</DialogTitle>
<DialogTitle>{isCmdOnly(deviceValue) ? LL.RUN_COMMAND() : LL.CHANGE_VALUE()}</DialogTitle>
<DialogContent dividers>
{deviceValue.l && (
<ValidatedTextField
@@ -478,7 +516,7 @@ const DashboardData: FC = () => {
onChange={updateValue(setDeviceValue)}
inputProps={deviceValue.u ? { min: deviceValue.m, max: deviceValue.x, step: deviceValue.s } : {}}
InputProps={{
startAdornment: <InputAdornment position="start">{DeviceValueUOM_s[deviceValue.u]}</InputAdornment>
startAdornment: <InputAdornment position="start">{setUom(deviceValue.u)}</InputAdornment>
}}
/>
)}
@@ -491,7 +529,7 @@ const DashboardData: FC = () => {
onClick={() => setDeviceValue(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SendIcon />}
@@ -500,7 +538,7 @@ const DashboardData: FC = () => {
onClick={() => sendDeviceValue()}
color="warning"
>
Send
{LL.SEND()}
</Button>
</DialogActions>
</Dialog>
@@ -521,15 +559,15 @@ const DashboardData: FC = () => {
offset: sensor.o
});
if (response.status === 204) {
enqueueSnackbar('Sensor change failed', { variant: 'error' });
enqueueSnackbar(LL.TEMP_SENSOR() + ' ' + LL.UPLOAD_TEXT() + ' ' + LL.FAILED(), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Sensor updated', { variant: 'success' });
enqueueSnackbar(LL.TEMP_SENSOR() + ' ' + LL.UPDATED(), { variant: 'success' });
}
setSensor(undefined);
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating sensor'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setSensor(undefined);
fetchSensorData();
@@ -541,16 +579,18 @@ const DashboardData: FC = () => {
if (sensor) {
return (
<Dialog open={sensor !== undefined} onClose={() => setSensor(undefined)}>
<DialogTitle>Edit Temperature Sensor</DialogTitle>
<DialogTitle>
{LL.EDIT()} {LL.TEMP_SENSOR()}
</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">Sensor ID {sensor.id}</Typography>
<Typography variant="body2">Sensor ID: {sensor.id}</Typography>
</Box>
<Grid container spacing={1}>
<Grid item>
<ValidatedTextField
name="n"
label="Name"
label={LL.ENTITY_NAME()}
value={sensor.n}
autoFocus
sx={{ width: '30ch' }}
@@ -560,7 +600,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Offset"
label={LL.OFFSET()}
value={numberValue(sensor.o)}
sx={{ width: '12ch' }}
type="number"
@@ -581,7 +621,7 @@ const DashboardData: FC = () => {
onClick={() => setSensor(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -590,7 +630,7 @@ const DashboardData: FC = () => {
onClick={() => sendSensor()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -602,17 +642,17 @@ const DashboardData: FC = () => {
if (coreData && coreData.devices.length > 0 && deviceDialog !== -1) {
return (
<Dialog open={deviceDialog !== -1} onClose={() => setDeviceDialog(-1)}>
<DialogTitle>Device Details</DialogTitle>
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
<DialogContent dividers>
<List dense={true}>
<ListItem>
<ListItemText primary="Type" secondary={coreData.devices[deviceDialog].t} />
<ListItemText primary={LL.TYPE()} secondary={coreData.devices[deviceDialog].t} />
</ListItem>
<ListItem>
<ListItemText primary="Name" secondary={coreData.devices[deviceDialog].n} />
<ListItemText primary={LL.NAME()} secondary={coreData.devices[deviceDialog].n} />
</ListItem>
<ListItem>
<ListItemText primary="Brand" secondary={coreData.devices[deviceDialog].b} />
<ListItemText primary={LL.BRAND()} secondary={coreData.devices[deviceDialog].b} />
</ListItem>
<ListItem>
<ListItemText
@@ -630,7 +670,7 @@ const DashboardData: FC = () => {
</DialogContent>
<DialogActions>
<Button variant="outlined" onClick={() => setDeviceDialog(-1)} color="secondary">
Close
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
@@ -640,17 +680,20 @@ const DashboardData: FC = () => {
const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '24', style: { verticalAlign: 'middle' } }}>
{!coreData.connected && <MessageBox my={2} level="error" message="EMSbus disconnected, check settings and board profile" />}
{coreData.connected && coreData.devices.length === 0 && <MessageBox my={2} level="warning" message="Scanning for EMS devices..." />}
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />}
{coreData.connected && coreData.devices.length === 0 && (
<MessageBox my={2} level="warning" message={LL.EMS_BUS_SCANNING()} />
)}
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff />
<HeaderCell stiff>TYPE</HeaderCell>
<HeaderCell resize>DESCRIPTION</HeaderCell>
<HeaderCell stiff>ENTITIES</HeaderCell>
<HeaderCell stiff>{LL.TYPE()}</HeaderCell>
<HeaderCell resize>{LL.DESCRIPTION()}</HeaderCell>
<HeaderCell stiff>{LL.ENTITIES()}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -676,7 +719,7 @@ const DashboardData: FC = () => {
<DeviceIcon type="Sensor" />
</Cell>
<Cell>Sensors</Cell>
<Cell>Attached EMS-ESP Sensors</Cell>
<Cell>{LL.ATTACHED_SENSORS()}</Cell>
<Cell>{coreData.active_sensors}</Cell>
<Cell>
<IconButton size="small" onClick={() => addAnalogSensor()}>
@@ -723,7 +766,7 @@ const DashboardData: FC = () => {
control={<Checkbox size="small" name="onlyFav" checked={onlyFav} onChange={() => setOnlyFav(!onlyFav)} />}
label={
<span style={{ fontSize: '12px' }}>
only show favorites&nbsp;
{LL.SHOW_FAV()}&nbsp;
<StarIcon color="primary" sx={{ fontSize: 12 }} />
</span>
}
@@ -749,7 +792,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'NAME')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
ENTITY NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell resize>
@@ -759,7 +802,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(dv_sort.state, 'VALUE')}
onClick={() => dv_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
>
VALUE
{LL.VALUE()}
</Button>
</HeaderCell>
<HeaderCell stiff />
@@ -806,7 +849,7 @@ const DashboardData: FC = () => {
const renderDallasData = () => (
<>
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
Temperature Sensors
{LL.TEMP_SENSORS()}
</Typography>
<Table
data={{ nodes: sensorData.sensors }}
@@ -825,7 +868,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(sensor_sort.state, 'NAME')}
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell stiff>
@@ -835,7 +878,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(sensor_sort.state, 'TEMPERATURE')}
onClick={() => sensor_sort.fns.onToggleSort({ sortKey: 'TEMPERATURE' })}
>
TEMPERATURE
{LL.VALUE()}
</Button>
</HeaderCell>
<HeaderCell stiff />
@@ -865,7 +908,7 @@ const DashboardData: FC = () => {
const renderAnalogData = () => (
<>
<Typography sx={{ pt: 2, pb: 1 }} variant="h6" color="secondary">
Analog Sensors
{LL.ANALOG_SENSORS()}
</Typography>
<Table data={{ nodes: sensorData.analogs }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}>
@@ -890,7 +933,7 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(analog_sort.state, 'NAME')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
{LL.ENTITY_NAME()}
</Button>
</HeaderCell>
<HeaderCell stiff>
@@ -900,10 +943,10 @@ const DashboardData: FC = () => {
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
>
TYPE
{LL.TYPE()}
</Button>
</HeaderCell>
<HeaderCell stiff>VALUE</HeaderCell>
<HeaderCell stiff>{LL.VALUE()}</HeaderCell>
<HeaderCell stiff />
</HeaderRow>
</Header>
@@ -943,14 +986,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog deletion failed', { variant: 'error' });
enqueueSnackbar(LL.ANALOG_SENSOR() + ' ' + LL.DELETION() + ' ' + LL.FAILED(), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor removed', { variant: 'success' });
enqueueSnackbar(LL.ANALOG_SENSOR() + ' ' + LL.REMOVED(), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog sensor'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -971,14 +1014,14 @@ const DashboardData: FC = () => {
});
if (response.status === 204) {
enqueueSnackbar('Analog sensor update failed', { variant: 'error' });
enqueueSnackbar(LL.ANALOG_SENSOR() + ' ' + LL.UPDATE() + ' ' + LL.FAILED(), { variant: 'error' });
} else if (response.status === 403) {
enqueueSnackbar('Access denied', { variant: 'error' });
enqueueSnackbar(LL.ACCESS_DENIED(), { variant: 'error' });
} else {
enqueueSnackbar('Analog sensor updated', { variant: 'success' });
enqueueSnackbar(LL.ANALOG_SENSOR() + ' ' + LL.UPDATED(), { variant: 'success' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem updating analog'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setAnalog(undefined);
fetchSensorData();
@@ -990,7 +1033,9 @@ const DashboardData: FC = () => {
if (analog) {
return (
<Dialog open={analog !== undefined} onClose={() => setAnalog(undefined)}>
<DialogTitle>Edit Analog Sensor</DialogTitle>
<DialogTitle>
{LL.EDIT()} {LL.ANALOG_SENSOR()}
</DialogTitle>
<DialogContent dividers>
<Grid container spacing={2}>
<Grid item>
@@ -1007,7 +1052,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="n"
label="Name"
label={LL.ENTITY_NAME()}
value={analog.n}
sx={{ width: '20ch' }}
variant="outlined"
@@ -1015,7 +1060,13 @@ const DashboardData: FC = () => {
/>
</Grid>
<Grid item>
<ValidatedTextField name="t" label="Type" value={analog.t} select onChange={updateValue(setAnalog)}>
<ValidatedTextField
name="t"
label={LL.TYPE()}
value={analog.t}
select
onChange={updateValue(setAnalog)}
>
{AnalogTypeNames.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
@@ -1038,7 +1089,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Offset"
label={LL.OFFSET()}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
type="number"
@@ -1055,7 +1106,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Start Value"
label={LL.STARTVALUE()}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
type="number"
@@ -1068,7 +1119,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="f"
label="Factor"
label={LL.FACTOR()}
value={numberValue(analog.f)}
sx={{ width: '20ch' }}
type="number"
@@ -1084,7 +1135,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="DAC Value"
label={LL.VALUE()}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
type="number"
@@ -1100,7 +1151,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Value"
label={LL.VALUE()}
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
type="number"
@@ -1116,7 +1167,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="f"
label="Frequency"
label={LL.CPU_FREQ()}
value={numberValue(analog.f)}
sx={{ width: '20ch' }}
type="number"
@@ -1131,7 +1182,7 @@ const DashboardData: FC = () => {
<Grid item>
<ValidatedTextField
name="o"
label="Dutycycle"
label="Duty Cycle"
value={numberValue(analog.o)}
sx={{ width: '20ch' }}
type="number"
@@ -1147,13 +1198,13 @@ const DashboardData: FC = () => {
)}
</Grid>
<Box color="warning.main" mt={2}>
<Typography variant="body2">Warning: be careful when assigning a GPIO!</Typography>
<Typography variant="body2">{LL.WARN_GPIO()}</Typography>
</Box>
</DialogContent>
<DialogActions>
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={() => sendRemoveAnalog()}>
Remove
{LL.REMOVE()}
</Button>
</Box>
<Button
@@ -1162,7 +1213,7 @@ const DashboardData: FC = () => {
onClick={() => setAnalog(undefined)}
color="secondary"
>
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
@@ -1171,7 +1222,7 @@ const DashboardData: FC = () => {
onClick={() => sendAnalog()}
color="warning"
>
Save
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
@@ -1180,7 +1231,7 @@ const DashboardData: FC = () => {
};
return (
<SectionContent title="Device and Sensor Data" titleGutter>
<SectionContent title={LL.DEVICE_SENSOR_DATA()} titleGutter>
{renderCoreData()}
{renderDeviceData()}
{renderDeviceDialog()}
@@ -1191,11 +1242,11 @@ const DashboardData: FC = () => {
{renderAnalogDialog()}
<ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}>
Refresh
{LL.REFRESH()}
</Button>
{device_select.state.id && device_select.state.id !== 'sensor' && (
<Button startIcon={<DownloadIcon />} variant="outlined" onClick={handleDownloadCsv}>
Export
{LL.EXPORT()}
</Button>
)}
</ButtonRow>

View File

@@ -32,10 +32,14 @@ import { ButtonRow, FormLoader, SectionContent } from '../components';
import { Status, busConnectionStatus, Stat } from './types';
import { formatDurationSec, pluralize, extractErrorMessage, useRest } from '../utils';
import { extractErrorMessage, useRest } from '../utils';
import * as EMSESP from './api';
import type { Translation } from '../i18n/i18n-types';
import { useI18nContext } from '../i18n/i18n-react';
import parseMilliseconds from 'parse-ms';
export const isConnected = ({ status }: Status) => status !== busConnectionStatus.BUS_STATUS_OFFLINE;
const busStatusHighlight = ({ status }: Status, theme: Theme) => {
@@ -51,19 +55,6 @@ const busStatusHighlight = ({ status }: Status, theme: Theme) => {
}
};
const busStatus = ({ status }: Status) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return 'Connected';
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return 'Tx issues - try a different Tx Mode';
case busConnectionStatus.BUS_STATUS_OFFLINE:
return 'Disconnected';
default:
return 'Unknown';
}
};
const showQuality = (stat: Stat) => {
if (stat.q === 0 || stat.s + stat.f === 0) {
return;
@@ -81,12 +72,32 @@ const showQuality = (stat: Stat) => {
const DashboardStatus: FC = () => {
const { loadData, data, errorMessage } = useRest<Status>({ read: EMSESP.readStatus });
const { LL } = useI18nContext();
const theme = useTheme();
const [confirmScan, setConfirmScan] = useState<boolean>(false);
const { enqueueSnackbar } = useSnackbar();
const { me } = useContext(AuthenticatedContext);
const showName = (id: any) => {
let name: keyof Translation['STATUS_NAMES'] = id;
return LL.STATUS_NAMES[name]();
};
const busStatus = ({ status }: Status) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return LL.CONNECTED();
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return LL.TX_ISSUES();
case busConnectionStatus.BUS_STATUS_OFFLINE:
return LL.DISCONNECTED();
default:
return 'Unknown';
}
};
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
@@ -137,24 +148,40 @@ const DashboardStatus: FC = () => {
const scan = async () => {
try {
await EMSESP.scanDevices();
enqueueSnackbar('Scanning for devices...', { variant: 'info' });
enqueueSnackbar(LL.SCANNING() + '...', { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem initiating scan'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setConfirmScan(false);
}
};
const formatDurationSec = (duration_sec: number) => {
const { days, hours, minutes, seconds } = parseMilliseconds(duration_sec * 1000);
let formatted = ' ';
if (days) {
formatted += LL.NUM_DAYS({ num: days }) + ' ';
}
if (hours) {
formatted += LL.NUM_HOURS({ num: hours }) + ' ';
}
if (minutes) {
formatted += LL.NUM_MINUTES({ num: minutes }) + ' ';
}
formatted += LL.NUM_SECONDS({ num: seconds });
return formatted;
};
const renderScanDialog = () => (
<Dialog open={confirmScan} onClose={() => setConfirmScan(false)}>
<DialogTitle>EMS Device Scan</DialogTitle>
<DialogContent dividers>Are you sure you want to initiate a full device scan of the EMS bus?</DialogContent>
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary">
Cancel
{LL.CANCEL()}
</Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary" autoFocus>
Scan
{LL.SCAN()}
</Button>
</DialogActions>
</Dialog>
@@ -174,7 +201,7 @@ const DashboardStatus: FC = () => {
<DirectionsBusIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="EMS Bus Status" secondary={busStatus(data) + formatDurationSec(data.uptime)} />
<ListItemText primary={LL.EMS_BUS_STATUS()} secondary={busStatus(data) + formatDurationSec(data.uptime)} />
</ListItem>
<ListItem>
<ListItemAvatar>
@@ -183,13 +210,13 @@ const DashboardStatus: FC = () => {
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Active Devices &amp; Sensors"
primary={LL.ACTIVE_DEVICES()}
secondary={
pluralize(data.num_devices, 'EMS Device') +
LL.NUM_DEVICES({ num: data.num_devices }) +
', ' +
pluralize(data.num_sensors, 'Temperature Sensor') +
LL.NUM_TEMP_SENSORS({ num: data.num_sensors }) +
', ' +
pluralize(data.num_analogs, 'Analog Sensor')
LL.NUM_ANALOG_SENSORS({ num: data.num_analogs })
}
/>
</ListItem>
@@ -200,15 +227,15 @@ const DashboardStatus: FC = () => {
<Header>
<HeaderRow>
<HeaderCell resize></HeaderCell>
<HeaderCell stiff>SUCCESS</HeaderCell>
<HeaderCell stiff>FAIL</HeaderCell>
<HeaderCell stiff>QUALITY</HeaderCell>
<HeaderCell stiff>{LL.SUCCESS()}</HeaderCell>
<HeaderCell stiff>{LL.FAIL()}</HeaderCell>
<HeaderCell stiff>{LL.QUALITY()}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((stat: Stat) => (
<Row key={stat.id} item={stat}>
<Cell>{stat.id}</Cell>
<Cell>{showName(stat.id)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.s)}</Cell>
<Cell stiff>{Intl.NumberFormat().format(stat.f)}</Cell>
<Cell stiff>{showQuality(stat)}</Cell>
@@ -223,7 +250,7 @@ const DashboardStatus: FC = () => {
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}>
Refresh
{LL.REFRESH()}
</Button>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
@@ -235,7 +262,7 @@ const DashboardStatus: FC = () => {
disabled={!me.admin}
onClick={() => setConfirmScan(true)}
>
Scan for new devices
{LL.SCAN_DEVICES()}
</Button>
</ButtonRow>
</Box>
@@ -245,7 +272,7 @@ const DashboardStatus: FC = () => {
};
return (
<SectionContent title="EMS Bus &amp; Activity Status" titleGutter>
<SectionContent title={LL.EMS_BUS_STATUS_TITLE()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -5,16 +5,20 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import HelpInformation from './HelpInformation';
const Help: FC = () => {
useLayoutTitle('Help');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.HELP());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="information" label="EMS-ESP Help" />
<Tab value="information" label={"EMS-ESP " + LL.HELP()} />
</RouterTabs>
<Routes>
<Route path="information" element={<HelpInformation />} />

View File

@@ -11,12 +11,17 @@ import MenuBookIcon from '@mui/icons-material/MenuBookTwoTone';
import GitHubIcon from '@mui/icons-material/GitHub';
import StarIcon from '@mui/icons-material/Star';
import DownloadIcon from '@mui/icons-material/GetApp';
import EastIcon from '@mui/icons-material/East';
import { extractErrorMessage } from '../utils';
import { useI18nContext } from '../i18n/i18n-react';
import * as EMSESP from './api';
const HelpInformation: FC = () => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const saveFile = (json: any, endpoint: string) => {
@@ -31,7 +36,7 @@ const HelpInformation: FC = () => {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
enqueueSnackbar('System information downloaded', { variant: 'info' });
enqueueSnackbar(LL.DOWNLOAD_SUCCESSFUL(), { variant: 'info' });
};
const callAPI = async (endpoint: string) => {
@@ -42,83 +47,85 @@ const HelpInformation: FC = () => {
id: 0
});
if (response.status !== 200) {
enqueueSnackbar('API call failed', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_LOADING(), { variant: 'error' });
} else {
saveFile(response.data, endpoint);
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem with downloading'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_LOADING()), { variant: 'error' });
}
};
return (
<SectionContent title="Support Information" titleGutter>
<SectionContent title={LL.SUPPORT_INFORMATION()} titleGutter>
<List>
<ListItem>
<ListItemAvatar>
<MenuBookIcon />
<MenuBookIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
Visit the online&nbsp;
{LL.HELP_INFORMATION_1()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
&nbsp;
<Link target="_blank" href="https://emsesp.github.io/docs" color="primary">
{'Wiki'}
{LL.CLICK_HERE()}
</Link>
&nbsp;to get instructions on how to&nbsp;
<Link
target="_blank"
href="https://emsesp.github.io/docs/#/Configure-firmware?id=ems-esp-settings"
color="primary"
>
{'configure'}
</Link>
&nbsp;EMS-ESP and access other information.
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<CommentIcon />
<CommentIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
For live community chat join our&nbsp;
{LL.HELP_INFORMATION_2()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
&nbsp;
<Link target="_blank" href="https://discord.gg/3J3GgnzpyT" color="primary">
{'Discord'}
{LL.CLICK_HERE()}
</Link>
&nbsp;server.
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<GitHubIcon />
<GitHubIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
</ListItemAvatar>
<ListItemText>
Submit a&nbsp;
{LL.HELP_INFORMATION_3()}&nbsp;
<EastIcon style={{ fontSize: 24, color: 'lightblue', verticalAlign: 'middle' }} />
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">
support issue
{LL.CLICK_HERE()}
</Link>
&nbsp;for requesting a new feature or reporting a bug.
<br />
Make sure you also&nbsp;
<Button startIcon={<DownloadIcon />} variant="outlined" color="primary" onClick={() => callAPI('info')}>
download
<i>({LL.HELP_INFORMATION_4()}</i>&nbsp;
<Button
startIcon={<DownloadIcon />}
size="small"
variant="outlined"
color="primary"
onClick={() => callAPI('info')}
>
{LL.SUPPORT_INFO()}
</Button>
&nbsp; and attach your system details for a faster response.
&nbsp;)
</ListItemText>
</ListItem>
</List>
<Box border={1} p={1} mt={4}>
<Box border={1} p={1} mt={4} color="orange">
<Typography align="center" variant="h6" color="orange">
EMS-ESP will always be a free and open-source project
<br></br>Please consider supporting it with a&nbsp;
<StarIcon style={{ fontSize: 16, color: 'yellow', verticalAlign: 'middle' }} /> on&nbsp;
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
{'GitHub'}
</Link>
{LL.HELP_INFORMATION_5()}
</Typography>
<Typography align="center">
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
{'Github'}
</Link>
<StarIcon style={{ fontSize: 20, color: 'yellow', verticalAlign: 'middle' }} />
</Typography>
<Typography color="white" align="center">
@proddy @MichaelDvP
</Typography>
<Typography align="center">@proddy @MichaelDvP</Typography>
</Box>
</SectionContent>
);

View File

@@ -6,6 +6,8 @@ import { AuthenticatedContext } from '../contexts/authentication';
import { PROJECT_PATH } from '../api/env';
import { useI18nContext } from '../i18n/i18n-react';
import TuneIcon from '@mui/icons-material/Tune';
import DashboardIcon from '@mui/icons-material/Dashboard';
import LayoutMenuItem from '../components/layout/LayoutMenuItem';
@@ -13,17 +15,18 @@ import InfoIcon from '@mui/icons-material/Info';
const ProjectMenu: FC = () => {
const authenticatedContext = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
return (
<List>
<LayoutMenuItem icon={DashboardIcon} label="Dashboard" to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem icon={DashboardIcon} label={LL.DASHBOARD()} to={`/${PROJECT_PATH}/dashboard`} />
<LayoutMenuItem
icon={TuneIcon}
label="Settings"
label={LL.SETTINGS()}
to={`/${PROJECT_PATH}/settings`}
disabled={!authenticatedContext.me.admin}
/>
<LayoutMenuItem icon={InfoIcon} label="Help" to={`/${PROJECT_PATH}/help`} />
<LayoutMenuItem icon={InfoIcon} label={LL.HELP()} to={`/${PROJECT_PATH}/help`} />
</List>
);
};

View File

@@ -5,18 +5,22 @@ import { Tab } from '@mui/material';
import { RouterTabs, useRouterTab, useLayoutTitle } from '../components';
import { useI18nContext } from '../i18n/i18n-react';
import SettingsApplication from './SettingsApplication';
import SettingsCustomization from './SettingsCustomization';
const Settings: FC = () => {
useLayoutTitle('Settings');
const { LL } = useI18nContext();
const { routerTab } = useRouterTab();
useLayoutTitle(LL.SETTINGS());
return (
<>
<RouterTabs value={routerTab}>
<Tab value="application" label="Application Settings" />
<Tab value="customization" label="Customization" />
<Tab value="application" label={LL.APPLICATION_SETTINGS()} />
<Tab value="customization" label={LL.CUSTOMIZATION()} />
</RouterTabs>
<Routes>
<Route path="application" element={<SettingsApplication />} />

View File

@@ -3,7 +3,7 @@ import { ValidateFieldsError } from 'async-validator';
import { useSnackbar } from 'notistack';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider } from '@mui/material';
import { Box, Button, Checkbox, MenuItem, Grid, Typography, Divider, InputAdornment } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
@@ -24,6 +24,8 @@ import { numberValue, extractErrorMessage, updateValue, useRest } from '../utils
import * as EMSESP from './api';
import { Settings, BOARD_PROFILES } from './types';
import { useI18nContext } from '../i18n/i18n-react';
export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}>
@@ -38,6 +40,8 @@ const SettingsApplication: FC = () => {
update: EMSESP.writeSettings
});
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const updateFormValue = updateValue(setData);
@@ -65,7 +69,7 @@ const SettingsApplication: FC = () => {
});
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem fetching board profile'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setProcessingBoard(false);
}
@@ -102,26 +106,23 @@ const SettingsApplication: FC = () => {
validateAndSubmit();
try {
await EMSESP.restart();
enqueueSnackbar('EMS-ESP is restarting...', { variant: 'info' });
enqueueSnackbar(LL.APPLICATION_RESTARTING(), { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
};
return (
<>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Interface Board Profile
Interface {LL.BOARD_PROFILE()}
</Typography>
<Box color="warning.main">
<Typography variant="body2">
Select a pre-configured interface board profile from the list below or choose "Custom" to configure your own
hardware settings.
</Typography>
<Typography variant="body2">{LL.BOARD_PROFILE_TEXT()}</Typography>
</Box>
<ValidatedTextField
name="board_profile"
label="Board Profile"
label={LL.BOARD_PROFILE()}
value={data.board_profile}
disabled={processingBoard}
variant="outlined"
@@ -170,7 +171,7 @@ const SettingsApplication: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="pbutton_gpio"
label="Button GPIO"
label={LL.BUTTON() + ' GPIO'}
fullWidth
variant="outlined"
value={numberValue(data.pbutton_gpio)}
@@ -184,7 +185,7 @@ const SettingsApplication: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="dallas_gpio"
label="Temperature GPIO (0=disabled)"
label={LL.TEMPERATURE() + ' GPIO (0=' + LL.DISABLED() + ')'}
fullWidth
variant="outlined"
value={numberValue(data.dallas_gpio)}
@@ -198,7 +199,7 @@ const SettingsApplication: FC = () => {
<ValidatedTextField
fieldErrors={fieldErrors}
name="led_gpio"
label="LED GPIO (0=disabled)"
label={'LED GPIO (0=' + LL.DISABLED() + ')'}
fullWidth
variant="outlined"
value={numberValue(data.led_gpio)}
@@ -221,7 +222,7 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={0}>No Ethernet Module</MenuItem>
<MenuItem value={0}>{LL.DISABLED()}</MenuItem>
<MenuItem value={1}>LAN8720</MenuItem>
<MenuItem value={2}>TLK110</MenuItem>
</ValidatedTextField>
@@ -231,7 +232,7 @@ const SettingsApplication: FC = () => {
<Grid item>
<ValidatedTextField
name="eth_power"
label="Eth Power GPIO (-1=disabled)"
label={'Eth Power GPIO (-1=' + LL.DISABLED() + ')'}
fullWidth
variant="outlined"
value={numberValue(data.eth_power)}
@@ -277,7 +278,7 @@ const SettingsApplication: FC = () => {
</>
)}
<Typography variant="h6" color="primary">
EMS Bus Settings
EMS Bus {LL.SETTINGS()}
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={6}>
@@ -319,63 +320,90 @@ const SettingsApplication: FC = () => {
</Grid>
</Grid>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
General Options
{LL.GENERAL_OPTIONS()}
</Typography>
<Box
sx={{
'& .MuiTextField-root': { width: '25ch' }
}}
>
<ValidatedTextField
name="locale"
label={LL.LANGUAGE_ENTITIES()}
disabled={saving}
value={data.locale}
variant="outlined"
onChange={updateFormValue}
margin="normal"
size="small"
select
>
<MenuItem value="en">English (EN)</MenuItem>
<MenuItem value="de">Deutsch (DE)</MenuItem>
<MenuItem value="nl">Nederlands (NL)</MenuItem>
<MenuItem value="se">Svenska (SE)</MenuItem>
<MenuItem disabled value="pl">Polski (PL)</MenuItem>
<MenuItem disabled value="no">Norsk (NO)</MenuItem>
</ValidatedTextField>
</Box>
{data.led_gpio !== 0 && (
<BlockFormControlLabel
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />}
label="Hide LED"
label={LL.HIDE_LED()}
disabled={saving}
/>
)}
<BlockFormControlLabel
control={<Checkbox checked={data.telnet_enabled} onChange={updateFormValue} name="telnet_enabled" />}
label="Enable Telnet Console"
label={LL.ENABLE_TELNET()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.analog_enabled} onChange={updateFormValue} name="analog_enabled" />}
label="Enable Analog Sensors"
label={LL.ENABLE_ANALOG()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />}
label="Convert temperature values to Fahrenheit"
label={LL.CONVERT_FAHRENHEIT()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.notoken_api} onChange={updateFormValue} name="notoken_api" />}
label="Bypass Access Token authorization on API calls"
label={LL.BYPASS_TOKEN()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />}
label="Enable Read only mode (blocks all outgoing EMS Tx write commands)"
label={LL.READONLY()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.low_clock} onChange={updateFormValue} name="low_clock" />}
label="Underclock CPU speed"
label={LL.UNDERCLOCK_CPU()}
disabled={saving}
/>
<Grid container spacing={0} direction="row" justifyContent="flex-start" alignItems="flex-start">
<BlockFormControlLabel
control={<Checkbox checked={data.shower_timer} onChange={updateFormValue} name="shower_timer" />}
label="Enable Shower Timer"
label={LL.ENABLE_SHOWER_TIMER()}
disabled={saving}
/>
<BlockFormControlLabel
control={<Checkbox checked={data.shower_alert} onChange={updateFormValue} name="shower_alert" />}
label="Enable Shower Alert"
label={LL.ENABLE_SHOWER_ALERT()}
disabled={!data.shower_timer}
/>
{data.shower_alert && (
<>
<Grid item xs={2}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_trigger"
label="Trigger Time (minutes)"
label={LL.TRIGGER_TIME()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_trigger}
type="number"
@@ -383,11 +411,14 @@ const SettingsApplication: FC = () => {
disabled={!data.shower_timer}
/>
</Grid>
<Grid item xs={2}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="shower_alert_coldshot"
label="Cold Shot Time (seconds)"
label={LL.COLD_SHOT_DURATION()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
variant="outlined"
value={data.shower_alert_coldshot}
type="number"
@@ -399,13 +430,13 @@ const SettingsApplication: FC = () => {
)}
</Grid>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Formatting Options
{LL.FORMATTING_OPTIONS()}
</Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={4}>
<ValidatedTextField
name="bool_dashboard"
label="Boolean Format Dashboard"
label={LL.BOOLEAN_FORMAT_DASHBOARD()}
value={data.bool_dashboard}
fullWidth
variant="outlined"
@@ -413,8 +444,8 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>on/off</MenuItem>
<MenuItem value={2}>ON/OFF</MenuItem>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>true/false</MenuItem>
<MenuItem value={5}>1/0</MenuItem>
</ValidatedTextField>
@@ -422,7 +453,7 @@ const SettingsApplication: FC = () => {
<Grid item xs={4}>
<ValidatedTextField
name="bool_format"
label="Boolean Format API/MQTT"
label={LL.BOOLEAN_FORMAT_API()}
value={data.bool_format}
fullWidth
variant="outlined"
@@ -430,8 +461,8 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>"on"/"off"</MenuItem>
<MenuItem value={2}>"ON"/"OFF"</MenuItem>
<MenuItem value={1}>{LL.ONOFF()}</MenuItem>
<MenuItem value={2}>{LL.ONOFF_CAP()}</MenuItem>
<MenuItem value={3}>"true"/"false"</MenuItem>
<MenuItem value={4}>true/false</MenuItem>
<MenuItem value={5}>"1"/"0"</MenuItem>
@@ -441,7 +472,7 @@ const SettingsApplication: FC = () => {
<Grid item xs={4}>
<ValidatedTextField
name="enum_format"
label="Enum Format API/MQTT"
label={LL.ENUM_FORMAT()}
value={data.enum_format}
fullWidth
variant="outlined"
@@ -449,29 +480,29 @@ const SettingsApplication: FC = () => {
margin="normal"
select
>
<MenuItem value={1}>Value</MenuItem>
<MenuItem value={2}>Index</MenuItem>
<MenuItem value={1}>{LL.VALUE()}</MenuItem>
<MenuItem value={2}>{LL.INDEX()}</MenuItem>
</ValidatedTextField>
</Grid>
</Grid>
{data.dallas_gpio !== 0 && (
<>
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Temperature Sensors
{LL.TEMP_SENSORS()}
</Typography>
<BlockFormControlLabel
control={<Checkbox checked={data.dallas_parasite} onChange={updateFormValue} name="dallas_parasite" />}
label="Enable parasite power"
label={LL.ENABLE_PARASITE()}
disabled={saving}
/>
</>
)}
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
Logging
{LL.LOGGING()}
</Typography>
<BlockFormControlLabel
control={<Checkbox checked={data.trace_raw} onChange={updateFormValue} name="trace_raw" />}
label="Log EMS telegrams in hexadecimal"
label={LL.LOG_HEX()}
disabled={saving}
/>
<BlockFormControlLabel
@@ -483,11 +514,11 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
}
label="Enable Syslog"
label={LL.ENABLE_SYSLOG()}
/>
{data.syslog_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_host"
@@ -500,7 +531,7 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_port"
@@ -514,7 +545,7 @@ const SettingsApplication: FC = () => {
disabled={saving}
/>
</Grid>
<Grid item xs={5}>
<Grid item xs={4}>
<ValidatedTextField
name="syslog_level"
label="Log Level"
@@ -534,11 +565,14 @@ const SettingsApplication: FC = () => {
<MenuItem value={9}>ALL</MenuItem>
</ValidatedTextField>
</Grid>
<Grid item xs={6}>
<Grid item xs={4}>
<ValidatedTextField
fieldErrors={fieldErrors}
name="syslog_mark_interval"
label="Mark Interval (seconds, 0=off)"
label={LL.MARK_INTERVAL()}
InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}}
fullWidth
variant="outlined"
value={data.syslog_mark_interval}
@@ -551,9 +585,9 @@ const SettingsApplication: FC = () => {
</Grid>
)}
{restartNeeded && (
<MessageBox my={2} level="warning" message="EMS-ESP needs to be restarted to apply changed system settings">
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT()}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}>
Restart
{LL.RESTART()}
</Button>
</MessageBox>
)}
@@ -567,7 +601,7 @@ const SettingsApplication: FC = () => {
type="submit"
onClick={validateAndSubmit}
>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
)}
@@ -576,7 +610,7 @@ const SettingsApplication: FC = () => {
};
return (
<SectionContent title="Application Settings" titleGutter>
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
{content()}
</SectionContent>
);

View File

@@ -19,7 +19,6 @@ import {
import { Table } from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme';
import { useSort, SortToggleType } from '@table-library/react-table-library/sort';
import { Header, HeaderRow, HeaderCell, Body, Row, Cell } from '@table-library/react-table-library/table';
import { useSnackbar } from 'notistack';
@@ -28,9 +27,6 @@ import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Cancel';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import KeyboardArrowUpOutlinedIcon from '@mui/icons-material/KeyboardArrowUpOutlined';
import KeyboardArrowDownOutlinedIcon from '@mui/icons-material/KeyboardArrowDownOutlined';
import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
import SearchIcon from '@mui/icons-material/Search';
import FilterListIcon from '@mui/icons-material/FilterList';
@@ -40,16 +36,22 @@ import { ButtonRow, FormLoader, ValidatedTextField, SectionContent } from '../co
import * as EMSESP from './api';
import { extractErrorMessage } from '../utils';
import { extractErrorMessage, updateValue } from '../utils';
import { DeviceShort, Devices, DeviceEntity, DeviceEntityMask } from './types';
import { useI18nContext } from '../i18n/i18n-react';
export const APIURL = window.location.origin + '/api/';
const SettingsCustomization: FC = () => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([{ id: '', v: 0, n: '', m: 0, w: false }]);
const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false };
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([emptyDeviceEntity]);
const [devices, setDevices] = useState<Devices>();
const [errorMessage, setErrorMessage] = useState<string>();
const [selectedDevice, setSelectedDevice] = useState<number>(-1);
@@ -57,6 +59,8 @@ const SettingsCustomization: FC = () => {
const [selectedFilters, setSelectedFilters] = useState<number>(0);
const [search, setSearch] = useState('');
const [deviceEntity, setDeviceEntity] = useState<DeviceEntity>();
// eslint-disable-next-line
const [masks, setMasks] = useState(() => ['']);
@@ -92,6 +96,7 @@ const SettingsCustomization: FC = () => {
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
.td {
border-top: 1px solid #565656;
@@ -104,6 +109,11 @@ const SettingsCustomization: FC = () => {
font-weight: normal;
}
&:hover .td {
border-top: 1px solid #177ac9;
border-bottom: 1px solid #177ac9;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
@@ -118,50 +128,24 @@ const SettingsCustomization: FC = () => {
`
});
const getSortIcon = (state: any, sortKey: any) => {
if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />;
}
if (state.sortKey === sortKey && !state.reverse) {
return <KeyboardArrowUpOutlinedIcon />;
}
return <UnfoldMoreOutlinedIcon />;
};
const entity_sort = useSort(
{ nodes: deviceEntities },
{},
{
sortIcon: {
iconDefault: <UnfoldMoreOutlinedIcon />,
iconUp: <KeyboardArrowUpOutlinedIcon />,
iconDown: <KeyboardArrowDownOutlinedIcon />
},
sortToggleType: SortToggleType.AlternateWithReset,
sortFns: {
NAME: (array) => array.sort((a, b) => a.id.localeCompare(b.id))
}
}
);
const fetchDevices = useCallback(async () => {
try {
setDevices((await EMSESP.readDevices()).data);
} catch (error: unknown) {
setErrorMessage(extractErrorMessage(error, 'Failed to fetch device list'));
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
}, []);
}, [LL]);
const setInitialMask = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, om: de.m })));
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn })));
};
const fetchDeviceEntities = async (unique_id: number) => {
try {
const data = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
setInitialMask(data);
const new_deviceEntities = (await EMSESP.readDeviceEntities({ id: unique_id })).data;
setInitialMask(new_deviceEntities);
} catch (error: unknown) {
setErrorMessage(extractErrorMessage(error, 'Problem fetching device entities'));
setErrorMessage(extractErrorMessage(error, LL.PROBLEM_LOADING()));
}
};
@@ -183,12 +167,16 @@ const SettingsCustomization: FC = () => {
function formatName(de: DeviceEntity) {
if (de.n === undefined || de.n === de.id) {
return de.id;
} else if (de.n === '') {
return 'Command: ' + de.id;
}
if (de.n === '') {
return LL.COMMAND() + ': ' + de.id;
}
return (
<>
{de.n}&nbsp;(
{de.cn !== undefined && de.cn !== '' ? de.cn : de.n}
&nbsp;(
<Link target="_blank" href={APIURL + devices?.devices[selectedDevice].t + '/' + de.id}>
{de.id}
</Link>
@@ -250,9 +238,9 @@ const SettingsCustomization: FC = () => {
const resetCustomization = async () => {
try {
await EMSESP.resetCustomizations();
enqueueSnackbar('All customizations have been removed. Restarting...', { variant: 'info' });
enqueueSnackbar(LL.CUSTOMIZATIONS_RESTART(), { variant: 'info' });
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem resetting customizations'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
} finally {
setConfirmReset(false);
}
@@ -261,26 +249,28 @@ const SettingsCustomization: FC = () => {
const saveCustomization = async () => {
if (devices && deviceEntities && selectedDevice !== -1) {
const masked_entities = deviceEntities
.filter((de) => de.m !== de.om)
.map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.id);
.filter((de) => de.m !== de.o_m || de.cn !== de.o_cn)
.map((new_de) => new_de.m.toString(16).padStart(2, '0') + new_de.id + (new_de.cn ? '|' + new_de.cn : ''));
if (masked_entities.length > 60) {
enqueueSnackbar('Selected entities exceeded limit of 60. Please Save in batches', { variant: 'warning' });
// check size in bytes to match buffer in CPP, which is 4096
const bytes = new TextEncoder().encode(JSON.stringify(masked_entities)).length;
if (bytes > 4000) {
enqueueSnackbar(LL.CUSTOMIZATIONS_FULL(), { variant: 'warning' });
return;
}
try {
const response = await EMSESP.writeMaskedEntities({
const response = await EMSESP.writeCustomEntities({
id: devices?.devices[selectedDevice].i,
entity_ids: masked_entities
});
if (response.status === 200) {
enqueueSnackbar('Customization saved', { variant: 'success' });
enqueueSnackbar(LL.CUSTOMIZATIONS_SAVED(), { variant: 'success' });
} else {
enqueueSnackbar('Customization save failed', { variant: 'error' });
enqueueSnackbar(LL.PROBLEM_UPDATING(), { variant: 'error' });
}
} catch (error: unknown) {
enqueueSnackbar(extractErrorMessage(error, 'Problem sending entity list'), { variant: 'error' });
enqueueSnackbar(extractErrorMessage(error, LL.PROBLEM_UPDATING()), { variant: 'error' });
}
setInitialMask(deviceEntities);
}
@@ -294,21 +284,17 @@ const SettingsCustomization: FC = () => {
return (
<>
<Box mb={2} color="warning.main">
<Typography variant="body2">Select a device and customize each of its entities using the options:</Typography>
<Typography variant="body2">{LL.CUSTOMIZATIONS_HELP_1()}</Typography>
<Typography variant="body2">
<OptionIcon type="favorite" isSet={true} />
=mark as favorite&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />
=disable write action&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />
=exclude from MQTT and API&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />
=hide from Dashboard
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp;
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp;
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
</Typography>
</Box>
<ValidatedTextField
name="device"
label="EMS Device"
label={'EMS ' + LL.DEVICE()}
variant="outlined"
fullWidth
value={selectedDevice}
@@ -317,7 +303,7 @@ const SettingsCustomization: FC = () => {
select
>
<MenuItem disabled key={0} value={-1}>
Select a device...
{LL.SELECT_DEVICE()}...
</MenuItem>
{devices.devices.map((device: DeviceShort, index) => (
<MenuItem key={index} value={index}>
@@ -329,6 +315,29 @@ const SettingsCustomization: FC = () => {
);
};
const editEntity = (de: DeviceEntity) => {
if (de.cn === undefined) {
de.cn = '';
}
setDeviceEntity(de);
};
const updateEntity = () => {
if (deviceEntity) {
setDeviceEntities((prevState) => {
const newState = prevState.map((obj) => {
if (obj.id === deviceEntity.id) {
return { ...obj, cn: deviceEntity.cn };
}
return obj;
});
return newState;
});
}
setDeviceEntity(undefined);
};
const renderDeviceData = () => {
if (devices?.devices.length === 0 || deviceEntities[0].id === '') {
return;
@@ -401,7 +410,7 @@ const SettingsCustomization: FC = () => {
color="inherit"
onClick={() => maskDisabled(false)}
>
set all&nbsp;
{LL.SET_ALL()}&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={false} />
<OptionIcon type="web_exclude" isSet={false} />
</Button>
@@ -416,35 +425,30 @@ const SettingsCustomization: FC = () => {
color="inherit"
onClick={() => maskDisabled(true)}
>
set all&nbsp;
{LL.SET_ALL()}&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />
<OptionIcon type="web_exclude" isSet={true} />
</Button>
</Tooltip>
</Grid>
</Grid>
<Table data={{ nodes: shown_data }} theme={entities_theme} sort={entity_sort} layout={{ custom: true }}>
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
{(tableList: any) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff>OPTIONS</HeaderCell>
<HeaderCell stiff>{LL.OPTIONS()}</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(entity_sort.state, 'NAME')}
onClick={() => entity_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
NAME
<Button fullWidth style={{ fontSize: '14px', justifyContent: 'flex-start' }}>
{LL.NAME()}
</Button>
</HeaderCell>
<HeaderCell resize>VALUE</HeaderCell>
<HeaderCell resize>{LL.VALUE()}</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((de: DeviceEntity) => (
<Row key={de.id} item={de}>
<Row key={de.id} item={de} onClick={() => editEntity(de)}>
<Cell stiff>
<ToggleButtonGroup
size="small"
@@ -503,14 +507,11 @@ const SettingsCustomization: FC = () => {
const renderResetDialog = () => (
<Dialog open={confirmReset} onClose={() => setConfirmReset(false)}>
<DialogTitle>Reset</DialogTitle>
<DialogContent dividers>
Are you sure you want remove all customizations including the custom settings of the Temperature and Analog
sensors?
</DialogContent>
<DialogTitle>{LL.RESET()}</DialogTitle>
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
<DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary">
Cancel
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
@@ -519,7 +520,7 @@ const SettingsCustomization: FC = () => {
autoFocus
color="error"
>
Reset
{LL.RESET()}
</Button>
</DialogActions>
</Dialog>
@@ -529,15 +530,15 @@ const SettingsCustomization: FC = () => {
return (
<>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
Device Entities
{LL.DEVICE_ENTITIES()}
</Typography>
{renderDeviceList()}
{renderDeviceData()}
{!deviceEntity && renderDeviceData()}
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<ButtonRow>
<Button startIcon={<SaveIcon />} variant="outlined" color="primary" onClick={() => saveCustomization()}>
Save
{LL.SAVE()}
</Button>
</ButtonRow>
</Box>
@@ -548,7 +549,7 @@ const SettingsCustomization: FC = () => {
color="error"
onClick={() => setConfirmReset(true)}
>
Reset
{LL.RESET()}
</Button>
</ButtonRow>
</Box>
@@ -557,8 +558,55 @@ const SettingsCustomization: FC = () => {
);
};
const renderEditEntity = () => {
if (deviceEntity) {
return (
<Dialog open={!!deviceEntity} onClose={() => setDeviceEntity(undefined)}>
<DialogTitle>{LL.RENAME() + ' ' + LL.ENTITY_NAME()}</DialogTitle>
<DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
<Typography variant="body2">{deviceEntity.n}</Typography>
</Box>
<Grid container spacing={1}>
<Grid item>
<TextField
name="cn"
label={LL.NEW() + ' ' + LL.ENTITY_NAME()}
value={deviceEntity.cn}
autoFocus
sx={{ width: '30ch' }}
onChange={updateValue(setDeviceEntity)}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setDeviceEntity(undefined)}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<SaveIcon />}
variant="outlined"
type="submit"
onClick={() => updateEntity()}
color="warning"
>
{LL.SAVE()}
</Button>
</DialogActions>
</Dialog>
);
}
};
return (
<SectionContent title="User Customization" titleGutter>
<SectionContent title={LL.USER_CUSTOMIZATION()} titleGutter>
{renderEditEntity()}
{content()}
</SectionContent>
);

View File

@@ -12,7 +12,7 @@ import {
DeviceData,
DeviceEntity,
UniqueID,
MaskedEntities,
CustomEntities,
WriteValue,
WriteSensor,
WriteAnalog,
@@ -63,8 +63,8 @@ export function readDeviceEntities(unique_id: UniqueID): AxiosPromise<DeviceEnti
return AXIOS_BIN.post('/deviceEntities', unique_id);
}
export function writeMaskedEntities(maskedEntities: MaskedEntities): AxiosPromise<void> {
return AXIOS.post('/maskedEntities', maskedEntities);
export function writeCustomEntities(customEntities: CustomEntities): AxiosPromise<void> {
return AXIOS.post('/customEntities', customEntities);
}
export function writeValue(writevalue: WriteValue): AxiosPromise<void> {

View File

@@ -1,4 +1,5 @@
export interface Settings {
locale: string;
tx_mode: number;
ems_bus_id: number;
syslog_enabled: boolean;
@@ -41,7 +42,7 @@ export enum busConnectionStatus {
}
export interface Stat {
id: string; // name
id: string; // id - needs to be a string
s: number; // success
f: number; // fail
q: number; // quality
@@ -136,12 +137,14 @@ export interface DeviceEntity {
id: string; // shortname
v?: any; // value, in any format, optional
n?: string; // fullname, optional
cn?: string; // custom fullname, optional
m: number; // mask
om?: number; // original mask before edits
o_m?: number; // original mask before edits
o_cn?: string; // original cn before edits
w: boolean; // writeable
}
export interface MaskedEntities {
export interface CustomEntities {
id: number;
entity_ids: string[];
}

View File

@@ -1,5 +1,3 @@
import parseMilliseconds from 'parse-ms';
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
day: 'numeric',
month: 'short',
@@ -21,21 +19,6 @@ export const formatLocalDateTime = (date: Date) => {
export const pluralize = (count: number, noun: string) =>
`${Intl.NumberFormat().format(count)} ${noun}${count !== 1 ? 's' : ''}`;
export const formatDurationMin = (duration_min: number) => {
const { days, hours, minutes } = parseMilliseconds(duration_min * 60000);
let formatted = '';
if (days) {
formatted += pluralize(days, 'day') + ' ';
}
if (hours) {
formatted += pluralize(hours, 'hour') + ' ';
}
if (minutes) {
formatted += pluralize(minutes, 'minute') + ' ';
}
return formatted;
};
export const formatDurationSec = (duration_sec: number) => {
if (duration_sec === 0) {
return ' ';

View File

@@ -4,12 +4,16 @@ import { AxiosPromise } from 'axios';
import { extractErrorMessage } from '.';
import { useI18nContext } from '../i18n/i18n-react';
export interface RestRequestOptions<D> {
read: () => AxiosPromise<D>;
update?: (value: D) => AxiosPromise<D>;
}
export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
const { LL } = useI18nContext();
const { enqueueSnackbar } = useSnackbar();
const [saving, setSaving] = useState<boolean>(false);
@@ -23,11 +27,11 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
try {
setData((await read()).data);
} catch (error: unknown) {
const message = extractErrorMessage(error, 'Problem loading data');
const message = extractErrorMessage(error, LL.PROBLEM_LOADING());
enqueueSnackbar(message, { variant: 'error' });
setErrorMessage(message);
}
}, [read, enqueueSnackbar]);
}, [read, enqueueSnackbar, LL]);
const save = useCallback(
async (toSave: D) => {
@@ -43,17 +47,17 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
if (response.status === 202) {
setRestartNeeded(true);
} else {
enqueueSnackbar('Settings saved', { variant: 'success' });
enqueueSnackbar(LL.SETTINGS() + ' ' + LL.SAVED(), { variant: 'success' });
}
} catch (error: unknown) {
const message = extractErrorMessage(error, 'Problem saving data');
const message = extractErrorMessage(error, LL.PROBLEM_UPDATING());
enqueueSnackbar(message, { variant: 'error' });
setErrorMessage(message);
} finally {
setSaving(false);
}
},
[update, enqueueSnackbar]
[update, enqueueSnackbar, LL]
);
const saveData = () => data && save(data);

View File

@@ -3,10 +3,8 @@ import Schema from 'async-validator';
export const SIGN_IN_REQUEST_VALIDATOR = new Schema({
username: {
required: true,
message: 'Please provide a username'
},
password: {
required: true,
message: 'Please provide a password'
}
});

View File

@@ -704,8 +704,13 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen){
} else if(_pinfo.opcode == WS_PING){
_queueControl(new AsyncWebSocketControl(WS_PONG, data, datalen));
} else if(_pinfo.opcode == WS_PONG){
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstringop-overflow"
if(datalen != AWSC_PING_PAYLOAD_LEN || memcmp_P(data, AWSC_PING_PAYLOAD, AWSC_PING_PAYLOAD_LEN) != 0)
_server->_handleEvent(this, WS_EVT_PONG, NULL, data, datalen);
#pragma GCC diagnostic pop
} else if(_pinfo.opcode < 8){//continuation or text/binary frame
_server->_handleEvent(this, WS_EVT_DATA, (void *)&_pinfo, data, datalen);
}

View File

@@ -143,6 +143,7 @@ sample code bearing this copyright.
#include "OneWire.h"
#include "util/OneWire_direct_gpio.h"
#pragma GCC diagnostic ignored "-Wunused-variable"
void OneWire::begin(uint8_t pin)
{
@@ -154,7 +155,6 @@ void OneWire::begin(uint8_t pin)
#endif
}
// Perform the onewire reset function. We will wait up to 250uS for
// the bus to come high, if it doesn't then it is broken or shorted
// and we return a 0;
@@ -578,3 +578,6 @@ uint16_t OneWire::crc16(const uint8_t* input, uint16_t len, uint16_t crc)
#endif
#endif
#pragma GCC diagnostic pop

View File

@@ -68,11 +68,28 @@ class FSPersistence {
JsonObject jsonObject = jsonDocument.to<JsonObject>();
_statefulService->read(jsonObject, _stateReader);
// make directories if required, for new IDF4.2 & LittleFS
String path(_filePath);
int index = 0;
while ((index = path.indexOf('/', index + 1)) != -1) {
String segment = path.substring(0, index);
if (!_fs->exists(segment)) {
_fs->mkdir(segment);
}
}
// serialize it to filesystem
File settingsFile = _fs->open(_filePath, "w");
// failed to open file, return false
if (!settingsFile) {
#if defined(EMSESP_DEBUG)
#if defined(EMSESP_USE_SERIAL)
Serial.println();
Serial.printf("Cannot write to file system.");
Serial.println();
#endif
#endif
return false;
}

View File

@@ -104,7 +104,7 @@ void MqttSettingsService::WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) {
case ARDUINO_EVENT_ETH_GOT_IP6:
case ARDUINO_EVENT_WIFI_STA_GOT_IP6:
if (_state.enabled) {
// emsesp::EMSESP::logger().info(F("Network connection found, starting MQTT client"));
// emsesp::EMSESP::logger().info(F("IPv4 Network connection found, starting MQTT client"));
onConfigUpdated();
}
break;

View File

@@ -50,6 +50,7 @@ void NTPSettingsService::configureNTP() {
emsesp::EMSESP::system_.ntp_connected(false);
if (connected_ && _state.enabled) {
emsesp::EMSESP::logger().info(F("Starting NTP"));
sntp_set_sync_interval(3600000); // onehour
sntp_set_time_sync_notification_cb(ntp_received);
configTzTime(_state.tzFormat.c_str(), _state.server.c_str());
} else {

View File

@@ -24,7 +24,7 @@ void SystemStatus::systemStatus(AsyncWebServerRequest * request) {
root["flash_chip_size"] = ESP.getFlashChipSize();
root["flash_chip_speed"] = ESP.getFlashChipSpeed();
root["fs_total"] = LittleFS.totalBytes();
root["fs_total"] = emsesp::EMSESP::system_.FStotal();
root["fs_used"] = LittleFS.usedBytes();
root["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);

View File

@@ -26,6 +26,7 @@
class DummySettings {
public:
String locale = "en";
uint8_t tx_mode = 1;
uint8_t ems_bus_id = 0x0B;
bool syslog_enabled = false;

View File

@@ -24,6 +24,8 @@ class FSPersistence {
}
void readFromFS() {
Serial.print("Fake reading file ");
Serial.println(_filePath);
applyDefaults();
}

View File

@@ -253,7 +253,7 @@ const UPLOAD_FILE_ENDPOINT = REST_ENDPOINT_ROOT + 'uploadFile'
const SIGN_IN_ENDPOINT = REST_ENDPOINT_ROOT + 'signIn'
const GENERATE_TOKEN_ENDPOINT = REST_ENDPOINT_ROOT + 'generateToken'
const system_status = {
emsesp_version: '3.4demo',
emsesp_version: '3.5demo',
esp_platform: 'ESP32',
max_alloc_heap: 113792,
psram_size: 0,
@@ -302,11 +302,12 @@ const EMSESP_BOARDPROFILE_ENDPOINT = REST_ENDPOINT_ROOT + 'boardProfile'
const EMSESP_WRITE_VALUE_ENDPOINT = REST_ENDPOINT_ROOT + 'writeValue'
const EMSESP_WRITE_SENSOR_ENDPOINT = REST_ENDPOINT_ROOT + 'writeSensor'
const EMSESP_WRITE_ANALOG_ENDPOINT = REST_ENDPOINT_ROOT + 'writeAnalog'
const EMSESP_MASKED_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'maskedEntities'
const EMSESP_CUSTOM_ENTITIES_ENDPOINT = REST_ENDPOINT_ROOT + 'customEntities'
const EMSESP_RESET_CUSTOMIZATIONS_ENDPOINT = REST_ENDPOINT_ROOT + 'resetCustomizations'
settings = {
tx_mode: 1,
locale: 'en',
tx_mode: 4,
ems_bus_id: 11,
syslog_enabled: false,
syslog_level: 3,
@@ -344,29 +345,24 @@ const emsesp_devices = {
devices: [
{
i: 1,
d: 23,
p: 77,
s: 'Thermostat (RC20/Moduline 300)',
t: 'thermostat1',
t: 'thermostat',
},
{
i: 2,
d: 8,
p: 123,
s: 'Boiler (Nefit GBx72/Trendline/Cerapur/Greenstar Si/27i)',
t: 'boiler',
},
{
i: 4,
d: 16,
p: 165,
s: 'Thermostat (RC100/Moduline 1000/1010)',
t: 'thermostat2',
t: 'thermostat',
},
],
}
const emsesp_coredata = {
connected: true,
// devices: [],
devices: [
{
@@ -429,13 +425,13 @@ const status = {
num_sensors: 1,
num_analogs: 1,
stats: [
{ id: 'EMS Telegrams Received (Rx)', s: 56506, f: 11, q: 100 },
{ id: 'EMS Reads (Tx)', s: 9026, f: 0, q: 100 },
{ id: 'EMS Writes (Tx)', s: 33, f: 2, q: 95 },
{ id: 'Temperature Sensor Reads', s: 56506, f: 11, q: 100 },
{ id: 'Analog Sensor Reads', s: 0, f: 0, q: 100 },
{ id: 'MQTT Publishes', s: 12, f: 10, q: 20 },
{ id: 'API Calls', s: 0, f: 0, q: 0 },
{ id: '0', s: 56506, f: 11, q: 100 },
{ id: '1', s: 9026, f: 0, q: 100 },
{ id: '2', s: 33, f: 2, q: 95 },
{ id: '3', s: 56506, f: 11, q: 100 },
{ id: '4', s: 0, f: 0, q: 100 },
{ id: '5', s: 12, f: 10, q: 20 },
{ id: '6', s: 0, f: 0, q: 0 },
],
}
@@ -446,7 +442,7 @@ const emsesp_devicedata_1 = {
{
v: '(0)',
u: 0,
id: '00error code',
id: '08my custom error code',
},
{
v: '14:54:39 06/06/2021',
@@ -584,8 +580,9 @@ const emsesp_deviceentities_1 = [
{
v: '(0)',
n: 'error code',
cn: 'my custom error code',
id: 'errorcode',
m: 0,
m: 8,
w: false,
},
{
@@ -602,19 +599,13 @@ const emsesp_deviceentities_1 = [
// m: 0,
// w: false,
// },
{
v: 'roomTemp',
id: 'hc1/HA climate config creation',
m: 0,
w: false,
},
{
v: 18.2,
n: 'hc1 selected room temperature',
id: 'hc1/seltemp',
m: 0,
w: true,
},
// {
// v: 18.2,
// n: 'hc1 selected room temperature',
// id: 'hc1/seltemp',
// m: 0,
// w: true,
// },
{
v: 22.6,
n: 'hc1 current room temperature',
@@ -852,6 +843,7 @@ rest_server.post(UPLOAD_FILE_ENDPOINT, (req, res) => {
res.sendStatus(200)
})
rest_server.post(SIGN_IN_ENDPOINT, (req, res) => {
console.log('Signed in as ' + req.body.username)
res.json(signin)
})
rest_server.get(GENERATE_TOKEN_ENDPOINT, (req, res) => {
@@ -931,29 +923,64 @@ rest_server.post(EMSESP_DEVICEENTITIES_ENDPOINT, (req, res) => {
})
function updateMask(entity, de, dd) {
const shortname = entity.slice(2)
const new_mask = parseInt(entity.slice(0, 2), 16)
const current_mask = parseInt(entity.slice(0, 2), 16)
const shortname_with_customname = entity.slice(2)
const shortname = shortname_with_customname.split('|')[0]
const new_custom_name = shortname_with_customname.split('|')[1]
// find in de
de_objIndex = de.findIndex((obj) => obj.id === shortname)
if (de_objIndex !== -1) {
if (de[de_objIndex].cn) {
fullname = de[de_objIndex].cn
} else {
fullname = de[de_objIndex].n
}
// find in dd, either looking for fullname or custom name
dd_objIndex = dd.data.findIndex((obj) => obj.id.slice(2) === fullname)
if (dd_objIndex !== -1) {
let changed = new Boolean(false)
objIndex = de.findIndex((obj) => obj.id == shortname)
if (objIndex !== -1) {
de[objIndex].m = new_mask
const fullname = de[objIndex].n
objIndex = dd.data.findIndex((obj) => obj.id.slice(2) == fullname)
if (objIndex !== -1) {
// see if the mask has changed
const old_mask = parseInt(dd.data[objIndex].id.slice(0, 2), 16)
if (old_mask !== new_mask) {
const mask_hex = entity.slice(0, 2)
console.log('Updating ' + dd.data[objIndex].id + ' -> ' + mask_hex + fullname)
dd.data[objIndex].id = mask_hex + fullname
const old_mask = parseInt(dd.data[dd_objIndex].id.slice(0, 2), 16)
if (old_mask !== current_mask) {
changed = true
console.log('mask has changed to ' + current_mask.toString(16))
}
// see if the custom name has changed
const old_custom_name = dd.data[dd_objIndex].cn
if (old_custom_name !== new_custom_name) {
changed = true
new_fullname = new_custom_name
console.log('name has changed to ' + new_custom_name)
} else {
new_fullname = fullname
}
if (changed) {
console.log(
'Updating ' + dd.data[dd_objIndex].id + ' -> ' + current_mask.toString(16).padStart(2, '0') + new_fullname,
)
de[de_objIndex].m = current_mask
de[de_objIndex].cn = new_fullname
dd.data[dd_objIndex].id = current_mask.toString(16).padStart(2, '0') + new_fullname
}
console.log('new dd:')
console.log(dd.data[dd_objIndex])
console.log('new de:')
console.log(de[de_objIndex])
} else {
console.log('error, dd not found')
}
} else {
console.log("can't locate record for name " + shortname)
console.log("can't locate record for shortname " + shortname)
}
}
rest_server.post(EMSESP_MASKED_ENTITIES_ENDPOINT, (req, res) => {
rest_server.post(EMSESP_CUSTOM_ENTITIES_ENDPOINT, (req, res) => {
const id = req.body.id
console.log('customization id = ' + id)
console.log(req.body.entity_ids)
@@ -1135,7 +1162,7 @@ rest_server.post(EMSESP_BOARDPROFILE_ENDPOINT, (req, res) => {
// EMS-ESP API specific
const emsesp_info = {
System: {
version: '3.4.2',
version: '3.5.0',
uptime: '001+06:40:34.018',
'uptime (seconds)': 110434,
freemem: 131,

View File

@@ -8,16 +8,27 @@
; my_build_flags = -DEMSESP_DEBUG -DEMSESP_USE_SERIAL
; my_build_flags = -DEMSESP_DEBUG -DCORE_DEBUG_LEVEL=5 ; 5=verbose, 4=debug, 3=info
[env:esp32]
[env:esp32_4M]
; if using OTA enter your details below
; upload_protocol = espota
; upload_flags =
; --port=8266
; --auth=ems-esp-neo
; upload_port = ems-esp.local
; upload_protocol = espota
; upload_flags =
; --port=8266
; --auth=ems-esp-neo
; upload_port = ems-esp.local
; for USB use
upload_port = /dev/ttyUSB*
; upload_port = COM3
extra_scripts =
pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
; pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
scripts/rename_fw.py
[env:esp32_16M]
upload_port = /dev/ttyUSB*
; upload_port = COM3
extra_scripts =
; pre:scripts/build_interface.py ; comment out if you don't want to re-build the WebUI each time
scripts/rename_fw.py
; pio run -e debug

View File

@@ -2,7 +2,8 @@
; override any settings with your own local ones in pio_local.ini
[platformio]
default_envs = esp32
default_envs = esp32_4M
; default_envs = esp32_16M
extra_configs =
factory_settings.ini
@@ -37,9 +38,11 @@ unbuild_flags =
[env]
framework = arduino
monitor_speed = 115200
monitor_raw = yes
upload_speed = 921600
build_type = release
lib_ldf_mode = chain+
debug_build_flags = -Os # optimize for size
check_tool = cppcheck, clangtidy
check_severity = high, medium
@@ -53,16 +56,28 @@ check_flags =
extra_scripts = scripts/rename_fw.py
board = esp32dev
platform = espressif32
board_build.partitions = esp32_partition_app1984k_spiffs64k.csv
board_build.partitions = esp32_partition_4M.csv
build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags}
[env:esp32]
[env:esp32_4M]
extra_scripts =
pre:scripts/build_interface.py
scripts/rename_fw.py
board = esp32dev
platform = espressif32
board_build.partitions = esp32_partition_app1984k_spiffs64k.csv
board_upload.flash_size = 4MB
board_build.partitions = esp32_partition_4M.csv
build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags}
[env:esp32_16M]
extra_scripts =
pre:scripts/build_interface.py
scripts/rename_fw.py
board = esp32dev
platform = espressif32
board_upload.flash_size = 16MB
board_build.partitions = esp32_partition_16M.csv
build_flags = ${common.build_flags}
build_unflags = ${common.unbuild_flags}

View File

@@ -5,7 +5,6 @@ Import("env")
OUTPUT_DIR = "build{}".format(os.path.sep)
def bin_copy(source, target, env):
# get the build info
@@ -21,20 +20,23 @@ def bin_copy(source, target, env):
app_version = bag.get('app_version')
platform = "ESP32"
flash_size = env["PIOENV"].split('_')[1]
# print(env.Dump())
# my_flags = env.ParseFlags(env['BUILD_FLAGS'])
# defines = {k: v for (k, v) in my_flags.get("CPPDEFINES")}
# print(my_flags)
# print((my_flags.get("CPPDEFINES"))
# alternatively take platfrom from the pio target
# alternatively take platform from the pio target
# platform = str(target[0]).split(os.path.sep)[2]
print("app version: "+app_version)
print("platform: "+platform)
print("flash size: "+flash_size)
# convert . to _ so Windows doesn't complain
variant = "EMS-ESP-" + app_version.replace(".", "_") + "-" + platform
variant = "EMS-ESP-" + app_version.replace(".", "_") + "-" + platform + "_" + flash_size
# check if output directories exist and create if necessary
if not os.path.isdir(OUTPUT_DIR):
@@ -52,10 +54,9 @@ def bin_copy(source, target, env):
if os.path.isfile(f):
os.remove(f)
print("renaming file to "+bin_file)
print("Renaming file to "+bin_file)
# copy firmware.bin to firmware/<variant>.bin
shutil.copy(str(target[0]), bin_file)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_copy])

0
scripts/run_sonar.sh Executable file → Normal file
View File

View File

@@ -1,7 +1,7 @@
sonar.organization=emsesp
sonar.projectKey=emsesp_EMS-ESP32
sonar.projectName=EMS-ESP32
sonar.projectVersion=3.4
sonar.projectVersion=3.5.0
sonar.sources=./src
sonar.cfamily.build-wrapper-output=build_wrapper_output_directory
sonar.sourceEncoding=UTF-8

View File

@@ -42,7 +42,7 @@ void AnalogSensor::start() {
EMSdevice::DeviceType::ANALOGSENSOR,
F_(setvalue),
[&](const char * value, const int8_t id) { return command_setvalue(value, id); },
F("set io value"),
F("set io value"), // TODO this needs translating
CommandFlag::ADMIN_ONLY);
Command::add(
EMSdevice::DeviceType::ANALOGSENSOR,
@@ -60,7 +60,10 @@ void AnalogSensor::reload() {
#if defined(EMSESP_STANDALONE)
analog_enabled_ = true; // for local offline testing
#endif
if (!analog_enabled_) {
sensors_.clear();
return;
}
// load the list of analog sensors from the customization service
// and store them locally and then activate them
EMSESP::webCustomizationService.read([&](WebCustomization & settings) {
@@ -459,7 +462,7 @@ bool AnalogSensor::get_value_info(JsonObject & output, const char * cmd, const i
output["gpio"] = sensor.gpio();
output["name"] = sensor.name();
output["type"] = F_(number);
output["analog"] = FL_(enum_sensortype)[sensor.type()];
output["analog"] = FL_(list_sensortype)[sensor.type()];
output["uom"] = EMSdevice::uom_to_string(sensor.uom());
output["offset"] = sensor.offset();
output["factor"] = sensor.factor();
@@ -497,7 +500,7 @@ bool AnalogSensor::command_info(const char * value, const int8_t id, JsonObject
JsonObject dataSensor = output.createNestedObject(sensor.name());
dataSensor["gpio"] = sensor.gpio();
dataSensor["type"] = F_(number);
dataSensor["analog"] = FL_(enum_sensortype)[sensor.type()];
dataSensor["analog"] = FL_(list_sensortype)[sensor.type()];
if (sensor.type() == AnalogType::ADC) {
dataSensor["uom"] = EMSdevice::uom_to_string(sensor.uom());
dataSensor["offset"] = sensor.offset();

View File

@@ -306,7 +306,7 @@ void Command::add(const uint8_t device_type, const __FlashStringHelper * cmd, co
}
// if the description is empty, it's hidden which means it will not show up in Web API or Console as an available command
if (description == nullptr) {
if (!description) {
flags |= CommandFlag::HIDDEN;
}
@@ -424,7 +424,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
shell.print(EMSdevice::tag_to_string(DeviceValueTAG::TAG_DEVICE_DATA_WW));
shell.print(' ');
}
shell.print(read_flash_string(cf.description_));
shell.print(read_flash_string(cf.description_).c_str());
if (!cf.has_flags(CommandFlag::ADMIN_ONLY)) {
shell.print(' ');
shell.print(COLOR_BRIGHT_RED);

64
src/common.h Normal file
View File

@@ -0,0 +1,64 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_COMMON_H
#define EMSESP_COMMON_H
// logging
#include <uuid/log.h>
using uuid::log::Level;
#define LOG_DEBUG(...) logger_.debug(__VA_ARGS__)
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
#define LOG_TRACE(...) logger_.trace(__VA_ARGS__)
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
// flash strings
using uuid::flash_string_vector;
using uuid::read_flash_string;
#ifdef FPSTR
#undef FPSTR
#endif
#define FJSON(x) x
// #define FJSON(x) F(x)
// clang-format off
#define MAKE_STR(string_name, string_literal) static constexpr const char * __str__##string_name = string_literal;
#define FPSTR(pstr_pointer) (reinterpret_cast<const __FlashStringHelper *>(pstr_pointer))
#define F_(string_name) FPSTR(__pstr__##string_name)
#define MAKE_PSTR(string_name, string_literal) static const char __pstr__##string_name[] __attribute__((__aligned__(sizeof(uint32_t)))) PROGMEM = string_literal;
#define MAKE_PSTR_WORD(string_name) MAKE_PSTR(string_name, #string_name)
#define FL_(list_name) (__pstr__L_##list_name)
#define MAKE_PSTR_LIST(list_name, ...) static const __FlashStringHelper * const __pstr__L_##list_name[] PROGMEM = {__VA_ARGS__, nullptr};
#define MAKE_PSTR_ENUM(enum_name, ...) static const __FlashStringHelper * const * __pstr__L_##enum_name[] PROGMEM = {__VA_ARGS__, nullptr};
// clang-format on
// load translations
#include "locale_translations.h"
#include "locale_common.h"
#endif

View File

@@ -225,6 +225,7 @@ void EMSESPShell::add_console_commands() {
flash_string_vector{F_(set)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
EMSESP::webSettingsService.read([&](WebSettings & settings) {
shell.printfln(F("Language: %s"), settings.locale.c_str());
shell.printfln(F_(tx_mode_fmt), settings.tx_mode);
shell.printfln(F_(bus_id_fmt), settings.ems_bus_id);
shell.printfln(F_(board_profile_fmt), settings.board_profile.c_str());
@@ -280,18 +281,19 @@ void EMSESPShell::add_console_commands() {
// get raw/pretty
if (arguments[0] == read_flash_string(F_(raw))) {
EMSESP::watch(EMSESP::WATCH_RAW); // raw
} else if (arguments[0] == read_flash_string(F_(on))) {
} else if (arguments[0] == Helpers::translated_word(FL_(on), true) || arguments[0] == read_flash_string(FL_(on)[0])) {
EMSESP::watch(EMSESP::WATCH_ON); // on
} else if (arguments[0] == read_flash_string(F_(off))) {
} else if (arguments[0] == Helpers::translated_word(FL_(off), true) || arguments[0] == read_flash_string(FL_(off)[0])) {
EMSESP::watch(EMSESP::WATCH_OFF); // off
} else if (arguments[0] == read_flash_string(F_(unknown))) {
} else if (arguments[0] == Helpers::translated_word(FL_(unknown), true) || arguments[0] == read_flash_string(FL_(unknown)[0])) {
EMSESP::watch(EMSESP::WATCH_UNKNOWN); // unknown
watch_id = WATCH_ID_NONE;
} else {
watch_id = Helpers::hextoint(arguments[0].c_str());
if (watch_id && ((EMSESP::watch() == EMSESP::WATCH_OFF) || (EMSESP::watch() == EMSESP::WATCH_UNKNOWN))) {
if (watch_id > 0 && ((EMSESP::watch() == EMSESP::WATCH_OFF) || (EMSESP::watch() == EMSESP::WATCH_UNKNOWN))) {
EMSESP::watch(EMSESP::WATCH_ON); // on
} else if (!watch_id) {
} else if (watch_id == 0) {
EMSESP::watch(EMSESP::WATCH_OFF); // off
return;
}
}
@@ -302,6 +304,9 @@ void EMSESPShell::add_console_commands() {
}
EMSESP::watch_id(watch_id);
} else {
shell.printfln(F("Invalid: use watch raw|on|off|unknown|id [id]"));
return;
}
uint8_t watch = EMSESP::watch();

View File

@@ -26,36 +26,8 @@
#include "system.h"
#include "mqtt.h"
using uuid::flash_string_vector;
using uuid::read_flash_string;
using uuid::console::Commands;
using uuid::console::Shell;
using uuid::log::Level;
#define LOG_DEBUG(...) logger_.debug(__VA_ARGS__)
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
#define LOG_TRACE(...) logger_.trace(__VA_ARGS__)
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
// clang-format off
// strings stored 32 bit aligned on ESP8266/ESP32
#define MAKE_STR(string_name, string_literal) static constexpr const char * __str__##string_name = string_literal;
#define MAKE_PSTR(string_name, string_literal) static const char __pstr__##string_name[] __attribute__((__aligned__(sizeof(uint32_t)))) PROGMEM = string_literal;
#define MAKE_PSTR_WORD(string_name) MAKE_PSTR(string_name, #string_name)
#define F_(string_name) FPSTR(__pstr__##string_name)
#define FSTR_(string_name) __str__##string_name
#define MAKE_PSTR_LIST(list_name, ...) static const __FlashStringHelper * const __pstr__##list_name[] PROGMEM = {__VA_ARGS__, nullptr};
#define FL_(list_name) (__pstr__##list_name)
// clang-format on
// localizations
#include "locale_EN.h"
#ifdef LOCAL
#undef LOCAL
#endif
static constexpr uint32_t INVALID_PASSWORD_DELAY_MS = 2000;
@@ -64,19 +36,14 @@ namespace emsesp {
using LogLevel = ::uuid::log::Level;
using LogFacility = ::uuid::log::Facility;
enum CommandFlags : uint8_t {
USER = 0,
ADMIN = (1 << 0),
LOCAL = (1 << 1)
};
#ifdef LOCAL
#undef LOCAL
#endif
enum CommandFlags : uint8_t { USER = 0, ADMIN = (1 << 0), LOCAL = (1 << 1) };
enum ShellContext : uint8_t {
MAIN = 0,
SYSTEM,
};
class EMSESPShell : virtual public uuid::console::Shell {

View File

@@ -36,6 +36,7 @@ void DallasSensor::start() {
reload();
if (!dallas_gpio_) {
sensors_.clear();
return; // disabled if dallas gpio is 0
}
@@ -152,23 +153,23 @@ void DallasSensor::loop() {
if (sensor.internal_id() == get_id(addr)) {
t += sensor.offset();
if (t != sensor.temperature_c) {
sensor.temperature_c = t;
publish_sensor(sensor);
changed_ |= true;
}
sensor.temperature_c = t;
sensor.read = true;
found = true;
sensor.read = true;
found = true;
break;
}
}
// add new sensor. this will create the id string, empty name and offset
if (!found && (sensors_.size() < (MAX_SENSORS - 1))) {
sensors_.emplace_back(addr);
sensors_.back().temperature_c = t + sensors_.back().offset();
sensors_.back().read = true;
changed_ = true;
sensors_.back().read = true;
changed_ = true;
// look in the customization service for an optional alias or offset for that particular sensor
sensors_.back().apply_customization();
sensors_.back().temperature_c = t + sensors_.back().offset();
publish_sensor(sensors_.back()); // call publish single
// sort the sensors based on name
// std::sort(sensors_.begin(), sensors_.end(), [](const Sensor & a, const Sensor & b) { return a.name() < b.name(); });
@@ -363,10 +364,10 @@ bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject
JsonObject dataSensor = output.createNestedObject(sensor.name());
dataSensor["id"] = sensor.id();
if (Helpers::hasValue(sensor.temperature_c)) {
dataSensor["temp"] = Helpers::round2((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
dataSensor["temp"] = Helpers::transformNumFloat((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
}
} else if (Helpers::hasValue(sensor.temperature_c)) {
output[sensor.name()] = Helpers::round2((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
output[sensor.name()] = Helpers::transformNumFloat((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
}
}
@@ -392,11 +393,11 @@ bool DallasSensor::get_value_info(JsonObject & output, const char * cmd, const i
output["id"] = sensor.id();
output["name"] = sensor.name();
if (Helpers::hasValue(sensor.temperature_c)) {
output["value"] = Helpers::round2((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
output["value"] = Helpers::transformNumFloat((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
}
output["type"] = F_(number);
output["min"] = Helpers::round2(-55, 0, EMSESP::system_.fahrenheit() ? 2 : 0);
output["max"] = Helpers::round2(125, 0, EMSESP::system_.fahrenheit() ? 2 : 0);
output["min"] = Helpers::transformNumFloat(-55, 0, EMSESP::system_.fahrenheit() ? 2 : 0);
output["max"] = Helpers::transformNumFloat(125, 0, EMSESP::system_.fahrenheit() ? 2 : 0);
output["uom"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);
output["writeable"] = false;
// if we're filtering on an attribute, go find it
@@ -472,10 +473,10 @@ void DallasSensor::publish_values(const bool force) {
JsonObject dataSensor = doc.createNestedObject(sensor.id());
dataSensor["name"] = sensor.name();
if (has_value) {
dataSensor["temp"] = Helpers::round2((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
dataSensor["temp"] = Helpers::transformNumFloat((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
}
} else if (has_value) {
doc[sensor.name()] = Helpers::round2((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
doc[sensor.name()] = Helpers::transformNumFloat((float)(sensor.temperature_c), 10, EMSESP::system_.fahrenheit() ? 2 : 0);
}
// create the HA MQTT config

View File

@@ -15,11 +15,20 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_DEFAULT_SETTINGS_H
#define EMSESP_DEFAULT_SETTINGS_H
// GENERAL SETTINGS
#ifndef EMSESP_STANDALONE
#define EMSESP_DEFAULT_LOCALE EMSESP_LOCALE_EN // English
#else
// this is for debugging different languages in standalone version
#define EMSESP_DEFAULT_LOCALE EMSESP_LOCALE_DE // German
// #define EMSESP_DEFAULT_LOCALE EMSESP_LOCALE_EN // English
#endif
#ifndef EMSESP_DEFAULT_TX_MODE
#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0
#endif
@@ -199,19 +208,19 @@
// matches Web UI settings
enum {
BOOL_FORMAT_ONOFF_STR = 1,
BOOL_FORMAT_ONOFF_STR_CAP,
BOOL_FORMAT_TRUEFALSE_STR,
BOOL_FORMAT_TRUEFALSE,
BOOL_FORMAT_10_STR,
BOOL_FORMAT_10
BOOL_FORMAT_ONOFF_STR = 1, // 1
BOOL_FORMAT_ONOFF_STR_CAP, // 2
BOOL_FORMAT_TRUEFALSE_STR, // 3
BOOL_FORMAT_TRUEFALSE, // 4
BOOL_FORMAT_10_STR, // 5
BOOL_FORMAT_10 // 6
};
enum {
ENUM_FORMAT_VALUE = 1,
ENUM_FORMAT_INDEX // 2
ENUM_FORMAT_VALUE = 1, // 1
ENUM_FORMAT_INDEX // 2
};

View File

@@ -27,6 +27,7 @@ uuid::log::Logger Boiler::logger_{F_(boiler), uuid::log::Facility::CONSOLE};
Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const char * version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
// alternative heatsource special messages
if (device_id == EMSdevice::EMS_DEVICE_ID_AM200) {
register_telegram_type(0x54D, F("AmTemperatures"), false, MAKE_PF_CB(process_amTempMessage));
register_telegram_type(0x54E, F("AmStatus"), false, MAKE_PF_CB(process_amStatusMessage));
@@ -34,25 +35,43 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_telegram_type(0x550, F("AmExtra"), false, MAKE_PF_CB(process_amExtraMessage));
register_telegram_type(0x54C, F("AmSettings"), true, MAKE_PF_CB(process_amSettingMessage)); // not broadcasted
register_device_value(DeviceValueTAG::TAG_AHS, &curFlowTemp_, DeviceValueType::SHORT, FL_(div10), FL_(sysFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &retTemp_, DeviceValueType::SHORT, FL_(div10), FL_(sysRetTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &aFlowTemp_, DeviceValueType::SHORT, FL_(div10), FL_(aFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &aRetTemp_, DeviceValueType::SHORT, FL_(div10), FL_(aRetTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &cylTopTemp_, DeviceValueType::SHORT, FL_(div10), FL_(aCylTopTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &cylCenterTemp_, DeviceValueType::SHORT, FL_(div10), FL_(aCylCenterTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &cylBottomTemp_, DeviceValueType::SHORT, FL_(div10), FL_(aCylBottomTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS,
&curFlowTemp_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(sysFlowTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &retTemp_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(sysRetTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &aFlowTemp_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(aFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS, &aRetTemp_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(aRetTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS,
&cylTopTemp_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(aCylTopTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS,
&cylCenterTemp_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(aCylCenterTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_AHS,
&cylBottomTemp_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(aCylBottomTemp),
DeviceValueUOM::DEGREES);
// register_device_value(DeviceValueTAG::TAG_AHS, &valveByPass_, DeviceValueType::BOOL, nullptr, FL_(valveByPass), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_AHS, &valveBuffer_, DeviceValueType::UINT, nullptr, FL_(valveBuffer), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_AHS, &valveReturn_, DeviceValueType::UINT, nullptr, FL_(valveReturn), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_AHS, &aPumpMod_, DeviceValueType::UINT, nullptr, FL_(aPumpMod), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_AHS, &valveBuffer_, DeviceValueType::UINT, FL_(valveBuffer), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_AHS, &valveReturn_, DeviceValueType::UINT, FL_(valveReturn), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_AHS, &aPumpMod_, DeviceValueType::UINT, FL_(aPumpMod), DeviceValueUOM::PERCENT);
// register_device_value(DeviceValueTAG::TAG_AHS, &heatSource_, DeviceValueType::BOOL, nullptr, FL_(heatSource), DeviceValueUOM::NONE);
// Settings:
register_device_value(
DeviceValueTAG::TAG_AHS, &vr2Config_, DeviceValueType::ENUM, FL_(enum_vr2Config), FL_(vr2Config), DeviceValueUOM::NONE, MAKE_CF_CB(set_vr2Config));
register_device_value(
DeviceValueTAG::TAG_AHS, &ahsActivated_, DeviceValueType::BOOL, nullptr, FL_(ahsActivated), DeviceValueUOM::NONE, MAKE_CF_CB(set_ahsActivated));
register_device_value(
DeviceValueTAG::TAG_AHS, &aPumpConfig_, DeviceValueType::BOOL, nullptr, FL_(aPumpConfig), DeviceValueUOM::NONE, MAKE_CF_CB(set_aPumpConfig));
register_device_value(DeviceValueTAG::TAG_AHS, &ahsActivated_, DeviceValueType::BOOL, FL_(ahsActivated), DeviceValueUOM::NONE, MAKE_CF_CB(set_ahsActivated));
register_device_value(DeviceValueTAG::TAG_AHS, &aPumpConfig_, DeviceValueType::BOOL, FL_(aPumpConfig), DeviceValueUOM::NONE, MAKE_CF_CB(set_aPumpConfig));
register_device_value(DeviceValueTAG::TAG_AHS,
&aPumpSignal_,
DeviceValueType::ENUM,
@@ -60,35 +79,19 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
FL_(aPumpSignal),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_aPumpSignal));
register_device_value(DeviceValueTAG::TAG_AHS, &aPumpMin_, DeviceValueType::UINT, FL_(aPumpMin), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_aPumpMin), 12, 50);
register_device_value(DeviceValueTAG::TAG_AHS, &tempRise_, DeviceValueType::BOOL, FL_(tempRise), DeviceValueUOM::NONE, MAKE_CF_CB(set_tempRise));
register_device_value(
DeviceValueTAG::TAG_AHS, &aPumpMin_, DeviceValueType::UINT, nullptr, FL_(aPumpMin), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_aPumpMin), 12, 50);
register_device_value(DeviceValueTAG::TAG_AHS, &tempRise_, DeviceValueType::BOOL, nullptr, FL_(tempRise), DeviceValueUOM::NONE, MAKE_CF_CB(set_tempRise));
register_device_value(DeviceValueTAG::TAG_AHS,
&setReturnTemp_,
DeviceValueType::UINT,
nullptr,
FL_(setReturnTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_setReturnTemp),
40,
75);
DeviceValueTAG::TAG_AHS, &setReturnTemp_, DeviceValueType::UINT, FL_(setReturnTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_setReturnTemp), 40, 75);
register_device_value(
DeviceValueTAG::TAG_AHS, &mixRuntime_, DeviceValueType::USHORT, nullptr, FL_(mixRuntime), DeviceValueUOM::SECONDS, MAKE_CF_CB(set_mixRuntime), 0, 600);
register_device_value(DeviceValueTAG::TAG_AHS,
&setFlowTemp_,
DeviceValueType::UINT,
nullptr,
FL_(setFlowTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_setFlowTemp),
40,
75);
DeviceValueTAG::TAG_AHS, &mixRuntime_, DeviceValueType::USHORT, FL_(mixRuntime), DeviceValueUOM::SECONDS, MAKE_CF_CB(set_mixRuntime), 0, 600);
register_device_value(
DeviceValueTAG::TAG_AHS, &setFlowTemp_, DeviceValueType::UINT, FL_(setFlowTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_setFlowTemp), 40, 75);
register_device_value(
DeviceValueTAG::TAG_AHS, &bufBypass_, DeviceValueType::ENUM, FL_(enum_bufBypass), FL_(bufBypass), DeviceValueUOM::NONE, MAKE_CF_CB(set_bufBypass));
register_device_value(DeviceValueTAG::TAG_AHS,
&bufMixRuntime_,
DeviceValueType::USHORT,
nullptr,
FL_(bufMixRuntime),
DeviceValueUOM::SECONDS,
MAKE_CF_CB(set_bufMixRuntime),
@@ -100,33 +103,31 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
DeviceValueTAG::TAG_AHS, &blockMode_, DeviceValueType::ENUM, FL_(enum_blockMode), FL_(blockMode), DeviceValueUOM::NONE, MAKE_CF_CB(set_blockMode));
register_device_value(
DeviceValueTAG::TAG_AHS, &blockTerm_, DeviceValueType::ENUM, FL_(enum_blockTerm), FL_(blockTerm), DeviceValueUOM::NONE, MAKE_CF_CB(set_blockTerm));
register_device_value(DeviceValueTAG::TAG_AHS, &blockHyst_, DeviceValueType::INT, FL_(blockHyst), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_blockHyst), 0, 50);
register_device_value(
DeviceValueTAG::TAG_AHS, &blockHyst_, DeviceValueType::INT, nullptr, FL_(blockHyst), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_blockHyst), 0, 50);
register_device_value(DeviceValueTAG::TAG_AHS,
&releaseWait_,
DeviceValueType::UINT,
nullptr,
FL_(releaseWait),
DeviceValueUOM::MINUTES,
MAKE_CF_CB(set_releaseWait),
0,
240);
DeviceValueTAG::TAG_AHS, &releaseWait_, DeviceValueType::UINT, FL_(releaseWait), DeviceValueUOM::MINUTES, MAKE_CF_CB(set_releaseWait), 0, 240);
return;
}
// cascaded heatingsources, only some values per individual heatsource (hs)
// cascaded heating sources, only some values per individual heatsource (hs)
if (device_id >= EMSdevice::EMS_DEVICE_ID_BOILER_1) {
uint8_t hs = device_id - EMSdevice::EMS_DEVICE_ID_BOILER_1; // heating source id, count from 0
// Runtime of each heatingsource in 0x06DC, ff
register_telegram_type(0x6DC + hs, F("CascadeMessage"), false, MAKE_PF_CB(process_CascadeMessage));
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &burnWorkMin_, DeviceValueType::TIME, nullptr, FL_(burnWorkMin), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &burnWorkMin_, DeviceValueType::TIME, FL_(burnWorkMin), DeviceValueUOM::MINUTES);
// selBurnpower in D2 and E4
// register_telegram_type(0xD2, F("CascadePowerMessage"), false, MAKE_PF_CB(process_CascadePowerMessage));
// individual Flowtemps and powervalues for each heatingsource in E4
register_telegram_type(0xE4, F("UBAMonitorFastPlus"), false, MAKE_PF_CB(process_UBAMonitorFastPlus));
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &selFlowTemp_, DeviceValueType::UINT, nullptr, FL_(selFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &selBurnPow_, DeviceValueType::UINT, nullptr, FL_(selBurnPow), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &curFlowTemp_, DeviceValueType::USHORT, FL_(div10), FL_(curFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &curBurnPow_, DeviceValueType::UINT, nullptr, FL_(curBurnPow), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &selFlowTemp_, DeviceValueType::UINT, FL_(selFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &selBurnPow_, DeviceValueType::UINT, FL_(selBurnPow), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_HS1 + hs,
&curFlowTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(curFlowTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_HS1 + hs, &curBurnPow_, DeviceValueType::UINT, FL_(curBurnPow), DeviceValueUOM::PERCENT);
return;
}
@@ -142,15 +143,18 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_telegram_type(0x14, F("UBATotalUptime"), true, MAKE_PF_CB(process_UBATotalUptime));
register_telegram_type(0x15, F("UBAMaintenanceData"), false, MAKE_PF_CB(process_UBAMaintenanceData));
register_telegram_type(0x1C, F("UBAMaintenanceStatus"), false, MAKE_PF_CB(process_UBAMaintenanceStatus));
// EMS1.0 and maybe EMS+?
register_telegram_type(0x18, F("UBAMonitorFast"), false, MAKE_PF_CB(process_UBAMonitorFast));
register_telegram_type(0x19, F("UBAMonitorSlow"), false, MAKE_PF_CB(process_UBAMonitorSlow));
register_telegram_type(0x1A, F("UBASetPoints"), false, MAKE_PF_CB(process_UBASetPoints));
register_telegram_type(0x35, F("UBAFlags"), false, MAKE_PF_CB(process_UBAFlags));
// only EMS 1.0
register_telegram_type(0x16, F("UBAParameters"), true, MAKE_PF_CB(process_UBAParameters));
register_telegram_type(0x33, F("UBAParameterWW"), true, MAKE_PF_CB(process_UBAParameterWW));
register_telegram_type(0x34, F("UBAMonitorWW"), false, MAKE_PF_CB(process_UBAMonitorWW));
// not ems1.0, but HT3
if (model() != EMSdevice::EMS_DEVICE_FLAG_EMS) {
register_telegram_type(0x26, F("UBASettingsWW"), true, MAKE_PF_CB(process_UBASettingsWW));
@@ -185,93 +189,100 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_telegram_type(0xBB, F("HybridHp"), true, MAKE_PF_CB(process_HybridHp));
}
*/
// reset is a command uses a dummy variable which is always zero, shown as blank, but provides command enum options
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &reset_, DeviceValueType::CMD, FL_(enum_reset), FL_(reset), DeviceValueUOM::NONE, MAKE_CF_CB(set_reset));
has_update(reset_, 0);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingActive_, DeviceValueType::BOOL, nullptr, FL_(heatingActive), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &tapwaterActive_, DeviceValueType::BOOL, nullptr, FL_(tapwaterActive), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingActive_, DeviceValueType::BOOL, FL_(heatingActive), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &tapwaterActive_, DeviceValueType::BOOL, FL_(tapwaterActive), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &selFlowTemp_, DeviceValueType::UINT, FL_(selFlowTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_flow_temp));
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &selFlowTemp_, DeviceValueType::UINT, nullptr, FL_(selFlowTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_flow_temp));
DeviceValueTAG::TAG_DEVICE_DATA, &selBurnPow_, DeviceValueType::UINT, FL_(selBurnPow), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_burn_power), 0, 254);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingPumpMod_, DeviceValueType::UINT, FL_(heatingPumpMod), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingPump2Mod_, DeviceValueType::UINT, FL_(heatingPump2Mod), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&selBurnPow_,
DeviceValueType::UINT,
nullptr,
FL_(selBurnPow),
DeviceValueUOM::PERCENT,
MAKE_CF_CB(set_burn_power),
0,
254);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingPumpMod_, DeviceValueType::UINT, nullptr, FL_(heatingPumpMod), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingPump2Mod_, DeviceValueType::UINT, nullptr, FL_(heatingPump2Mod), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &outdoorTemp_, DeviceValueType::SHORT, FL_(div10), FL_(outdoorTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &curFlowTemp_, DeviceValueType::USHORT, FL_(div10), FL_(curFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &retTemp_, DeviceValueType::USHORT, FL_(div10), FL_(retTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &switchTemp_, DeviceValueType::USHORT, FL_(div10), FL_(switchTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &sysPress_, DeviceValueType::UINT, FL_(div10), FL_(sysPress), DeviceValueUOM::BAR);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &boilTemp_, DeviceValueType::USHORT, FL_(div10), FL_(boilTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &exhaustTemp_, DeviceValueType::USHORT, FL_(div10), FL_(exhaustTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnGas_, DeviceValueType::BOOL, nullptr, FL_(burnGas), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnGas2_, DeviceValueType::BOOL, nullptr, FL_(burnGas2), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &flameCurr_, DeviceValueType::USHORT, FL_(div10), FL_(flameCurr), DeviceValueUOM::UA);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingPump_, DeviceValueType::BOOL, nullptr, FL_(heatingPump), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &fanWork_, DeviceValueType::BOOL, nullptr, FL_(fanWork), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &ignWork_, DeviceValueType::BOOL, nullptr, FL_(ignWork), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &oilPreHeat_, DeviceValueType::BOOL, nullptr, FL_(oilPreHeat), DeviceValueUOM::NONE);
&outdoorTemp_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(outdoorTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&curFlowTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(curFlowTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &retTemp_, DeviceValueType::USHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(retTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&switchTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(switchTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &sysPress_, DeviceValueType::UINT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(sysPress), DeviceValueUOM::BAR);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&boilTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(boilTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&exhaustTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(exhaustTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnGas_, DeviceValueType::BOOL, FL_(burnGas), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnGas2_, DeviceValueType::BOOL, FL_(burnGas2), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &flameCurr_, DeviceValueType::USHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(flameCurr), DeviceValueUOM::UA);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingPump_, DeviceValueType::BOOL, FL_(heatingPump), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &fanWork_, DeviceValueType::BOOL, FL_(fanWork), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &ignWork_, DeviceValueType::BOOL, FL_(ignWork), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &oilPreHeat_, DeviceValueType::BOOL, FL_(oilPreHeat), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&heatingActivated_,
DeviceValueType::BOOL,
nullptr,
FL_(heatingActivated),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_heating_activated));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&heatingTemp_,
DeviceValueType::UINT,
nullptr,
FL_(heatingTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_heating_temp));
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &pumpModMax_, DeviceValueType::UINT, nullptr, FL_(pumpModMax), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_max_pump));
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &pumpModMin_, DeviceValueType::UINT, nullptr, FL_(pumpModMin), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_min_pump));
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &pumpDelay_, DeviceValueType::UINT, nullptr, FL_(pumpDelay), DeviceValueUOM::MINUTES, MAKE_CF_CB(set_pump_delay));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &pumpModMax_, DeviceValueType::UINT, FL_(pumpModMax), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_max_pump));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &pumpModMin_, DeviceValueType::UINT, FL_(pumpModMin), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_min_pump));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &pumpDelay_, DeviceValueType::UINT, FL_(pumpDelay), DeviceValueUOM::MINUTES, MAKE_CF_CB(set_pump_delay));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&burnMinPeriod_,
DeviceValueType::UINT,
nullptr,
FL_(burnMinPeriod),
DeviceValueUOM::MINUTES,
MAKE_CF_CB(set_burn_period));
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &burnMinPower_, DeviceValueType::UINT, nullptr, FL_(burnMinPower), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_min_power));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&burnMaxPower_,
&burnMinPower_,
DeviceValueType::UINT,
nullptr,
FL_(burnMaxPower),
FL_(burnMinPower),
DeviceValueUOM::PERCENT,
MAKE_CF_CB(set_max_power),
0,
254);
MAKE_CF_CB(set_min_power));
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &boilHystOn_, DeviceValueType::INT, nullptr, FL_(boilHystOn), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_hyst_on));
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &boilHystOff_, DeviceValueType::INT, nullptr, FL_(boilHystOff), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_hyst_off));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &setFlowTemp_, DeviceValueType::UINT, nullptr, FL_(setFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &setBurnPow_, DeviceValueType::UINT, nullptr, FL_(setBurnPow), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &curBurnPow_, DeviceValueType::UINT, nullptr, FL_(curBurnPow), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnStarts_, DeviceValueType::ULONG, nullptr, FL_(burnStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnWorkMin_, DeviceValueType::TIME, nullptr, FL_(burnWorkMin), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burn2WorkMin_, DeviceValueType::TIME, nullptr, FL_(burn2WorkMin), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatWorkMin_, DeviceValueType::TIME, nullptr, FL_(heatWorkMin), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &UBAuptime_, DeviceValueType::TIME, nullptr, FL_(UBAuptime), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &lastCode_, DeviceValueType::STRING, nullptr, FL_(lastCode), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &serviceCode_, DeviceValueType::STRING, nullptr, FL_(serviceCode), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &serviceCodeNumber_, DeviceValueType::USHORT, nullptr, FL_(serviceCodeNumber), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &maintenanceMessage_, DeviceValueType::STRING, nullptr, FL_(maintenanceMessage), DeviceValueUOM::NONE);
DeviceValueTAG::TAG_DEVICE_DATA, &burnMaxPower_, DeviceValueType::UINT, FL_(burnMaxPower), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_max_power), 0, 254);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &boilHystOn_, DeviceValueType::INT, FL_(boilHystOn), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_hyst_on));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &boilHystOff_, DeviceValueType::INT, FL_(boilHystOff), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_hyst_off));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &setFlowTemp_, DeviceValueType::UINT, FL_(setFlowTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &setBurnPow_, DeviceValueType::UINT, FL_(setBurnPow), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &curBurnPow_, DeviceValueType::UINT, FL_(curBurnPow), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnStarts_, DeviceValueType::ULONG, FL_(burnStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burnWorkMin_, DeviceValueType::TIME, FL_(burnWorkMin), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &burn2WorkMin_, DeviceValueType::TIME, FL_(burn2WorkMin), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatWorkMin_, DeviceValueType::TIME, FL_(heatWorkMin), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &UBAuptime_, DeviceValueType::TIME, FL_(UBAuptime), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &lastCode_, DeviceValueType::STRING, FL_(lastCode), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &serviceCode_, DeviceValueType::STRING, FL_(serviceCode), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &serviceCodeNumber_, DeviceValueType::USHORT, FL_(serviceCodeNumber), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &maintenanceMessage_, DeviceValueType::STRING, FL_(maintenanceMessage), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&maintenanceType_,
DeviceValueType::ENUM,
@@ -282,7 +293,6 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&maintenanceTime_,
DeviceValueType::USHORT,
nullptr,
FL_(maintenanceTime),
DeviceValueUOM::HOURS,
MAKE_CF_CB(set_maintenancetime));
@@ -296,14 +306,12 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&emergencyOps_,
DeviceValueType::BOOL,
nullptr,
FL_(emergencyOps),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_emergency_ops));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&emergencyTemp_,
DeviceValueType::UINT,
nullptr,
FL_(emergencyTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_emergency_temp),
@@ -336,7 +344,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&energyCostRatio_,
DeviceValueType::UINT,
FL_(div10),
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(energyCostRatio),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_energyCostRatio),
@@ -345,7 +353,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&fossileFactor_,
DeviceValueType::UINT,
FL_(div10),
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(fossileFactor),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_fossileFactor),
@@ -354,7 +362,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&electricFactor_,
DeviceValueType::UINT,
FL_(div10),
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(electricFactor),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_electricFactor),
@@ -382,81 +390,110 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
*/
// heatpump info
if (model() == EMS_DEVICE_FLAG_HEATPUMP) {
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &upTimeControl_, DeviceValueType::TIME, FL_(div60), FL_(upTimeControl), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &upTimeCompHeating_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompHeating), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &upTimeCompCooling_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompCooling), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &upTimeCompWw_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompWw), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &upTimeCompPool_, DeviceValueType::TIME, FL_(div60), FL_(upTimeCompPool), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &totalCompStarts_, DeviceValueType::ULONG, nullptr, FL_(totalCompStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingStarts_, DeviceValueType::ULONG, nullptr, FL_(heatingStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &coolingStarts_, DeviceValueType::ULONG, nullptr, FL_(coolingStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwStarts2_, DeviceValueType::ULONG, nullptr, FL_(wwStarts2), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &poolStarts_, DeviceValueType::ULONG, nullptr, FL_(poolStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsTotal_, DeviceValueType::ULONG, nullptr, FL_(nrgConsTotal), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompTotal_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompTotal), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompHeating_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompHeating), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompWw_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompWw), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompCooling_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompCooling), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompPool_, DeviceValueType::ULONG, nullptr, FL_(nrgConsCompPool), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&auxElecHeatNrgConsTotal_,
DeviceValueType::ULONG,
nullptr,
FL_(auxElecHeatNrgConsTotal),
DeviceValueUOM::KWH);
&upTimeControl_,
DeviceValueType::TIME,
DeviceValueNumOp::DV_NUMOP_DIV60,
FL_(upTimeControl),
DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&upTimeCompHeating_,
DeviceValueType::TIME,
DeviceValueNumOp::DV_NUMOP_DIV60,
FL_(upTimeCompHeating),
DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&upTimeCompCooling_,
DeviceValueType::TIME,
DeviceValueNumOp::DV_NUMOP_DIV60,
FL_(upTimeCompCooling),
DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&upTimeCompWw_,
DeviceValueType::TIME,
DeviceValueNumOp::DV_NUMOP_DIV60,
FL_(upTimeCompWw),
DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&upTimeCompPool_,
DeviceValueType::TIME,
DeviceValueNumOp::DV_NUMOP_DIV60,
FL_(upTimeCompPool),
DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &totalCompStarts_, DeviceValueType::ULONG, FL_(totalCompStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingStarts_, DeviceValueType::ULONG, FL_(heatingStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &coolingStarts_, DeviceValueType::ULONG, FL_(coolingStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwStarts2_, DeviceValueType::ULONG, FL_(wwStarts2), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &poolStarts_, DeviceValueType::ULONG, FL_(poolStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsTotal_, DeviceValueType::ULONG, FL_(nrgConsTotal), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompTotal_, DeviceValueType::ULONG, FL_(nrgConsCompTotal), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompHeating_, DeviceValueType::ULONG, FL_(nrgConsCompHeating), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompWw_, DeviceValueType::ULONG, FL_(nrgConsCompWw), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompCooling_, DeviceValueType::ULONG, FL_(nrgConsCompCooling), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgConsCompPool_, DeviceValueType::ULONG, FL_(nrgConsCompPool), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &auxElecHeatNrgConsTotal_, DeviceValueType::ULONG, FL_(auxElecHeatNrgConsTotal), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&auxElecHeatNrgConsHeating_,
DeviceValueType::ULONG,
nullptr,
FL_(auxElecHeatNrgConsHeating),
DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&auxElecHeatNrgConsWW_,
DeviceValueType::ULONG,
nullptr,
FL_(auxElecHeatNrgConsWW),
DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&auxElecHeatNrgConsPool_,
DeviceValueType::ULONG,
nullptr,
FL_(auxElecHeatNrgConsPool),
DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppTotal_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppTotal), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppHeating_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppHeating), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppWw_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppWw), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppCooling_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppCooling), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppPool_, DeviceValueType::ULONG, nullptr, FL_(nrgSuppPool), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPower_, DeviceValueType::UINT, FL_(div10), FL_(hpPower), DeviceValueUOM::KW);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCompOn_, DeviceValueType::BOOL, nullptr, FL_(hpCompOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &auxElecHeatNrgConsWW_, DeviceValueType::ULONG, FL_(auxElecHeatNrgConsWW), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &auxElecHeatNrgConsPool_, DeviceValueType::ULONG, FL_(auxElecHeatNrgConsPool), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppTotal_, DeviceValueType::ULONG, FL_(nrgSuppTotal), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppHeating_, DeviceValueType::ULONG, FL_(nrgSuppHeating), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppWw_, DeviceValueType::ULONG, FL_(nrgSuppWw), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppCooling_, DeviceValueType::ULONG, FL_(nrgSuppCooling), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &nrgSuppPool_, DeviceValueType::ULONG, FL_(nrgSuppPool), DeviceValueUOM::KWH);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPower_, DeviceValueType::UINT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpPower), DeviceValueUOM::KW);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCompOn_, DeviceValueType::BOOL, FL_(hpCompOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpActivity_, DeviceValueType::ENUM, FL_(enum_hpactivity), FL_(hpActivity), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpHeatingOn_, DeviceValueType::BOOL, nullptr, FL_(hpHeatingOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCoolingOn_, DeviceValueType::BOOL, nullptr, FL_(hpCoolingOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpWwOn_, DeviceValueType::BOOL, nullptr, FL_(hpWwOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPoolOn_, DeviceValueType::BOOL, nullptr, FL_(hpPoolOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpBrinePumpSpd_, DeviceValueType::UINT, nullptr, FL_(hpBrinePumpSpd), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpSwitchValve_, DeviceValueType::BOOL, nullptr, FL_(hpSwitchValve), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCompSpd_, DeviceValueType::UINT, nullptr, FL_(hpCompSpd), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCircSpd_, DeviceValueType::UINT, nullptr, FL_(hpCircSpd), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpBrineIn_, DeviceValueType::SHORT, FL_(div10), FL_(hpBrineIn), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpBrineOut_, DeviceValueType::SHORT, FL_(div10), FL_(hpBrineOut), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpSuctionGas_, DeviceValueType::SHORT, FL_(div10), FL_(hpSuctionGas), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpHotGas_, DeviceValueType::SHORT, FL_(div10), FL_(hpHotGas), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTc0_, DeviceValueType::SHORT, FL_(div10), FL_(hpTc0), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTc1_, DeviceValueType::SHORT, FL_(div10), FL_(hpTc1), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTc3_, DeviceValueType::SHORT, FL_(div10), FL_(hpTc3), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr3_, DeviceValueType::SHORT, FL_(div10), FL_(hpTr3), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr4_, DeviceValueType::SHORT, FL_(div10), FL_(hpTr4), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr5_, DeviceValueType::SHORT, FL_(div10), FL_(hpTr5), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr6_, DeviceValueType::SHORT, FL_(div10), FL_(hpTr6), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr7_, DeviceValueType::SHORT, FL_(div10), FL_(hpTr7), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTl2_, DeviceValueType::SHORT, FL_(div10), FL_(hpTl2), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPl1_, DeviceValueType::SHORT, FL_(div10), FL_(hpPl1), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPh1_, DeviceValueType::SHORT, FL_(div10), FL_(hpPh1), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpHeatingOn_, DeviceValueType::BOOL, FL_(hpHeatingOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCoolingOn_, DeviceValueType::BOOL, FL_(hpCoolingOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpWwOn_, DeviceValueType::BOOL, FL_(hpWwOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPoolOn_, DeviceValueType::BOOL, FL_(hpPoolOn), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpBrinePumpSpd_, DeviceValueType::UINT, FL_(hpBrinePumpSpd), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpSwitchValve_, DeviceValueType::BOOL, FL_(hpSwitchValve), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCompSpd_, DeviceValueType::UINT, FL_(hpCompSpd), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpCircSpd_, DeviceValueType::UINT, FL_(hpCircSpd), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&hpBrineIn_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(hpBrineIn),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&hpBrineOut_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(hpBrineOut),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&hpSuctionGas_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(hpSuctionGas),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&hpHotGas_,
DeviceValueType::SHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(hpHotGas),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTc0_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTc0), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTc1_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTc1), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTc3_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTc3), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr3_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTr3), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr4_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTr4), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr5_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTr5), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr6_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTr6), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTr7_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTr7), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpTl2_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpTl2), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPl1_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpPl1), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hpPh1_, DeviceValueType::SHORT, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(hpPh1), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&poolSetTemp_,
DeviceValueType::UINT,
FL_(div2),
DeviceValueNumOp::DV_NUMOP_DIV2,
FL_(poolSetTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_pool_temp));
@@ -466,25 +503,21 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwTapActivated_,
DeviceValueType::BOOL,
nullptr,
FL_(wwtapactivated),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_tapwarmwater_activated));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSetTemp_, DeviceValueType::UINT, nullptr, FL_(wwSetTemp), DeviceValueUOM::DEGREES);
register_device_value(
DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSelTemp_, DeviceValueType::UINT, nullptr, FL_(wwSelTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_temp));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSetTemp_, DeviceValueType::UINT, FL_(wwSetTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSelTemp_, DeviceValueType::UINT, FL_(wwSelTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_temp));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwSelTempLow_,
DeviceValueType::UINT,
nullptr,
FL_(wwSelTempLow),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_ww_temp_low));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSelTempOff_, DeviceValueType::UINT, nullptr, FL_(wwSelTempOff), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSelTempOff_, DeviceValueType::UINT, FL_(wwSelTempOff), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwSelTempSingle_,
DeviceValueType::UINT,
nullptr,
FL_(wwSelTempSingle),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_ww_temp_single));
@@ -506,7 +539,6 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwFlowTempOffset_,
DeviceValueType::UINT,
nullptr,
FL_(wwFlowTempOffset),
DeviceValueUOM::DEGREES_R,
MAKE_CF_CB(set_ww_flowTempOffset),
@@ -515,44 +547,30 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwChargeOptimization_,
DeviceValueType::BOOL,
nullptr,
FL_(wwChargeOptimization),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_ww_chargeOptimization));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwMaxPower_,
DeviceValueType::UINT,
nullptr,
FL_(wwMaxPower),
DeviceValueUOM::PERCENT,
MAKE_CF_CB(set_ww_maxpower),
0,
254);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwMaxTemp_,
DeviceValueType::UINT,
nullptr,
FL_(wwMaxTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_ww_maxtemp),
0,
70);
register_device_value(
DeviceValueTAG::TAG_BOILER_DATA_WW, &wwMaxPower_, DeviceValueType::UINT, FL_(wwMaxPower), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_ww_maxpower), 0, 254);
register_device_value(
DeviceValueTAG::TAG_BOILER_DATA_WW, &wwMaxTemp_, DeviceValueType::UINT, FL_(wwMaxTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ww_maxtemp), 0, 70);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwCircPump_,
DeviceValueType::BOOL,
nullptr,
FL_(wwCircPump),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_ww_circulation_pump));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwChargeType_, DeviceValueType::ENUM, FL_(enum_charge), FL_(wwChargeType), DeviceValueUOM::NONE);
register_device_value(
DeviceValueTAG::TAG_BOILER_DATA_WW, &wwHystOn_, DeviceValueType::INT, nullptr, FL_(wwHystOn), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_ww_hyst_on));
register_device_value(
DeviceValueTAG::TAG_BOILER_DATA_WW, &wwHystOff_, DeviceValueType::INT, nullptr, FL_(wwHystOff), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_ww_hyst_off));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwHystOn_, DeviceValueType::INT, FL_(wwHystOn), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_ww_hyst_on));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwHystOff_,
DeviceValueType::INT,
FL_(wwHystOff),
DeviceValueUOM::DEGREES_R,
MAKE_CF_CB(set_ww_hyst_off));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwDisinfectionTemp_,
DeviceValueType::UINT,
nullptr,
FL_(wwDisinfectionTemp),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_ww_disinfect_temp));
@@ -563,45 +581,76 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
FL_(wwCircMode),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_ww_circulation_mode));
register_device_value(
DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCirc_, DeviceValueType::BOOL, nullptr, FL_(wwCirc), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_circulation));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCurTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwCurTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCurTemp2_, DeviceValueType::USHORT, FL_(div10), FL_(wwCurTemp2), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCurFlow_, DeviceValueType::UINT, FL_(div10), FL_(wwCurFlow), DeviceValueUOM::LMIN);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwStorageTemp1_, DeviceValueType::USHORT, FL_(div10), FL_(wwStorageTemp1), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwStorageTemp2_, DeviceValueType::USHORT, FL_(div10), FL_(wwStorageTemp2), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCirc_, DeviceValueType::BOOL, FL_(wwCirc), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_circulation));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwCurTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(wwCurTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwCurTemp2_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(wwCurTemp2),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwCurFlow_,
DeviceValueType::UINT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(wwCurFlow),
DeviceValueUOM::LMIN);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwStorageTemp1_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(wwStorageTemp1),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwStorageTemp2_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(wwStorageTemp2),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwActivated_,
DeviceValueType::BOOL,
nullptr,
FL_(wwActivated),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_ww_activated));
register_device_value(
DeviceValueTAG::TAG_BOILER_DATA_WW, &wwOneTime_, DeviceValueType::BOOL, nullptr, FL_(wwOneTime), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_onetime));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwOneTime_, DeviceValueType::BOOL, FL_(wwOneTime), DeviceValueUOM::NONE, MAKE_CF_CB(set_ww_onetime));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwDisinfect_,
DeviceValueType::BOOL,
nullptr,
FL_(wwDisinfecting),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_ww_disinfect));
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCharging_, DeviceValueType::BOOL, nullptr, FL_(wwCharging), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwRecharging_, DeviceValueType::BOOL, nullptr, FL_(wwRecharging), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwTempOK_, DeviceValueType::BOOL, nullptr, FL_(wwTempOK), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwActive_, DeviceValueType::BOOL, nullptr, FL_(wwActive), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &ww3wayValve_, DeviceValueType::BOOL, nullptr, FL_(ww3wayValve), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSetPumpPower_, DeviceValueType::UINT, nullptr, FL_(wwSetPumpPower), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwMixerTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwMixerTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCylMiddleTemp_, DeviceValueType::USHORT, FL_(div10), FL_(wwCylMiddleTemp), DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwStarts_, DeviceValueType::ULONG, nullptr, FL_(wwStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwWorkM_, DeviceValueType::TIME, nullptr, FL_(wwWorkM), DeviceValueUOM::MINUTES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwCharging_, DeviceValueType::BOOL, FL_(wwCharging), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwRecharging_, DeviceValueType::BOOL, FL_(wwRecharging), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwTempOK_, DeviceValueType::BOOL, FL_(wwTempOK), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwActive_, DeviceValueType::BOOL, FL_(wwActive), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &ww3wayValve_, DeviceValueType::BOOL, FL_(ww3wayValve), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwSetPumpPower_, DeviceValueType::UINT, FL_(wwSetPumpPower), DeviceValueUOM::PERCENT);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwMixerTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(wwMixerTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW,
&wwCylMiddleTemp_,
DeviceValueType::USHORT,
DeviceValueNumOp::DV_NUMOP_DIV10,
FL_(wwCylMiddleTemp),
DeviceValueUOM::DEGREES);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwStarts_, DeviceValueType::ULONG, FL_(wwStarts), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_BOILER_DATA_WW, &wwWorkM_, DeviceValueType::TIME, FL_(wwWorkM), DeviceValueUOM::MINUTES);
// fetch some initial data
EMSESP::send_read_request(0x10, device_id); // read last errorcode on start (only published on errors)
EMSESP::send_read_request(0x11, device_id); // read last errorcode on start (only published on errors)
EMSESP::send_read_request(0x15, device_id); // read maintenace data on start (only published on change)
EMSESP::send_read_request(0x1C, device_id); // read maintenace status on start (only published on change)
EMSESP::send_read_request(0x15, device_id); // read maintenance data on start (only published on change)
EMSESP::send_read_request(0x1C, device_id); // read maintenance status on start (only published on change)
EMSESP::send_read_request(0xC2, device_id); // read last errorcode on start (only published on errors)
}
@@ -1286,9 +1335,6 @@ void Boiler::process_amCommandMessage(std::shared_ptr<const Telegram> telegram)
// pos 6: boiler blocking 0-off, 1-on
}
// 0x0550 AM200 broadcasted message, all 27 bytes unkown
// Rx: 60 00 FF 00 04 50 00 FF 00 FF FF 00 0D 00 01 00 00 00 00 01 03 01 00 03 00 2D 19 C8 02 94 00 4A
// Rx: 60 00 FF 19 04 50 00 FF FF 39
void Boiler::process_amExtraMessage(std::shared_ptr<const Telegram> telegram) {
}
@@ -1458,6 +1504,11 @@ bool Boiler::set_releaseWait(const char * value, const int8_t id) {
return true;
}
// 0x0550 AM200 broadcasted message, all 27 bytes unkown
// Rx: 60 00 FF 00 04 50 00 FF 00 FF FF 00 0D 00 01 00 00 00 00 01 03 01 00 03 00 2D 19 C8 02 94 00 4A
// Rx: 60 00 FF 19 04 50 00 FF FF 39
/*
* Hybrid heatpump with telegram 0xBB is readable and writeable in boiler and thermostat
* thermostat always overwrites settings in boiler
@@ -2079,7 +2130,7 @@ bool Boiler::set_maintenance(const char * value, const int8_t id) {
std::string s;
if (Helpers::value2string(value, s)) {
if (s == Helpers::toLower(read_flash_string(F_(reset)))) {
if (s == Helpers::translated_word(FL_(reset))) {
// LOG_INFO(F("Reset boiler maintenance message"));
write_command(0x05, 0x08, 0xFF, 0x1C);
return true;

View File

@@ -227,15 +227,11 @@ class Boiler : public EMSdevice {
int8_t blockHyst_; // pos 14?: Hyst. for bolier block (K)
uint8_t releaseWait_; // pos 15: Boiler release wait time (min)
/*
* Hybrid heatpump with telegram 0xBB is readable and writeable in boiler and thermostat
* thermostat always overwrites settings in boiler
* enable settings here if no thermostat is used in system
*
// HybridHP
// Hybrid heatpump with telegram 0xBB is readable and writeable in boiler and thermostat
// thermostat always overwrites settings in boiler
//enable settings here if no thermostat is used in system
// HybridHP
uint8_t hybridStrategy_; // cost = 2, temperature = 3, mix = 4
int8_t switchOverTemp_; // degrees
uint8_t energyCostRatio_; // is *10
@@ -243,7 +239,7 @@ class Boiler : public EMSdevice {
uint8_t electricFactor_; // is * 10
uint8_t delayBoiler_; // minutes
uint8_t tempDiffBoiler_; // relative temperature degrees
*/
*/
void process_UBAParameterWW(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram);

Some files were not shown because too many files have changed in this diff Show More