formatting

This commit is contained in:
proddy
2024-04-21 15:10:22 +02:00
parent befa487482
commit ac39a46442
100 changed files with 2778 additions and 798 deletions

View File

@@ -4,7 +4,7 @@
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"printWidth": 120, "printWidth": 85,
"bracketSpacing": true, "bracketSpacing": true,
"importOrder": ["^react", "^@mui/(.*)$", "^api*/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"], "importOrder": ["^react", "^@mui/(.*)$", "^api*/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true, "importOrderSeparation": true,

View File

@@ -1,5 +1,11 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { createWriteStream, existsSync, readFileSync, readdirSync, unlinkSync } from 'fs'; import {
createWriteStream,
existsSync,
readFileSync,
readdirSync,
unlinkSync
} from 'fs';
import mime from 'mime-types'; import mime from 'mime-types';
import { relative, resolve, sep } from 'path'; import { relative, resolve, sep } from 'path';
import zlib from 'zlib'; import zlib from 'zlib';
@@ -18,12 +24,7 @@ const generateWWWClass = () =>
class WWWData { class WWWData {
${indent}public: ${indent}public:
${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) { ${indent.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo ${fileInfo.map((file) => `${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`).join('\n')}
.map(
(file) =>
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${file.variable}, ${file.size}, "${file.hash}");`
)
.join('\n')}
${indent.repeat(2)}} ${indent.repeat(2)}}
}; };
`; `;

View File

@@ -12,7 +12,8 @@
local('Roboto'), local('Roboto'),
local('Roboto-Regular'), local('Roboto-Regular'),
url(../fonts/re.woff2) format('woff2'); url(../fonts/re.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131, U+0141-0144, U+0152-0153, U+015A-015B, unicode-range: U+0000-00FF, U+0104-0107, U+0118-0119, U+011E-011F, U+0130-0131,
U+015E-015F, U+0179-017C, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+0141-0144, U+0152-0153, U+015A-015B, U+015E-015F, U+0179-017C, U+02BB-02BC,
U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD; U+2212, U+2215, U+FEFF, U+FFFD;
} }

View File

@@ -44,8 +44,14 @@ const AppRouting: FC = () => {
<Authentication> <Authentication>
<RemoveTrailingSlashes /> <RemoveTrailingSlashes />
<Routes> <Routes>
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} /> <Route
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />} /> path="/unauthorized"
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
/>
<Route
path="/fileUpdated"
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
/>
<Route <Route
path="/" path="/"
element={ element={

View File

@@ -45,7 +45,10 @@ const AuthenticatedRouting: FC = () => {
<Route path="/settings/mqtt/*" element={<Mqtt />} /> <Route path="/settings/mqtt/*" element={<Mqtt />} />
<Route path="/settings/ota/*" element={<OTASettings />} /> <Route path="/settings/ota/*" element={<OTASettings />} />
<Route path="/settings/security/*" element={<Security />} /> <Route path="/settings/security/*" element={<Security />} />
<Route path="/settings/espsystemstatus/*" element={<ESPSystemStatus />} /> <Route
path="/settings/espsystemstatus/*"
element={<ESPSystemStatus />}
/>
<Route path="/settings/upload/*" element={<UploadDownload />} /> <Route path="/settings/upload/*" element={<UploadDownload />} />
</> </>
)} )}

View File

@@ -1,7 +1,11 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { CssBaseline } from '@mui/material'; import { CssBaseline } from '@mui/material';
import { ThemeProvider, createTheme, responsiveFontSizes } from '@mui/material/styles'; import {
ThemeProvider,
createTheme,
responsiveFontSizes
} from '@mui/material/styles';
import type { RequiredChildrenProps } from 'utils'; import type { RequiredChildrenProps } from 'utils';

View File

@@ -41,9 +41,12 @@ const SignIn: FC = () => {
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const { send: callSignIn, onSuccess } = useRequest((request: SignInRequest) => AuthenticationApi.signIn(request), { const { send: callSignIn, onSuccess } = useRequest(
immediate: false (request: SignInRequest) => AuthenticationApi.signIn(request),
}); {
immediate: false
}
);
onSuccess((response) => { onSuccess((response) => {
if (response.data) { if (response.data) {
@@ -80,7 +83,9 @@ const SignIn: FC = () => {
const submitOnEnter = onEnterCallback(signIn); const submitOnEnter = onEnterCallback(signIn);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => { const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
target
}) => {
const loc = target.value as Locales; const loc = target.value as Locales;
localStorage.setItem('lang', loc); localStorage.setItem('lang', loc);
await loadLocaleAsync(loc); await loadLocaleAsync(loc);
@@ -110,7 +115,14 @@ const SignIn: FC = () => {
> >
<Typography variant="h4">{PROJECT_NAME}</Typography> <Typography variant="h4">{PROJECT_NAME}</Typography>
<TextField name="locale" variant="outlined" value={locale} onChange={onLocaleSelected} size="small" select> <TextField
name="locale"
variant="outlined"
value={locale}
onChange={onLocaleSelected}
size="small"
select
>
<MenuItem key="de" value="de"> <MenuItem key="de" value="de">
<img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} /> <img src={DEflag} style={{ width: 16, verticalAlign: 'middle' }} />
&nbsp;DE &nbsp;DE
@@ -182,7 +194,13 @@ const SignIn: FC = () => {
/> />
</Box> </Box>
<Button variant="contained" color="primary" sx={{ mt: 2 }} onClick={validateAndSignIn} disabled={processing}> <Button
variant="contained"
color="primary"
sx={{ mt: 2 }}
onClick={validateAndSignIn}
disabled={processing}
>
<ForwardIcon sx={{ mr: 1 }} /> <ForwardIcon sx={{ mr: 1 }} />
{LL.SIGN_IN()} {LL.SIGN_IN()}
</Button> </Button>

View File

@@ -3,5 +3,7 @@ import type { APSettingsType, APStatusType } from 'types';
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus'); export const readAPStatus = () => alovaInstance.Get<APStatusType>('/rest/apStatus');
export const readAPSettings = () => alovaInstance.Get<APSettingsType>('/rest/apSettings'); export const readAPSettings = () =>
export const updateAPSettings = (data: APSettingsType) => alovaInstance.Post<APSettingsType>('/rest/apSettings', data); alovaInstance.Get<APSettingsType>('/rest/apSettings');
export const updateAPSettings = (data: APSettingsType) =>
alovaInstance.Post<APSettingsType>('/rest/apSettings', data);

View File

@@ -9,8 +9,10 @@ import { ACCESS_TOKEN, alovaInstance } from './endpoints';
export const SIGN_IN_PATHNAME = 'loginPathname'; export const SIGN_IN_PATHNAME = 'loginPathname';
export const SIGN_IN_SEARCH = 'loginSearch'; export const SIGN_IN_SEARCH = 'loginSearch';
export const verifyAuthorization = () => alovaInstance.Get('/rest/verifyAuthorization'); export const verifyAuthorization = () =>
export const signIn = (request: SignInRequest) => alovaInstance.Post<SignInResponse>('/rest/signIn', request); alovaInstance.Get('/rest/verifyAuthorization');
export const signIn = (request: SignInRequest) =>
alovaInstance.Post<SignInResponse>('/rest/signIn', request);
export function getStorage() { export function getStorage() {
return localStorage || sessionStorage; return localStorage || sessionStorage;

View File

@@ -19,7 +19,8 @@ export const alovaInstance = createAlova({
requestAdapter: xhrRequestAdapter(), requestAdapter: xhrRequestAdapter(),
beforeRequest(method) { beforeRequest(method) {
if (localStorage.getItem(ACCESS_TOKEN)) { if (localStorage.getItem(ACCESS_TOKEN)) {
method.config.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN); method.config.headers.Authorization =
'Bearer ' + localStorage.getItem(ACCESS_TOKEN);
} }
}, },

View File

@@ -2,7 +2,9 @@ import type { MqttSettingsType, MqttStatusType } from 'types';
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
export const readMqttStatus = () => alovaInstance.Get<MqttStatusType>('/rest/mqttStatus'); export const readMqttStatus = () =>
export const readMqttSettings = () => alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings'); alovaInstance.Get<MqttStatusType>('/rest/mqttStatus');
export const readMqttSettings = () =>
alovaInstance.Get<MqttSettingsType>('/rest/mqttSettings');
export const updateMqttSettings = (data: MqttSettingsType) => export const updateMqttSettings = (data: MqttSettingsType) =>
alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data); alovaInstance.Post<MqttSettingsType>('/rest/mqttSettings', data);

View File

@@ -2,7 +2,8 @@ import type { NetworkSettingsType, NetworkStatusType, WiFiNetworkList } from 'ty
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
export const readNetworkStatus = () => alovaInstance.Get<NetworkStatusType>('/rest/networkStatus'); export const readNetworkStatus = () =>
alovaInstance.Get<NetworkStatusType>('/rest/networkStatus');
export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks'); export const scanNetworks = () => alovaInstance.Get('/rest/scanNetworks');
export const listNetworks = () => export const listNetworks = () =>
alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', { alovaInstance.Get<WiFiNetworkList>('/rest/listNetworks', {
@@ -10,6 +11,8 @@ export const listNetworks = () =>
timeout: 20000 // timeout 20 seconds timeout: 20000 // timeout 20 seconds
}); });
export const readNetworkSettings = () => export const readNetworkSettings = () =>
alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings', { name: 'networkSettings' }); alovaInstance.Get<NetworkSettingsType>('/rest/networkSettings', {
name: 'networkSettings'
});
export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) => export const updateNetworkSettings = (wifiSettings: NetworkSettingsType) =>
alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings); alovaInstance.Post<NetworkSettingsType>('/rest/networkSettings', wifiSettings);

View File

@@ -2,7 +2,8 @@ import type { NTPSettingsType, NTPStatusType, Time } from 'types';
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
export const readNTPStatus = () => alovaInstance.Get<NTPStatusType>('/rest/ntpStatus'); export const readNTPStatus = () =>
alovaInstance.Get<NTPStatusType>('/rest/ntpStatus');
export const readNTPSettings = () => export const readNTPSettings = () =>
alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', { alovaInstance.Get<NTPSettingsType>('/rest/ntpSettings', {
name: 'ntpSettings' name: 'ntpSettings'
@@ -10,4 +11,5 @@ export const readNTPSettings = () =>
export const updateNTPSettings = (data: NTPSettingsType) => export const updateNTPSettings = (data: NTPSettingsType) =>
alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data); alovaInstance.Post<NTPSettingsType>('/rest/ntpSettings', data);
export const updateTime = (data: Time) => alovaInstance.Post<Time>('/rest/time', data); export const updateTime = (data: Time) =>
alovaInstance.Post<Time>('/rest/time', data);

View File

@@ -2,7 +2,8 @@ import type { SecuritySettingsType, Token } from 'types';
import { alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
export const readSecuritySettings = () => alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings'); export const readSecuritySettings = () =>
alovaInstance.Get<SecuritySettingsType>('/rest/securitySettings');
export const updateSecuritySettings = (securitySettings: SecuritySettingsType) => export const updateSecuritySettings = (securitySettings: SecuritySettingsType) =>
alovaInstance.Post('/rest/securitySettings', securitySettings); alovaInstance.Post('/rest/securitySettings', securitySettings);

View File

@@ -8,10 +8,12 @@ import type { ESPSystemStatus, LogSettings, OTASettings, SystemStatus } from 'ty
import { alovaInstance, alovaInstanceGH } from './endpoints'; import { alovaInstance, alovaInstanceGH } from './endpoints';
// ESPSystemStatus - also used to ping in Restart monitor for pinging // ESPSystemStatus - also used to ping in Restart monitor for pinging
export const readESPSystemStatus = () => alovaInstance.Get<ESPSystemStatus>('/rest/ESPSystemStatus'); export const readESPSystemStatus = () =>
alovaInstance.Get<ESPSystemStatus>('/rest/ESPSystemStatus');
// SystemStatus // SystemStatus
export const readSystemStatus = () => alovaInstance.Get<SystemStatus>('/rest/systemStatus'); export const readSystemStatus = () =>
alovaInstance.Get<SystemStatus>('/rest/systemStatus');
// commands // commands
export const restart = () => alovaInstance.Post('/rest/restart'); export const restart = () => alovaInstance.Post('/rest/restart');
@@ -19,12 +21,16 @@ export const partition = () => alovaInstance.Post('/rest/partition');
export const factoryReset = () => alovaInstance.Post('/rest/factoryReset'); export const factoryReset = () => alovaInstance.Post('/rest/factoryReset');
// OTA // OTA
export const readOTASettings = () => alovaInstance.Get<OTASettings>(`/rest/otaSettings`); export const readOTASettings = () =>
export const updateOTASettings = (data: OTASettings) => alovaInstance.Post('/rest/otaSettings', data); alovaInstance.Get<OTASettings>(`/rest/otaSettings`);
export const updateOTASettings = (data: OTASettings) =>
alovaInstance.Post('/rest/otaSettings', data);
// SystemLog // SystemLog
export const readLogSettings = () => alovaInstance.Get<LogSettings>(`/rest/logSettings`); export const readLogSettings = () =>
export const updateLogSettings = (data: LogSettings) => alovaInstance.Post('/rest/logSettings', data); alovaInstance.Get<LogSettings>(`/rest/logSettings`);
export const updateLogSettings = (data: LogSettings) =>
alovaInstance.Post('/rest/logSettings', data);
export const fetchLog = () => alovaInstance.Post('/rest/fetchLog'); export const fetchLog = () => alovaInstance.Post('/rest/fetchLog');
export const fetchLogES = () => alovaInstance.Get('/es/log'); export const fetchLogES = () => alovaInstance.Get('/es/log');
@@ -47,6 +53,6 @@ export const uploadFile = (file: File) => {
formData.append('file', file); formData.append('file', file);
return alovaInstance.Post('/rest/uploadFile', formData, { return alovaInstance.Post('/rest/uploadFile', formData, {
timeout: 60000, // override timeout for uploading firmware - 1 minute timeout: 60000, // override timeout for uploading firmware - 1 minute
enableUpload: true enableUpload: true // can be removed with Alova 2.20+
}); });
}; };

View File

@@ -38,7 +38,8 @@ try {
export class Unpackr { export class Unpackr {
constructor(options) { constructor(options) {
if (options) { if (options) {
if (options.useRecords === false && options.mapsAsObjects === undefined) options.mapsAsObjects = true; if (options.useRecords === false && options.mapsAsObjects === undefined)
options.mapsAsObjects = true;
if (options.sequential && options.trusted !== false) { if (options.sequential && options.trusted !== false) {
options.trusted = true; options.trusted = true;
if (!options.structures && options.useRecords != false) { if (!options.structures && options.useRecords != false) {
@@ -46,7 +47,8 @@ export class Unpackr {
if (!options.maxSharedStructures) options.maxSharedStructures = 0; if (!options.maxSharedStructures) options.maxSharedStructures = 0;
} }
} }
if (options.structures) options.structures.sharedLength = options.structures.length; if (options.structures)
options.structures.sharedLength = options.structures.length;
else if (options.getStructures) { else if (options.getStructures) {
(options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures (options.structures = []).uninitialized = true; // this is what we use to denote an uninitialized structures
options.structures.sharedLength = 0; options.structures.sharedLength = 0;
@@ -63,11 +65,14 @@ export class Unpackr {
// re-entrant execution, save the state and restore it after we do this unpack // re-entrant execution, save the state and restore it after we do this unpack
return saveState(() => { return saveState(() => {
clearSource(); clearSource();
return this ? this.unpack(source, options) : Unpackr.prototype.unpack.call(defaultOptions, source, options); return this
? this.unpack(source, options)
: Unpackr.prototype.unpack.call(defaultOptions, source, options);
}); });
} }
if (!source.buffer && source.constructor === ArrayBuffer) if (!source.buffer && source.constructor === ArrayBuffer)
source = typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source); source =
typeof Buffer !== 'undefined' ? Buffer.from(source) : new Uint8Array(source);
if (typeof options === 'object') { if (typeof options === 'object') {
srcEnd = options.end || source.length; srcEnd = options.end || source.length;
position = options.start || 0; position = options.start || 0;
@@ -86,14 +91,21 @@ export class Unpackr {
// new ones // new ones
try { try {
dataView = dataView =
source.dataView || (source.dataView = new DataView(source.buffer, source.byteOffset, source.byteLength)); source.dataView ||
(source.dataView = new DataView(
source.buffer,
source.byteOffset,
source.byteLength
));
} catch (error) { } catch (error) {
// if it doesn't have a buffer, maybe it is the wrong type of object // if it doesn't have a buffer, maybe it is the wrong type of object
src = null; src = null;
if (source instanceof Uint8Array) throw error; if (source instanceof Uint8Array) throw error;
throw new Error( throw new Error(
'Source must be a Uint8Array or Buffer but was a ' + 'Source must be a Uint8Array or Buffer but was a ' +
(source && typeof source == 'object' ? source.constructor.name : typeof source) (source && typeof source == 'object'
? source.constructor.name
: typeof source)
); );
} }
if (this instanceof Unpackr) { if (this instanceof Unpackr) {
@@ -117,7 +129,9 @@ export class Unpackr {
try { try {
sequentialMode = true; sequentialMode = true;
const size = source.length; const size = source.length;
const value = this ? this.unpack(source, size) : defaultUnpackr.unpack(source, size); const value = this
? this.unpack(source, size)
: defaultUnpackr.unpack(source, size);
if (forEach) { if (forEach) {
if (forEach(value) === false) return; if (forEach(value) === false) return;
while (position < size) { while (position < size) {
@@ -145,9 +159,11 @@ export class Unpackr {
} }
_mergeStructures(loadedStructures, existingStructures) { _mergeStructures(loadedStructures, existingStructures) {
if (onLoadedStructures) loadedStructures = onLoadedStructures.call(this, loadedStructures); if (onLoadedStructures)
loadedStructures = onLoadedStructures.call(this, loadedStructures);
loadedStructures = loadedStructures || []; loadedStructures = loadedStructures || [];
if (Object.isFrozen(loadedStructures)) loadedStructures = loadedStructures.map((structure) => structure.slice(0)); if (Object.isFrozen(loadedStructures))
loadedStructures = loadedStructures.map((structure) => structure.slice(0));
for (let i = 0, l = loadedStructures.length; i < l; i++) { for (let i = 0, l = loadedStructures.length; i < l; i++) {
const structure = loadedStructures[i]; const structure = loadedStructures[i];
if (structure) { if (structure) {
@@ -162,7 +178,8 @@ export class Unpackr {
const existing = existingStructures[id]; const existing = existingStructures[id];
if (existing) { if (existing) {
if (structure) if (structure)
(loadedStructures.restoreStructures || (loadedStructures.restoreStructures = []))[id] = structure; (loadedStructures.restoreStructures ||
(loadedStructures.restoreStructures = []))[id] = structure;
loadedStructures[id] = existing; loadedStructures[id] = existing;
} }
} }
@@ -181,10 +198,16 @@ export function checkedRead(options: any) {
try { try {
if (!currentUnpackr.trusted && !sequentialMode) { if (!currentUnpackr.trusted && !sequentialMode) {
const sharedLength = currentStructures.sharedLength || 0; const sharedLength = currentStructures.sharedLength || 0;
if (sharedLength < currentStructures.length) currentStructures.length = sharedLength; if (sharedLength < currentStructures.length)
currentStructures.length = sharedLength;
} }
let result; let result;
if (currentUnpackr.randomAccessStructure && src[position] < 0x40 && src[position] >= 0x20 && readStruct) { if (
currentUnpackr.randomAccessStructure &&
src[position] < 0x40 &&
src[position] >= 0x20 &&
readStruct
) {
result = readStruct(src, position, srcEnd, currentUnpackr); result = readStruct(src, position, srcEnd, currentUnpackr);
src = null; // dispose of this so that recursive unpack calls don't save state src = null; // dispose of this so that recursive unpack calls don't save state
if (!(options && options.lazy) && result) result = result.toJSON(); if (!(options && options.lazy) && result) result = result.toJSON();
@@ -198,7 +221,8 @@ export function checkedRead(options: any) {
if (position == srcEnd) { if (position == srcEnd) {
// finished reading this source, cleanup references // finished reading this source, cleanup references
if (currentStructures && currentStructures.restoreStructures) restoreStructures(); if (currentStructures && currentStructures.restoreStructures)
restoreStructures();
currentStructures = null; currentStructures = null;
src = null; src = null;
if (referenceMap) referenceMap = null; if (referenceMap) referenceMap = null;
@@ -208,10 +232,9 @@ export function checkedRead(options: any) {
} else if (!sequentialMode) { } else if (!sequentialMode) {
let jsonView; let jsonView;
try { try {
jsonView = JSON.stringify(result, (_, value) => (typeof value === 'bigint' ? `${value}n` : value)).slice( jsonView = JSON.stringify(result, (_, value) =>
0, typeof value === 'bigint' ? `${value}n` : value
100 ).slice(0, 100);
);
} catch (error) { } catch (error) {
jsonView = '(JSON view not available ' + error + ')'; jsonView = '(JSON view not available ' + error + ')';
} }
@@ -220,9 +243,14 @@ export function checkedRead(options: any) {
// else more to read, but we are reading sequentially, so don't clear source yet // else more to read, but we are reading sequentially, so don't clear source yet
return result; return result;
} catch (error) { } catch (error) {
if (currentStructures && currentStructures.restoreStructures) restoreStructures(); if (currentStructures && currentStructures.restoreStructures)
restoreStructures();
clearSource(); clearSource();
if (error instanceof RangeError || error.message.startsWith('Unexpected end of buffer') || position > srcEnd) { if (
error instanceof RangeError ||
error.message.startsWith('Unexpected end of buffer') ||
position > srcEnd
) {
error.incomplete = true; error.incomplete = true;
} }
throw error; throw error;
@@ -243,7 +271,8 @@ export function read() {
if (token < 0x40) return token; if (token < 0x40) return token;
else { else {
const structure = const structure =
currentStructures[token & 0x3f] || (currentUnpackr.getStructures && loadStructures()[token & 0x3f]); currentStructures[token & 0x3f] ||
(currentUnpackr.getStructures && loadStructures()[token & 0x3f]);
if (structure) { if (structure) {
if (!structure.read) { if (!structure.read) {
structure.read = createStructureReader(structure, token & 0x3f); structure.read = createStructureReader(structure, token & 0x3f);
@@ -282,7 +311,10 @@ export function read() {
// fixstr // fixstr
const length = token - 0xa0; const length = token - 0xa0;
if (srcStringEnd >= position) { if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart); return srcString.slice(
position - srcStringStart,
(position += length) - srcStringStart
);
} }
if (srcStringEnd == 0 && srcEnd < 140) { if (srcStringEnd == 0 && srcEnd < 140) {
// for small blocks, avoiding the overhead of the extract call is helpful // for small blocks, avoiding the overhead of the extract call is helpful
@@ -298,8 +330,16 @@ export function read() {
case 0xc1: case 0xc1:
if (bundledStrings) { if (bundledStrings) {
value = read(); // followed by the length of the string in characters (not bytes!) value = read(); // followed by the length of the string in characters (not bytes!)
if (value > 0) return bundledStrings[1].slice(bundledStrings.position1, (bundledStrings.position1 += value)); if (value > 0)
else return bundledStrings[0].slice(bundledStrings.position0, (bundledStrings.position0 -= value)); return bundledStrings[1].slice(
bundledStrings.position1,
(bundledStrings.position1 += value)
);
else
return bundledStrings[0].slice(
bundledStrings.position0,
(bundledStrings.position0 -= value)
);
} }
return C1; // "never-used", return special object to denote that return C1; // "never-used", return special object to denote that
case 0xc2: case 0xc2:
@@ -338,7 +378,8 @@ export function read() {
value = dataView.getFloat32(position); value = dataView.getFloat32(position);
if (currentUnpackr.useFloat32 > 2) { if (currentUnpackr.useFloat32 > 2) {
// this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved // this does rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
const multiplier = mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)]; const multiplier =
mult10[((src[position] & 0x7f) << 1) | (src[position + 1] >> 7)];
position += 4; position += 4;
return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier; return ((multiplier * value + (value > 0 ? 0.5 : -0.5)) >> 0) / multiplier;
} }
@@ -391,7 +432,8 @@ export function read() {
value = dataView.getBigInt64(position).toString(); value = dataView.getBigInt64(position).toString();
} else if (currentUnpackr.int64AsType === 'auto') { } else if (currentUnpackr.int64AsType === 'auto') {
value = dataView.getBigInt64(position); value = dataView.getBigInt64(position);
if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52)) value = Number(value); if (value >= BigInt(-2) << BigInt(52) && value <= BigInt(2) << BigInt(52))
value = Number(value);
} else value = dataView.getBigInt64(position); } else value = dataView.getBigInt64(position);
position += 8; position += 8;
return value; return value;
@@ -433,7 +475,10 @@ export function read() {
// str 8 // str 8
value = src[position++]; value = src[position++];
if (srcStringEnd >= position) { if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart); return srcString.slice(
position - srcStringStart,
(position += value) - srcStringStart
);
} }
return readString8(value); return readString8(value);
case 0xda: case 0xda:
@@ -441,7 +486,10 @@ export function read() {
value = dataView.getUint16(position); value = dataView.getUint16(position);
position += 2; position += 2;
if (srcStringEnd >= position) { if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart); return srcString.slice(
position - srcStringStart,
(position += value) - srcStringStart
);
} }
return readString16(value); return readString16(value);
case 0xdb: case 0xdb:
@@ -449,7 +497,10 @@ export function read() {
value = dataView.getUint32(position); value = dataView.getUint32(position);
position += 4; position += 4;
if (srcStringEnd >= position) { if (srcStringEnd >= position) {
return srcString.slice(position - srcStringStart, (position += value) - srcStringStart); return srcString.slice(
position - srcStringStart,
(position += value) - srcStringStart
);
} }
return readString32(value); return readString32(value);
case 0xdc: case 0xdc:
@@ -504,7 +555,8 @@ function createStructureReader(structure, firstId) {
.join(',') + .join(',') +
'})}' '})}'
)(read)); )(read));
if (structure.highByte === 0) structure.read = createSecondByteReader(firstId, structure.read); if (structure.highByte === 0)
structure.read = createSecondByteReader(firstId, structure.read);
return readObject(); // second byte is already read, if there is one so immediately read object return readObject(); // second byte is already read, if there is one so immediately read object
} }
const object = {}; const object = {};
@@ -527,7 +579,8 @@ const createSecondByteReader = (firstId, read0) =>
function () { function () {
const highByte = src[position++]; const highByte = src[position++];
if (highByte === 0) return read0(); if (highByte === 0) return read0();
const id = firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5); const id =
firstId < 32 ? -(firstId + (highByte << 5)) : firstId + (highByte << 5);
const structure = currentStructures[id] || loadStructures()[id]; const structure = currentStructures[id] || loadStructures()[id];
if (!structure) { if (!structure) {
throw new Error('Record id is not defined for ' + id); throw new Error('Record id is not defined for ' + id);
@@ -542,7 +595,10 @@ export function loadStructures() {
src = null; src = null;
return currentUnpackr.getStructures(); return currentUnpackr.getStructures();
}); });
return (currentStructures = currentUnpackr._mergeStructures(loadedStructures, currentStructures)); return (currentStructures = currentUnpackr._mergeStructures(
loadedStructures,
currentStructures
));
} }
var readFixedString = readStringJS; var readFixedString = readStringJS;
@@ -563,7 +619,11 @@ export function setExtractor(extractStrings) {
if (string == null) { if (string == null) {
if (bundledStrings) return readStringJS(length); if (bundledStrings) return readStringJS(length);
const byteOffset = src.byteOffset; const byteOffset = src.byteOffset;
const extraction = extractStrings(position - headerLength + byteOffset, srcEnd + byteOffset, src.buffer); const extraction = extractStrings(
position - headerLength + byteOffset,
srcEnd + byteOffset,
src.buffer
);
if (typeof extraction == 'string') { if (typeof extraction == 'string') {
string = extraction; string = extraction;
strings = EMPTY_ARRAY; strings = EMPTY_ARRAY;
@@ -593,7 +653,8 @@ function readStringJS(length) {
if (length < 16) { if (length < 16) {
if ((result = shortStringInJS(length))) return result; if ((result = shortStringInJS(length))) return result;
} }
if (length > 64 && decoder) return decoder.decode(src.subarray(position, (position += length))); if (length > 64 && decoder)
return decoder.decode(src.subarray(position, (position += length)));
const end = position + length; const end = position + length;
const units = []; const units = [];
result = ''; result = '';
@@ -616,7 +677,8 @@ function readStringJS(length) {
const byte2 = src[position++] & 0x3f; const byte2 = src[position++] & 0x3f;
const byte3 = src[position++] & 0x3f; const byte3 = src[position++] & 0x3f;
const byte4 = src[position++] & 0x3f; const byte4 = src[position++] & 0x3f;
let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; let unit =
((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
if (unit > 0xffff) { if (unit > 0xffff) {
unit -= 0x10000; unit -= 0x10000;
units.push(((unit >>> 10) & 0x3ff) | 0xd800); units.push(((unit >>> 10) & 0x3ff) | 0xd800);
@@ -810,7 +872,8 @@ function shortStringInJS(length) {
position -= 14; position -= 14;
return; return;
} }
if (length < 15) return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n); if (length < 15)
return fromCharCode(a, b, c, d, e, f, g, h, i, j, k, l, m, n);
const o = src[position++]; const o = src[position++];
if ((o & 0x80) > 0) { if ((o & 0x80) > 0) {
position -= 15; position -= 15;
@@ -862,14 +925,17 @@ function readExt(length) {
const type = src[position++]; const type = src[position++];
if (currentExtensions[type]) { if (currentExtensions[type]) {
let end; let end;
return currentExtensions[type](src.subarray(position, (end = position += length)), (readPosition) => { return currentExtensions[type](
position = readPosition; src.subarray(position, (end = position += length)),
try { (readPosition) => {
return read(); position = readPosition;
} finally { try {
position = end; return read();
} finally {
position = end;
}
} }
}); );
} else throw new Error('Unknown extension type ' + type); } else throw new Error('Unknown extension type ' + type);
} }
@@ -881,14 +947,20 @@ function readKey() {
length = length - 0xa0; length = length - 0xa0;
if (srcStringEnd >= position) if (srcStringEnd >= position)
// if it has been extracted, must use it (and faster anyway) // if it has been extracted, must use it (and faster anyway)
return srcString.slice(position - srcStringStart, (position += length) - srcStringStart); return srcString.slice(
position - srcStringStart,
(position += length) - srcStringStart
);
else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length); else if (!(srcStringEnd == 0 && srcEnd < 180)) return readFixedString(length);
} else { } else {
// not cacheable, go back and do a standard read // not cacheable, go back and do a standard read
position--; position--;
return read().toString(); return read().toString();
} }
const key = ((length << 5) ^ (length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) & 0xfff; const key =
((length << 5) ^
(length > 1 ? dataView.getUint16(position) : length > 0 ? src[position] : 0)) &
0xfff;
let entry = keyCache[key]; let entry = keyCache[key];
let checkPosition = position; let checkPosition = position;
let end = position + length - 3; let end = position + length - 3;
@@ -947,7 +1019,8 @@ const recordDefinition = (id, highByte) => {
} }
const existingStructure = currentStructures[id]; const existingStructure = currentStructures[id];
if (existingStructure && existingStructure.isShared) { if (existingStructure && existingStructure.isShared) {
(currentStructures.restoreStructures || (currentStructures.restoreStructures = []))[id] = existingStructure; (currentStructures.restoreStructures ||
(currentStructures.restoreStructures = []))[id] = existingStructure;
} }
currentStructures[id] = structure; currentStructures[id] = structure;
structure.read = createStructureReader(structure, firstByte); structure.read = createStructureReader(structure, firstByte);
@@ -1009,7 +1082,8 @@ export const typedArrays = [
currentExtensions[0x74] = (data) => { currentExtensions[0x74] = (data) => {
const typeCode = data[0]; const typeCode = data[0];
const typedArrayName = typedArrays[typeCode]; const typedArrayName = typedArrays[typeCode];
if (!typedArrayName) throw new Error('Could not find typed array for code ' + typeCode); if (!typedArrayName)
throw new Error('Could not find typed array for code ' + typeCode);
// we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned // we have to always slice/copy here to get a new ArrayBuffer that is word/byte aligned
return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer); return new glbl[typedArrayName](Uint8Array.prototype.slice.call(data, 1).buffer);
}; };
@@ -1033,11 +1107,20 @@ currentExtensions[0x62] = (data) => {
currentExtensions[0xff] = (data) => { currentExtensions[0xff] = (data) => {
// 32-bit date extension // 32-bit date extension
if (data.length == 4) return new Date((data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000); if (data.length == 4)
return new Date(
(data[0] * 0x1000000 + (data[1] << 16) + (data[2] << 8) + data[3]) * 1000
);
else if (data.length == 8) else if (data.length == 8)
return new Date( return new Date(
((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) / 1000000 + ((data[0] << 22) + (data[1] << 14) + (data[2] << 6) + (data[3] >> 2)) /
((data[3] & 0x3) * 0x100000000 + data[4] * 0x1000000 + (data[5] << 16) + (data[6] << 8) + data[7]) * 1000 1000000 +
((data[3] & 0x3) * 0x100000000 +
data[4] * 0x1000000 +
(data[5] << 16) +
(data[6] << 8) +
data[7]) *
1000
); );
else if (data.length == 12) else if (data.length == 12)
return new Date( return new Date(
@@ -1070,7 +1153,10 @@ function saveState(callback) {
const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed const savedSrc = new Uint8Array(src.slice(0, srcEnd)); // we copy the data in case it changes while external data is processed
const savedStructures = currentStructures; const savedStructures = currentStructures;
const savedStructuresContents = currentStructures.slice(0, currentStructures.length); const savedStructuresContents = currentStructures.slice(
0,
currentStructures.length
);
const savedPackr = currentUnpackr; const savedPackr = currentUnpackr;
const savedSequentialMode = sequentialMode; const savedSequentialMode = sequentialMode;
const value = callback(); const value = callback();
@@ -1122,7 +1208,10 @@ const u8Array = new Uint8Array(f32Array.buffer, 0, 4);
export function roundFloat32(float32Number) { export function roundFloat32(float32Number) {
f32Array[0] = float32Number; f32Array[0] = float32Number;
const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)]; const multiplier = mult10[((u8Array[3] & 0x7f) << 1) | (u8Array[2] >> 7)];
return ((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) / multiplier; return (
((multiplier * float32Number + (float32Number > 0 ? 0.5 : -0.5)) >> 0) /
multiplier
);
} }
export function setReadStruct(updatedReadStruct, loadedStructs, saveState) { export function setReadStruct(updatedReadStruct, loadedStructs, saveState) {
readStruct = updatedReadStruct; readStruct = updatedReadStruct;

View File

@@ -14,27 +14,44 @@ export interface MessageBoxProps extends BoxProps {
message: string; message: string;
} }
const LEVEL_ICONS: { [type in MessageBoxLevel]: React.ComponentType<SvgIconProps> } = { const LEVEL_ICONS: {
[type in MessageBoxLevel]: React.ComponentType<SvgIconProps>;
} = {
success: CheckCircleOutlineOutlinedIcon, success: CheckCircleOutlineOutlinedIcon,
info: InfoOutlinedIcon, info: InfoOutlinedIcon,
warning: ReportProblemOutlinedIcon, warning: ReportProblemOutlinedIcon,
error: ErrorIcon error: ErrorIcon
}; };
const LEVEL_BACKGROUNDS: { [type in MessageBoxLevel]: (theme: Theme) => string } = { const LEVEL_BACKGROUNDS: {
[type in MessageBoxLevel]: (theme: Theme) => string;
} = {
success: (theme: Theme) => theme.palette.success.dark, success: (theme: Theme) => theme.palette.success.dark,
info: (theme: Theme) => theme.palette.info.main, info: (theme: Theme) => theme.palette.info.main,
warning: (theme: Theme) => theme.palette.warning.dark, warning: (theme: Theme) => theme.palette.warning.dark,
error: (theme: Theme) => theme.palette.error.dark error: (theme: Theme) => theme.palette.error.dark
}; };
const MessageBox: FC<MessageBoxProps> = ({ level, message, sx, children, ...rest }) => { const MessageBox: FC<MessageBoxProps> = ({
level,
message,
sx,
children,
...rest
}) => {
const theme = useTheme(); const theme = useTheme();
const Icon = LEVEL_ICONS[level]; const Icon = LEVEL_ICONS[level];
const backgroundColor = LEVEL_BACKGROUNDS[level](theme); const backgroundColor = LEVEL_BACKGROUNDS[level](theme);
const color = 'white'; const color = 'white';
return ( return (
<Box p={2} display="flex" alignItems="center" borderRadius={1} sx={{ backgroundColor, color, ...sx }} {...rest}> <Box
p={2}
display="flex"
alignItems="center"
borderRadius={1}
sx={{ backgroundColor, color, ...sx }}
{...rest}
>
<Icon /> <Icon />
<Typography sx={{ ml: 2, flexGrow: 1 }} variant="body1"> <Typography sx={{ ml: 2, flexGrow: 1 }} variant="body1">
{message} {message}

View File

@@ -14,7 +14,16 @@ const SectionContent: FC<SectionContentProps> = (props) => {
return ( return (
<Paper id={id} sx={{ p: 2, m: 2 }}> <Paper id={id} sx={{ p: 2, m: 2 }}>
{title && ( {title && (
<Divider sx={{ pb: 2, borderColor: 'primary.main', fontSize: 20, color: 'primary.main' }}>{title}</Divider> <Divider
sx={{
pb: 2,
borderColor: 'primary.main',
fontSize: 20,
color: 'primary.main'
}}
>
{title}
</Divider>
)} )}
{children} {children}
</Paper> </Paper>

View File

@@ -10,7 +10,10 @@ import type { ValidatedTextFieldProps } from './ValidatedTextField';
type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>; type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ InputProps, ...props }) => { const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({
InputProps,
...props
}) => {
const [showPassword, setShowPassword] = useState<boolean>(false); const [showPassword, setShowPassword] = useState<boolean>(false);
return ( return (

View File

@@ -12,9 +12,14 @@ interface ValidatedFieldProps {
export type ValidatedTextFieldProps = ValidatedFieldProps & TextFieldProps; export type ValidatedTextFieldProps = ValidatedFieldProps & TextFieldProps;
const ValidatedTextField: FC<ValidatedTextFieldProps> = ({ fieldErrors, ...rest }) => { const ValidatedTextField: FC<ValidatedTextFieldProps> = ({
fieldErrors,
...rest
}) => {
const errors = fieldErrors && fieldErrors[rest.name]; const errors = fieldErrors && fieldErrors[rest.name];
const renderErrors = () => errors && errors.map((e, i) => <FormHelperText key={i}>{e.message}</FormHelperText>); const renderErrors = () =>
errors &&
errors.map((e, i) => <FormHelperText key={i}>{e.message}</FormHelperText>);
return ( return (
<> <>
<TextField error={!!errors} {...rest} /> <TextField error={!!errors} {...rest} />

View File

@@ -21,7 +21,12 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => (
}} }}
> >
<Toolbar> <Toolbar>
<IconButton color="inherit" edge="start" onClick={onToggleDrawer} sx={{ mr: 2, display: { md: 'none' } }}> <IconButton
color="inherit"
edge="start"
onClick={onToggleDrawer}
sx={{ mr: 2, display: { md: 'none' } }}
>
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" noWrap component="div"> <Typography variant="h6" noWrap component="div">

View File

@@ -54,7 +54,9 @@ const LayoutMenu: FC = () => {
const [menuOpen, setMenuOpen] = useState(true); const [menuOpen, setMenuOpen] = useState(true);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({ target }) => { const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
target
}) => {
const loc = target.value as Locales; const loc = target.value as Locales;
localStorage.setItem('lang', loc); localStorage.setItem('lang', loc);
await loadLocaleAsync(loc); await loadLocaleAsync(loc);
@@ -98,7 +100,14 @@ const LayoutMenu: FC = () => {
mb: '2px', mb: '2px',
color: 'lightblue' color: 'lightblue'
}} }}
secondary={LL.CUSTOMIZATIONS() + ', ' + LL.SCHEDULER() + ', ' + LL.CUSTOM_ENTITIES(0) + '...'} secondary={
LL.CUSTOMIZATIONS() +
', ' +
LL.SCHEDULER() +
', ' +
LL.CUSTOM_ENTITIES(0) +
'...'
}
secondaryTypographyProps={{ secondaryTypographyProps={{
noWrap: true, noWrap: true,
fontSize: 12, fontSize: 12,
@@ -123,7 +132,12 @@ const LayoutMenu: FC = () => {
disabled={!me.admin} disabled={!me.admin}
to={`/customizations`} to={`/customizations`}
/> />
<LayoutMenuItem icon={MoreTimeIcon} label={LL.SCHEDULER()} disabled={!me.admin} to={`/scheduler`} /> <LayoutMenuItem
icon={MoreTimeIcon}
label={LL.SCHEDULER()}
disabled={!me.admin}
to={`/scheduler`}
/>
<LayoutMenuItem <LayoutMenuItem
icon={PlaylistAddIcon} icon={PlaylistAddIcon}
label={LL.CUSTOM_ENTITIES(0)} label={LL.CUSTOM_ENTITIES(0)}
@@ -137,7 +151,12 @@ const LayoutMenu: FC = () => {
<List style={{ marginTop: `auto` }}> <List style={{ marginTop: `auto` }}>
<LayoutMenuItem icon={AssessmentIcon} label={LL.SYSTEM(0)} to="/system" /> <LayoutMenuItem icon={AssessmentIcon} label={LL.SYSTEM(0)} to="/system" />
<LayoutMenuItem icon={SettingsIcon} label={LL.SETTINGS(0)} disabled={!me.admin} to="/settings" /> <LayoutMenuItem
icon={SettingsIcon}
label={LL.SETTINGS(0)}
disabled={!me.admin}
to="/settings"
/>
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP_OF('')} to={`/help`} /> <LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP_OF('')} to={`/help`} />
</List> </List>
<Divider /> <Divider />
@@ -239,7 +258,12 @@ const LayoutMenu: FC = () => {
</TextField> </TextField>
</Box> </Box>
<Box> <Box>
<Button variant="outlined" fullWidth color="primary" onClick={() => signOut(true)}> <Button
variant="outlined"
fullWidth
color="primary"
onClick={() => signOut(true)}
>
{LL.SIGN_OUT()} {LL.SIGN_OUT()}
</Button> </Button>
</Box> </Box>

View File

@@ -13,7 +13,12 @@ interface LayoutMenuItemProps {
disabled?: boolean; disabled?: boolean;
} }
const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabled }) => { const LayoutMenuItem: FC<LayoutMenuItemProps> = ({
icon: Icon,
label,
to,
disabled
}) => {
const { pathname } = useLocation(); const { pathname } = useLocation();
const selected = routeMatches(to, pathname); const selected = routeMatches(to, pathname);
@@ -23,7 +28,9 @@ const LayoutMenuItem: FC<LayoutMenuItemProps> = ({ icon: Icon, label, to, disabl
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}> <ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
<Icon /> <Icon />
</ListItemIcon> </ListItemIcon>
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>{label}</ListItemText> <ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>
{label}
</ListItemText>
</ListItemButton> </ListItemButton>
); );
}; };

View File

@@ -2,7 +2,14 @@ import type { FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; import {
Avatar,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemIcon,
ListItemText
} from '@mui/material';
import type { SvgIconProps } from '@mui/material'; import type { SvgIconProps } from '@mui/material';
interface ListMenuItemProps { interface ListMenuItemProps {
@@ -27,19 +34,38 @@ function RenderIcon({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) {
); );
} }
const LayoutMenuItem: FC<ListMenuItemProps> = ({ icon, bgcolor, label, text, to, disabled }) => ( const LayoutMenuItem: FC<ListMenuItemProps> = ({
icon,
bgcolor,
label,
text,
to,
disabled
}) => (
<> <>
{to && !disabled ? ( {to && !disabled ? (
<ListItem <ListItem
disablePadding disablePadding
secondaryAction={ secondaryAction={
<ListItemIcon style={{ justifyContent: 'right', color: 'lightblue', verticalAlign: 'middle' }}> <ListItemIcon
style={{
justifyContent: 'right',
color: 'lightblue',
verticalAlign: 'middle'
}}
>
<NavigateNextIcon /> <NavigateNextIcon />
</ListItemIcon> </ListItemIcon>
} }
> >
<ListItemButton component={Link} to={to}> <ListItemButton component={Link} to={to}>
<RenderIcon icon={icon} bgcolor={bgcolor} label={label} text={text} to="" /> <RenderIcon
icon={icon}
bgcolor={bgcolor}
label={label}
text={text}
to=""
/>
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
) : ( ) : (

View File

@@ -22,7 +22,13 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ message }) => (
borderRadius: 0 borderRadius: 0
}} }}
> >
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}> <Box
display="flex"
flexDirection="row"
justifyContent="center"
alignItems="center"
mb={2}
>
<WarningIcon fontSize="large" color="error" /> <WarningIcon fontSize="large" color="error" />
<Box ml={2}> <Box ml={2}>
<Typography variant="h4">Application Error</Typography> <Typography variant="h4">Application Error</Typography>

View File

@@ -12,14 +12,23 @@ interface FormLoaderProps {
onRetry?: () => void; onRetry?: () => void;
} }
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, onRetry, message = 'Loading…' }) => { const FormLoader: FC<FormLoaderProps> = ({
errorMessage,
onRetry,
message = 'Loading…'
}) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
if (errorMessage) { if (errorMessage) {
return ( return (
<MessageBox my={2} level="error" message={errorMessage}> <MessageBox my={2} level="error" message={errorMessage}>
{onRetry && ( {onRetry && (
<Button startIcon={<RefreshIcon />} variant="contained" color="error" onClick={onRetry}> <Button
startIcon={<RefreshIcon />}
variant="contained"
color="error"
onClick={onRetry}
>
{LL.RETRY()} {LL.RETRY()}
</Button> </Button>
)} )}

View File

@@ -13,7 +13,14 @@ const LoadingSpinner: FC<LoadingSpinnerProps> = ({ height = '100%' }) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
return ( return (
<Box display="flex" alignItems="center" justifyContent="center" flexDirection="column" padding={2} height={height}> <Box
display="flex"
alignItems="center"
justifyContent="center"
flexDirection="column"
padding={2}
height={height}
>
<CircularProgress <CircularProgress
sx={(theme: Theme) => ({ sx={(theme: Theme) => ({
margin: theme.spacing(4), margin: theme.spacing(4),

View File

@@ -1,7 +1,13 @@
import type { FC } from 'react'; import type { FC } from 'react';
import type { Blocker } from 'react-router-dom'; import type { Blocker } from 'react-router-dom';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle
} from '@mui/material';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -18,10 +24,18 @@ const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
<DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle> <DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle>
<DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent> <DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent>
<DialogActions> <DialogActions>
<Button variant="outlined" onClick={() => blocker.reset?.()} color="secondary"> <Button
variant="outlined"
onClick={() => blocker.reset?.()}
color="secondary"
>
{LL.STAY()} {LL.STAY()}
</Button> </Button>
<Button variant="contained" onClick={() => blocker.proceed?.()} color="primary"> <Button
variant="contained"
onClick={() => blocker.proceed?.()}
color="primary"
>
{LL.LEAVE()} {LL.LEAVE()}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -7,7 +7,11 @@ import type { RequiredChildrenProps } from 'utils';
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => { const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
const authenticatedContext = useContext(AuthenticatedContext); const authenticatedContext = useContext(AuthenticatedContext);
return authenticatedContext.me.admin ? <>{children}</> : <Navigate replace to="/" />; return authenticatedContext.me.admin ? (
<>{children}</>
) : (
<Navigate replace to="/" />
);
}; };
export default RequireAdmin; export default RequireAdmin;

View File

@@ -5,7 +5,10 @@ import { Navigate, useLocation } from 'react-router-dom';
import { storeLoginRedirect } from 'api/authentication'; import { storeLoginRedirect } from 'api/authentication';
import type { AuthenticatedContextValue } from 'contexts/authentication/context'; import type { AuthenticatedContextValue } from 'contexts/authentication/context';
import { AuthenticatedContext, AuthenticationContext } from 'contexts/authentication/context'; import {
AuthenticatedContext,
AuthenticationContext
} from 'contexts/authentication/context';
import type { RequiredChildrenProps } from 'utils'; import type { RequiredChildrenProps } from 'utils';
const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => { const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
@@ -19,7 +22,9 @@ const RequireAuthenticated: FC<RequiredChildrenProps> = ({ children }) => {
}); });
return authenticationContext.me ? ( return authenticationContext.me ? (
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}> <AuthenticatedContext.Provider
value={authenticationContext as AuthenticatedContextValue}
>
{children} {children}
</AuthenticatedContext.Provider> </AuthenticatedContext.Provider>
) : ( ) : (

View File

@@ -10,7 +10,11 @@ import type { RequiredChildrenProps } from 'utils';
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => { const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
const authenticationContext = useContext(AuthenticationContext); const authenticationContext = useContext(AuthenticationContext);
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect()} /> : <>{children}</>; return authenticationContext.me ? (
<Navigate to={AuthenticationApi.fetchLoginRedirect()} />
) : (
<>{children}</>
);
}; };
export default RequireUnauthenticated; export default RequireUnauthenticated;

View File

@@ -20,7 +20,11 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
}; };
return ( return (
<Tabs value={value} onChange={handleTabChange} variant={smallDown ? 'scrollable' : 'fullWidth'}> <Tabs
value={value}
onChange={handleTabChange}
variant={smallDown ? 'scrollable' : 'fullWidth'}
>
{children} {children}
</Tabs> </Tabs>
); );

View File

@@ -31,7 +31,12 @@ export interface SingleUploadProps {
progress: Progress; progress: Progress;
} }
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, progress }) => { const SingleUpload: FC<SingleUploadProps> = ({
onDrop,
onCancel,
isUploading,
progress
}) => {
const uploading = isUploading && progress.total > 0; const uploading = isUploading && progress.total > 0;
const dropzoneState = useDropzone({ const dropzoneState = useDropzone({
@@ -53,8 +58,14 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, pr
if (uploading) { if (uploading) {
if (progress.total && progress.loaded) { if (progress.total && progress.loaded) {
return progress.loaded <= progress.total return progress.loaded <= progress.total
? LL.UPLOADING() + ': ' + Math.round((progress.loaded * 100) / progress.total) + '%' ? LL.UPLOADING() +
: LL.UPLOADING() + ': ' + Math.round((progress.total * 100) / progress.loaded) + '%'; ': ' +
Math.round((progress.loaded * 100) / progress.total) +
'%'
: LL.UPLOADING() +
': ' +
Math.round((progress.total * 100) / progress.loaded) +
'%';
} }
} }
return LL.UPLOAD_DROP_TEXT(); return LL.UPLOAD_DROP_TEXT();
@@ -95,7 +106,12 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, isUploading, pr
} }
/> />
</Box> </Box>
<Button startIcon={<CancelIcon />} variant="outlined" color="secondary" onClick={onCancel}> <Button
startIcon={<CancelIcon />}
variant="outlined"
color="secondary"
onClick={onCancel}
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
</Fragment> </Fragment>

View File

@@ -20,9 +20,12 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const [initialized, setInitialized] = useState<boolean>(false); const [initialized, setInitialized] = useState<boolean>(false);
const [me, setMe] = useState<Me>(); const [me, setMe] = useState<Me>();
const { send: verifyAuthorization } = useRequest(AuthenticationApi.verifyAuthorization(), { const { send: verifyAuthorization } = useRequest(
immediate: false AuthenticationApi.verifyAuthorization(),
}); {
immediate: false
}
);
const signIn = (accessToken: string) => { const signIn = (accessToken: string) => {
try { try {

View File

@@ -10,7 +10,9 @@ export interface AuthenticationContextValue {
} }
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue; const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
export const AuthenticationContext = createContext(AuthenticationContextDefaultValue); export const AuthenticationContext = createContext(
AuthenticationContextDefaultValue
);
export interface AuthenticatedContextValue extends AuthenticationContextValue { export interface AuthenticatedContextValue extends AuthenticationContextValue {
me: Me; me: Me;

View File

@@ -13,7 +13,15 @@ import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet'; import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TuneIcon from '@mui/icons-material/Tune'; import TuneIcon from '@mui/icons-material/Tune';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, List } from '@mui/material'; import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List
} from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
@@ -92,7 +100,11 @@ const Settings: FC = () => {
}; };
const renderRestartDialog = () => ( const renderRestartDialog = () => (
<Dialog sx={dialogStyle} open={confirmRestart} onClose={() => setConfirmRestart(false)}> <Dialog
sx={dialogStyle}
open={confirmRestart}
onClose={() => setConfirmRestart(false)}
>
<DialogTitle>{LL.RESTART()}</DialogTitle> <DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent> <DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions> <DialogActions>
@@ -128,7 +140,11 @@ const Settings: FC = () => {
); );
const renderFactoryResetDialog = () => ( const renderFactoryResetDialog = () => (
<Dialog sx={dialogStyle} open={confirmFactoryReset} onClose={() => setConfirmFactoryReset(false)}> <Dialog
sx={dialogStyle}
open={confirmFactoryReset}
onClose={() => setConfirmFactoryReset(false)}
>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle> <DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent> <DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
<DialogActions> <DialogActions>
@@ -189,9 +205,26 @@ const Settings: FC = () => {
to="ntp" to="ntp"
/> />
<ListMenuItem icon={DeviceHubIcon} bgcolor="#68374d" label="MQTT" text={LL.CONFIGURE('MQTT')} to="mqtt" /> <ListMenuItem
<ListMenuItem icon={CastIcon} bgcolor="#efc34b" label="OTA" text={LL.CONFIGURE('OTA')} to="ota" /> icon={DeviceHubIcon}
<ListMenuItem icon={LockIcon} label={LL.SECURITY(0)} text={LL.SECURITY_1()} to="security" /> bgcolor="#68374d"
label="MQTT"
text={LL.CONFIGURE('MQTT')}
to="mqtt"
/>
<ListMenuItem
icon={CastIcon}
bgcolor="#efc34b"
label="OTA"
text={LL.CONFIGURE('OTA')}
to="ota"
/>
<ListMenuItem
icon={LockIcon}
label={LL.SECURITY(0)}
text={LL.SECURITY_1()}
to="security"
/>
<ListMenuItem <ListMenuItem
icon={MemoryIcon} icon={MemoryIcon}
@@ -242,7 +275,9 @@ const Settings: FC = () => {
</> </>
); );
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>; return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
}; };
export default Settings; export default Settings;

View File

@@ -25,7 +25,8 @@ import { numberValue, updateValueDirty, useRest } from 'utils';
import { createAPSettingsValidator, validate } from 'validators'; import { createAPSettingsValidator, validate } from 'validators';
export const isAPEnabled = ({ provision_mode }: APSettingsType) => export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED; provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
const APSettings: FC = () => { const APSettings: FC = () => {
const { const {
@@ -48,7 +49,12 @@ const APSettings: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => { const content = () => {
if (!data) { if (!data) {
@@ -78,9 +84,15 @@ const APSettings: FC = () => {
onChange={updateFormValue} onChange={updateFormValue}
margin="normal" margin="normal"
> >
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>{LL.AP_PROVIDE_TEXT_1()}</MenuItem> <MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>{LL.AP_PROVIDE_TEXT_2()}</MenuItem> {LL.AP_PROVIDE_TEXT_1()}
<MenuItem value={APProvisionMode.AP_NEVER}>{LL.AP_PROVIDE_TEXT_3()}</MenuItem> </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> </ValidatedTextField>
{isAPEnabled(data) && ( {isAPEnabled(data) && (
<> <>
@@ -123,7 +135,13 @@ const APSettings: FC = () => {
))} ))}
</ValidatedTextField> </ValidatedTextField>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="ssid_hidden" checked={data.ssid_hidden} onChange={updateFormValue} />} control={
<Checkbox
name="ssid_hidden"
checked={data.ssid_hidden}
onChange={updateFormValue}
/>
}
label={LL.AP_HIDE_SSID()} label={LL.AP_HIDE_SSID()}
/> />
<ValidatedTextField <ValidatedTextField

View File

@@ -4,7 +4,16 @@ import ComputerIcon from '@mui/icons-material/Computer';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material'; import {
Avatar,
Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import * as APApi from 'api/ap'; import * as APApi from 'api/ap';
@@ -69,7 +78,10 @@ const APStatus: FC = () => {
<ListItemAvatar> <ListItemAvatar>
<Avatar>IP</Avatar> <Avatar>IP</Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.ADDRESS_OF('IP')} secondary={data.ip_address} /> <ListItemText
primary={LL.ADDRESS_OF('IP')}
secondary={data.ip_address}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>
@@ -78,7 +90,10 @@ const APStatus: FC = () => {
<DeviceHubIcon /> <DeviceHubIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} /> <ListItemText
primary={LL.ADDRESS_OF('MAC')}
secondary={data.mac_address}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>
@@ -92,7 +107,12 @@ const APStatus: FC = () => {
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</List> </List>
<ButtonRow> <ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -3,7 +3,15 @@ import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Button, Checkbox, Grid, InputAdornment, MenuItem, TextField, Typography } from '@mui/material'; import {
Button,
Checkbox,
Grid,
InputAdornment,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
@@ -43,7 +51,12 @@ const MqttSettings: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => { const content = () => {
if (!data) { if (!data) {
@@ -63,10 +76,22 @@ const MqttSettings: FC = () => {
return ( return (
<> <>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />} control={
<Checkbox
name="enabled"
checked={data.enabled}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_MQTT()} label={LL.ENABLE_MQTT()}
/> />
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start"> <Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
@@ -144,7 +169,9 @@ const MqttSettings: FC = () => {
name="keep_alive" name="keep_alive"
label="Keep Alive" label="Keep Alive"
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -173,7 +200,13 @@ const MqttSettings: FC = () => {
</Grid> </Grid>
{data.enableTLS !== undefined && ( {data.enableTLS !== undefined && (
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="enableTLS" checked={data.enableTLS} onChange={updateFormValue} />} control={
<Checkbox
name="enableTLS"
checked={data.enableTLS}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_TLS()} label={LL.ENABLE_TLS()}
/> />
)} )}
@@ -190,11 +223,23 @@ const MqttSettings: FC = () => {
)} )}
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="clean_session" checked={data.clean_session} onChange={updateFormValue} />} control={
<Checkbox
name="clean_session"
checked={data.clean_session}
onChange={updateFormValue}
/>
}
label={LL.MQTT_CLEAN_SESSION()} label={LL.MQTT_CLEAN_SESSION()}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="mqtt_retain" checked={data.mqtt_retain} onChange={updateFormValue} />} control={
<Checkbox
name="mqtt_retain"
checked={data.mqtt_retain}
onChange={updateFormValue}
/>
}
label={LL.MQTT_RETAIN_FLAG()} label={LL.MQTT_RETAIN_FLAG()}
/> />
@@ -215,7 +260,13 @@ const MqttSettings: FC = () => {
<MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem> <MenuItem value={2}>{LL.MQTT_NEST_2()}</MenuItem>
</TextField> </TextField>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="send_response" checked={data.send_response} onChange={updateFormValue} />} control={
<Checkbox
name="send_response"
checked={data.send_response}
onChange={updateFormValue}
/>
}
label={LL.MQTT_RESPONSE()} label={LL.MQTT_RESPONSE()}
/> />
{!data.ha_enabled && ( {!data.ha_enabled && (
@@ -229,7 +280,13 @@ const MqttSettings: FC = () => {
> >
<Grid item> <Grid item>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="publish_single" checked={data.publish_single} onChange={updateFormValue} />} control={
<Checkbox
name="publish_single"
checked={data.publish_single}
onChange={updateFormValue}
/>
}
label={LL.MQTT_PUBLISH_TEXT_1()} label={LL.MQTT_PUBLISH_TEXT_1()}
/> />
</Grid> </Grid>
@@ -237,7 +294,11 @@ const MqttSettings: FC = () => {
<Grid item> <Grid item>
<BlockFormControlLabel <BlockFormControlLabel
control={ control={
<Checkbox name="publish_single2cmd" checked={data.publish_single2cmd} onChange={updateFormValue} /> <Checkbox
name="publish_single2cmd"
checked={data.publish_single2cmd}
onChange={updateFormValue}
/>
} }
label={LL.MQTT_PUBLISH_TEXT_2()} label={LL.MQTT_PUBLISH_TEXT_2()}
/> />
@@ -246,10 +307,22 @@ const MqttSettings: FC = () => {
</Grid> </Grid>
)} )}
{!data.publish_single && ( {!data.publish_single && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start"> <Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item> <Grid item>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="ha_enabled" checked={data.ha_enabled} onChange={updateFormValue} />} control={
<Checkbox
name="ha_enabled"
checked={data.ha_enabled}
onChange={updateFormValue}
/>
}
label={LL.MQTT_PUBLISH_TEXT_3()} label={LL.MQTT_PUBLISH_TEXT_3()}
/> />
</Grid> </Grid>
@@ -312,14 +385,22 @@ const MqttSettings: FC = () => {
<Typography sx={{ pt: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto) {LL.MQTT_PUBLISH_INTERVALS()}&nbsp;(0=auto)
</Typography> </Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start"> <Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}> <Grid item xs={12} sm={6} md={4}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
name="publish_time_heartbeat" name="publish_time_heartbeat"
label="Heartbeat" label="Heartbeat"
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -334,7 +415,9 @@ const MqttSettings: FC = () => {
name="publish_time_boiler" name="publish_time_boiler"
label={LL.MQTT_INT_BOILER()} label={LL.MQTT_INT_BOILER()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -349,7 +432,9 @@ const MqttSettings: FC = () => {
name="publish_time_thermostat" name="publish_time_thermostat"
label={LL.MQTT_INT_THERMOSTATS()} label={LL.MQTT_INT_THERMOSTATS()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -364,7 +449,9 @@ const MqttSettings: FC = () => {
name="publish_time_solar" name="publish_time_solar"
label={LL.MQTT_INT_SOLAR()} label={LL.MQTT_INT_SOLAR()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -379,7 +466,9 @@ const MqttSettings: FC = () => {
name="publish_time_mixer" name="publish_time_mixer"
label={LL.MQTT_INT_MIXER()} label={LL.MQTT_INT_MIXER()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -394,7 +483,9 @@ const MqttSettings: FC = () => {
name="publish_time_water" name="publish_time_water"
label={LL.MQTT_INT_WATER()} label={LL.MQTT_INT_WATER()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -409,7 +500,9 @@ const MqttSettings: FC = () => {
name="publish_time_sensor" name="publish_time_sensor"
label={LL.TEMP_SENSORS()} label={LL.TEMP_SENSORS()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -423,7 +516,9 @@ const MqttSettings: FC = () => {
<TextField <TextField
name="publish_time_other" name="publish_time_other"
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
label={LL.DEFAULT(0)} label={LL.DEFAULT(0)}
fullWidth fullWidth

View File

@@ -5,7 +5,16 @@ import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import ReportIcon from '@mui/icons-material/Report'; import ReportIcon from '@mui/icons-material/Report';
import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff'; import SpeakerNotesOffIcon from '@mui/icons-material/SpeakerNotesOff';
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material'; import {
Avatar,
Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import * as MqttApi from 'api/mqtt'; import * as MqttApi from 'api/mqtt';
@@ -16,7 +25,10 @@ import { useI18nContext } from 'i18n/i18n-react';
import type { MqttStatusType } from 'types'; import type { MqttStatusType } from 'types';
import { MqttDisconnectReason } from 'types'; import { MqttDisconnectReason } from 'types';
export const mqttStatusHighlight = ({ enabled, connected }: MqttStatusType, theme: Theme) => { export const mqttStatusHighlight = (
{ enabled, connected }: MqttStatusType,
theme: Theme
) => {
if (!enabled) { if (!enabled) {
return theme.palette.info.main; return theme.palette.info.main;
} }
@@ -26,14 +38,20 @@ export const mqttStatusHighlight = ({ enabled, connected }: MqttStatusType, them
return theme.palette.error.main; return theme.palette.error.main;
}; };
export const mqttPublishHighlight = ({ mqtt_fails }: MqttStatusType, theme: Theme) => { export const mqttPublishHighlight = (
{ mqtt_fails }: MqttStatusType,
theme: Theme
) => {
if (mqtt_fails === 0) return theme.palette.success.main; if (mqtt_fails === 0) return theme.palette.success.main;
if (mqtt_fails < 10) return theme.palette.warning.main; if (mqtt_fails < 10) return theme.palette.warning.main;
return theme.palette.error.main; return theme.palette.error.main;
}; };
export const mqttQueueHighlight = ({ mqtt_queued }: MqttStatusType, theme: Theme) => { export const mqttQueueHighlight = (
{ mqtt_queued }: MqttStatusType,
theme: Theme
) => {
if (mqtt_queued <= 1) return theme.palette.success.main; if (mqtt_queued <= 1) return theme.palette.success.main;
return theme.palette.warning.main; return theme.palette.warning.main;
@@ -92,7 +110,10 @@ const MqttStatus: FC = () => {
<ReportIcon /> <ReportIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.DISCONNECT_REASON()} secondary={disconnectReason(data)} /> <ListItemText
primary={LL.DISCONNECT_REASON()}
secondary={disconnectReason(data)}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</> </>
@@ -140,7 +161,12 @@ const MqttStatus: FC = () => {
{data.enabled && renderConnectionStatus()} {data.enabled && renderConnectionStatus()}
</List> </List>
<ButtonRow> <ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -99,7 +99,12 @@ const NetworkSettings: FC = () => {
} }
}, [initialized, setInitialized, data, selectedNetwork]); }, [initialized, setInitialized, data, selectedNetwork]);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -142,7 +147,9 @@ const NetworkSettings: FC = () => {
<List> <List>
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar>{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}</Avatar> <Avatar>
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
</Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={selectedNetwork.ssid} primary={selectedNetwork.ssid}
@@ -220,11 +227,23 @@ const NetworkSettings: FC = () => {
<MenuItem value={8}>2 dBm</MenuItem> <MenuItem value={8}>2 dBm</MenuItem>
</TextField> </TextField>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="nosleep" checked={data.nosleep} onChange={updateFormValue} />} control={
<Checkbox
name="nosleep"
checked={data.nosleep}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_DISABLE_SLEEP()} label={LL.NETWORK_DISABLE_SLEEP()}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="bandwidth20" checked={data.bandwidth20} onChange={updateFormValue} />} control={
<Checkbox
name="bandwidth20"
checked={data.bandwidth20}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_LOW_BAND()} label={LL.NETWORK_LOW_BAND()}
/> />
<Typography sx={{ pt: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2 }} variant="h6" color="primary">
@@ -241,11 +260,23 @@ const NetworkSettings: FC = () => {
margin="normal" margin="normal"
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="enableMDNS" checked={data.enableMDNS} onChange={updateFormValue} />} control={
<Checkbox
name="enableMDNS"
checked={data.enableMDNS}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_USE_DNS()} label={LL.NETWORK_USE_DNS()}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="enableCORS" checked={data.enableCORS} onChange={updateFormValue} />} control={
<Checkbox
name="enableCORS"
checked={data.enableCORS}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_ENABLE_CORS()} label={LL.NETWORK_ENABLE_CORS()}
/> />
{data.enableCORS && ( {data.enableCORS && (
@@ -261,12 +292,24 @@ const NetworkSettings: FC = () => {
)} )}
{data.enableIPv6 !== undefined && ( {data.enableIPv6 !== undefined && (
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="enableIPv6" checked={data.enableIPv6} onChange={updateFormValue} />} control={
<Checkbox
name="enableIPv6"
checked={data.enableIPv6}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_ENABLE_IPV6()} label={LL.NETWORK_ENABLE_IPV6()}
/> />
)} )}
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="static_ip_config" checked={data.static_ip_config} onChange={updateFormValue} />} control={
<Checkbox
name="static_ip_config"
checked={data.static_ip_config}
onChange={updateFormValue}
/>
}
label={LL.NETWORK_FIXED_IP()} label={LL.NETWORK_FIXED_IP()}
/> />
{data.static_ip_config && ( {data.static_ip_config && (
@@ -325,36 +368,42 @@ const NetworkSettings: FC = () => {
)} )}
{restartNeeded && ( {restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}> <MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}> <Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()} {LL.RESTART()}
</Button> </Button>
</MessageBox> </MessageBox>
)} )}
{!restartNeeded && (selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && ( {!restartNeeded &&
<ButtonRow> (selectedNetwork || (dirtyFlags && dirtyFlags.length !== 0)) && (
<Button <ButtonRow>
startIcon={<CancelIcon />} <Button
disabled={saving} startIcon={<CancelIcon />}
variant="outlined" disabled={saving}
color="primary" variant="outlined"
type="submit" color="primary"
onClick={loadData} type="submit"
> onClick={loadData}
{LL.CANCEL()} >
</Button> {LL.CANCEL()}
<Button </Button>
startIcon={<WarningIcon color="warning" />} <Button
disabled={saving} startIcon={<WarningIcon color="warning" />}
variant="contained" disabled={saving}
color="info" variant="contained"
type="submit" color="info"
onClick={validateAndSubmit} type="submit"
> onClick={validateAndSubmit}
{LL.APPLY_CHANGES(dirtyFlags.length)} >
</Button> {LL.APPLY_CHANGES(dirtyFlags.length)}
</ButtonRow> </Button>
)} </ButtonRow>
)}
</> </>
); );
}; };

View File

@@ -8,7 +8,16 @@ import RouterIcon from '@mui/icons-material/Router';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent'; import SettingsInputComponentIcon from '@mui/icons-material/SettingsInputComponent';
import WifiIcon from '@mui/icons-material/Wifi'; import WifiIcon from '@mui/icons-material/Wifi';
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, useTheme } from '@mui/material'; import {
Avatar,
Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import * as NetworkApi from 'api/network'; import * as NetworkApi from 'api/network';
@@ -49,7 +58,8 @@ const networkQualityHighlight = ({ rssi }: NetworkStatusType, theme: Theme) => {
return theme.palette.success.main; return theme.palette.success.main;
}; };
export const isWiFi = ({ status }: NetworkStatusType) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED; export const isWiFi = ({ status }: NetworkStatusType) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
export const isEthernet = ({ status }: NetworkStatusType) => export const isEthernet = ({ status }: NetworkStatusType) =>
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED; status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
@@ -61,7 +71,10 @@ const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatusType) => {
}; };
const IPs = (status: NetworkStatusType) => { const IPs = (status: NetworkStatusType) => {
if (!status.local_ipv6 || status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000') { if (
!status.local_ipv6 ||
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000'
) {
return status.local_ip; return status.local_ip;
} }
if (!status.local_ip || status.local_ip === '0.0.0.0') { if (!status.local_ip || status.local_ip === '0.0.0.0') {
@@ -71,7 +84,11 @@ const IPs = (status: NetworkStatusType) => {
}; };
const NetworkStatus: FC = () => { const NetworkStatus: FC = () => {
const { data: data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus); const {
data: data,
send: loadData,
error
} = useRequest(NetworkApi.readNetworkStatus);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -135,7 +152,10 @@ const NetworkStatus: FC = () => {
<SettingsInputAntennaIcon /> <SettingsInputAntennaIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary="SSID (RSSI)" secondary={data.ssid + ' (' + data.rssi + ' dBm)'} /> <ListItemText
primary="SSID (RSSI)"
secondary={data.ssid + ' (' + data.rssi + ' dBm)'}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</> </>
@@ -155,14 +175,20 @@ const NetworkStatus: FC = () => {
<DeviceHubIcon /> <DeviceHubIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.ADDRESS_OF('MAC')} secondary={data.mac_address} /> <ListItemText
primary={LL.ADDRESS_OF('MAC')}
secondary={data.mac_address}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar>#</Avatar> <Avatar>#</Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.NETWORK_SUBNET()} secondary={data.subnet_mask} /> <ListItemText
primary={LL.NETWORK_SUBNET()}
secondary={data.subnet_mask}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>
@@ -171,7 +197,10 @@ const NetworkStatus: FC = () => {
<SettingsInputComponentIcon /> <SettingsInputComponentIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.NETWORK_GATEWAY()} secondary={data.gateway_ip || 'none'} /> <ListItemText
primary={LL.NETWORK_GATEWAY()}
secondary={data.gateway_ip || 'none'}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>
@@ -180,14 +209,22 @@ const NetworkStatus: FC = () => {
<DnsIcon /> <DnsIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.NETWORK_DNS()} secondary={dnsServers(data)} /> <ListItemText
primary={LL.NETWORK_DNS()}
secondary={dnsServers(data)}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</> </>
)} )}
</List> </List>
<ButtonRow> <ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -9,4 +9,6 @@ export interface WiFiConnectionContextValue {
} }
const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue; const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContextValue;
export const WiFiConnectionContext = createContext(WiFiConnectionContextDefaultValue); export const WiFiConnectionContext = createContext(
WiFiConnectionContextDefaultValue
);

View File

@@ -20,7 +20,9 @@ const WiFiNetworkScanner: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(NetworkApi.scanNetworks); // is called on page load to start network scan const { send: scanNetworks, onComplete: onCompleteScanNetworks } = useRequest(
NetworkApi.scanNetworks
); // is called on page load to start network scan
const { const {
data: networkList, data: networkList,
send: getNetworkList, send: getNetworkList,
@@ -51,7 +53,9 @@ const WiFiNetworkScanner: FC = () => {
const renderNetworkScanner = () => { const renderNetworkScanner = () => {
if (!networkList) { if (!networkList) {
return <FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />; return (
<FormLoader message={LL.SCANNING() + '...'} errorMessage={errorMessage} />
);
} }
return <WiFiNetworkSelector networkList={networkList} />; return <WiFiNetworkSelector networkList={networkList} />;
}; };

View File

@@ -4,7 +4,16 @@ import type { FC } from 'react';
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen'; import LockOpenIcon from '@mui/icons-material/LockOpen';
import WifiIcon from '@mui/icons-material/Wifi'; import WifiIcon from '@mui/icons-material/Wifi';
import { Avatar, Badge, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText, useTheme } from '@mui/material'; import {
Avatar,
Badge,
List,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemText,
useTheme
} from '@mui/material';
import type { Theme } from '@mui/material'; import type { Theme } from '@mui/material';
import { MessageBox } from 'components'; import { MessageBox } from 'components';
@@ -60,14 +69,22 @@ const WiFiNetworkSelector: FC<WiFiNetworkSelectorProps> = ({ networkList }) => {
const wifiConnectionContext = useContext(WiFiConnectionContext); const wifiConnectionContext = useContext(WiFiConnectionContext);
const renderNetwork = (network: WiFiNetwork) => ( const renderNetwork = (network: WiFiNetwork) => (
<ListItem key={network.bssid} onClick={() => wifiConnectionContext.selectNetwork(network)}> <ListItem
key={network.bssid}
onClick={() => wifiConnectionContext.selectNetwork(network)}
>
<ListItemAvatar> <ListItemAvatar>
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar> <Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={network.ssid} primary={network.ssid}
secondary={ secondary={
'Security: ' + networkSecurityMode(network) + ', Ch: ' + network.channel + ', bssid: ' + network.bssid 'Security: ' +
networkSecurityMode(network) +
', Ch: ' +
network.channel +
', bssid: ' +
network.bssid
} }
/> />
<ListItemIcon> <ListItemIcon>

View File

@@ -44,7 +44,12 @@ const NTPSettings: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -76,7 +81,13 @@ const NTPSettings: FC = () => {
return ( return (
<> <>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />} control={
<Checkbox
name="enabled"
checked={data.enabled}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_NTP()} label={LL.ENABLE_NTP()}
/> />
<ValidatedTextField <ValidatedTextField

View File

@@ -46,14 +46,19 @@ const NTPStatus: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { send: updateTime } = useRequest((local_time: Time) => NTPApi.updateTime(local_time), { const { send: updateTime } = useRequest(
immediate: false (local_time: Time) => NTPApi.updateTime(local_time),
}); {
immediate: false
}
);
NTPApi.updateTime; NTPApi.updateTime;
const isNtpActive = ({ status }: NTPStatusType) => status === NTPSyncStatus.NTP_ACTIVE; const isNtpActive = ({ status }: NTPStatusType) =>
const isNtpEnabled = ({ status }: NTPStatusType) => status !== NTPSyncStatus.NTP_DISABLED; status === NTPSyncStatus.NTP_ACTIVE;
const isNtpEnabled = ({ status }: NTPStatusType) =>
status !== NTPSyncStatus.NTP_DISABLED;
const ntpStatusHighlight = ({ status }: NTPStatusType, theme: Theme) => { const ntpStatusHighlight = ({ status }: NTPStatusType, theme: Theme) => {
switch (status) { switch (status) {
@@ -68,7 +73,8 @@ const NTPStatus: FC = () => {
} }
}; };
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value); const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
setLocalTime(event.target.value);
const openSetTime = () => { const openSetTime = () => {
setLocalTime(formatLocalDateTime(new Date())); setLocalTime(formatLocalDateTime(new Date()));
@@ -108,7 +114,11 @@ const NTPStatus: FC = () => {
}; };
const renderSetTimeDialog = () => ( const renderSetTimeDialog = () => (
<Dialog sx={dialogStyle} open={settingTime} onClose={() => setSettingTime(false)}> <Dialog
sx={dialogStyle}
open={settingTime}
onClose={() => setSettingTime(false)}
>
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle> <DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}> <Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
@@ -127,7 +137,12 @@ const NTPStatus: FC = () => {
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setSettingTime(false)} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setSettingTime(false)}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button <Button
@@ -179,7 +194,10 @@ const NTPStatus: FC = () => {
<AccessTimeIcon /> <AccessTimeIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.LOCAL_TIME()} secondary={formatDateTime(data.local_time)} /> <ListItemText
primary={LL.LOCAL_TIME()}
secondary={formatDateTime(data.local_time)}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>
@@ -188,14 +206,22 @@ const NTPStatus: FC = () => {
<SwapVerticalCircleIcon /> <SwapVerticalCircleIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={LL.UTC_TIME()} secondary={formatDateTime(data.utc_time)} /> <ListItemText
primary={LL.UTC_TIME()}
secondary={formatDateTime(data.utc_time)}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</List> </List>
<Box display="flex" flexWrap="wrap"> <Box display="flex" flexWrap="wrap">
<Box flexGrow={1}> <Box flexGrow={1}>
<ButtonRow> <ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</ButtonRow> </ButtonRow>
@@ -203,7 +229,12 @@ const NTPStatus: FC = () => {
{data && !isNtpActive(data) && ( {data && !isNtpActive(data) && (
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow> <ButtonRow>
<Button onClick={openSetTime} variant="outlined" color="primary" startIcon={<AccessTimeIcon />}> <Button
onClick={openSetTime}
variant="outlined"
color="primary"
startIcon={<AccessTimeIcon />}
>
{LL.SET_TIME(0)} {LL.SET_TIME(0)}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -43,7 +43,12 @@ const OTASettings: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -67,7 +72,13 @@ const OTASettings: FC = () => {
return ( return (
<> <>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="enabled" checked={data.enabled} onChange={updateFormValue} />} control={
<Checkbox
name="enabled"
checked={data.enabled}
onChange={updateFormValue}
/>
}
label={LL.ENABLE_OTA()} label={LL.ENABLE_OTA()}
/> />
<ValidatedTextField <ValidatedTextField

View File

@@ -30,9 +30,12 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const open = !!username; const open = !!username;
const { data: token, send: generateToken } = useRequest(SecurityApi.generateToken(username), { const { data: token, send: generateToken } = useRequest(
immediate: false SecurityApi.generateToken(username),
}); {
immediate: false
}
);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@@ -41,14 +44,26 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
}, [open]); }, [open]);
return ( return (
<Dialog sx={dialogStyle} onClose={onClose} open={!!username} fullWidth maxWidth="sm"> <Dialog
sx={dialogStyle}
onClose={onClose}
open={!!username}
fullWidth
maxWidth="sm"
>
<DialogTitle>{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle> <DialogTitle>{LL.ACCESS_TOKEN_FOR() + ' ' + username}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
{token ? ( {token ? (
<> <>
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} /> <MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
<Box mt={2} mb={2}> <Box mt={2} mb={2}>
<TextField label="Token" multiline value={token.token} fullWidth contentEditable={false} /> <TextField
label="Token"
multiline
value={token.token}
fullWidth
contentEditable={false}
/>
</Box> </Box>
</> </>
) : ( ) : (
@@ -59,7 +74,12 @@ const GenerateToken: FC<GenerateTokenProps> = ({ username, onClose }) => {
)} )}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<CloseIcon />} variant="outlined" onClick={onClose} color="secondary"> <Button
startIcon={<CloseIcon />}
variant="outlined"
onClick={onClose}
color="secondary"
>
{LL.CLOSE()} {LL.CLOSE()}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -14,9 +14,23 @@ import { Box, Button, IconButton } from '@mui/material';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { BlockNavigation, ButtonRow, FormLoader, MessageBox, SectionContent } from 'components'; import {
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType, UserType } from 'types'; import type { SecuritySettingsType, UserType } from 'types';
@@ -27,10 +41,11 @@ import GenerateToken from './GenerateToken';
import User from './User'; import User from './User';
const ManageUsers: FC = () => { const ManageUsers: FC = () => {
const { loadData, saveData, saving, data, updateDataValue, errorMessage } = useRest<SecuritySettingsType>({ const { loadData, saveData, saving, data, updateDataValue, errorMessage } =
read: SecurityApi.readSecuritySettings, useRest<SecuritySettingsType>({
update: SecurityApi.updateSecuritySettings read: SecurityApi.readSecuritySettings,
}); update: SecurityApi.updateSecuritySettings
});
const [user, setUser] = useState<UserType>(); const [user, setUser] = useState<UserType>();
const [creating, setCreating] = useState<boolean>(false); const [creating, setCreating] = useState<boolean>(false);
@@ -114,7 +129,12 @@ const ManageUsers: FC = () => {
const doneEditingUser = () => { const doneEditingUser = () => {
if (user) { if (user) {
const users = [...data.users.filter((u: { username: string }) => u.username !== user.username), user]; const users = [
...data.users.filter(
(u: { username: string }) => u.username !== user.username
),
user
];
updateDataValue({ ...data, users }); updateDataValue({ ...data, users });
setUser(undefined); setUser(undefined);
setChanged(changed + 1); setChanged(changed + 1);
@@ -148,11 +168,18 @@ const ManageUsers: FC = () => {
} }
// add id to the type, needed for the table // add id to the type, needed for the table
const user_table = data.users.map((u) => ({ ...u, id: u.username })) as UserType2[]; const user_table = data.users.map((u) => ({
...u,
id: u.username
})) as UserType2[];
return ( return (
<> <>
<Table data={{ nodes: user_table }} theme={table_theme} layout={{ custom: true }}> <Table
data={{ nodes: user_table }}
theme={table_theme}
layout={{ custom: true }}
>
{(tableList: UserType2[]) => ( {(tableList: UserType2[]) => (
<> <>
<Header> <Header>
@@ -189,7 +216,9 @@ const ManageUsers: FC = () => {
)} )}
</Table> </Table>
{noAdminConfigured() && <MessageBox level="warning" message={LL.USER_WARNING()} my={2} />} {noAdminConfigured() && (
<MessageBox level="warning" message={LL.USER_WARNING()} my={2} />
)}
<Box display="flex" flexWrap="wrap"> <Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}> <Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
@@ -221,7 +250,12 @@ const ManageUsers: FC = () => {
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow> <ButtonRow>
<Button startIcon={<PersonAddIcon />} variant="outlined" color="secondary" onClick={createUser}> <Button
startIcon={<PersonAddIcon />}
variant="outlined"
color="secondary"
onClick={createUser}
>
{LL.ADD(0)} {LL.ADD(0)}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -8,7 +8,14 @@ import { Button } from '@mui/material';
import * as SecurityApi from 'api/security'; import * as SecurityApi from 'api/security';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import { BlockNavigation, ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField } from 'components'; import {
BlockNavigation,
ButtonRow,
FormLoader,
MessageBox,
SectionContent,
ValidatedPasswordField
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType } from 'types'; import type { SecuritySettingsType } from 'types';
@@ -37,7 +44,12 @@ const SecuritySettings: FC = () => {
const authenticatedContext = useContext(AuthenticatedContext); const authenticatedContext = useContext(AuthenticatedContext);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const content = () => { const content = () => {
if (!data) { if (!data) {

View File

@@ -4,12 +4,23 @@ import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import PersonAddIcon from '@mui/icons-material/PersonAdd'; import PersonAddIcon from '@mui/icons-material/PersonAdd';
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from '@mui/icons-material/Save';
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'; import {
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle
} from '@mui/material';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import type Schema from 'async-validator'; import type Schema from 'async-validator';
import type { ValidateFieldsError } from 'async-validator'; import type { ValidateFieldsError } from 'async-validator';
import { BlockFormControlLabel, ValidatedPasswordField, ValidatedTextField } from 'components'; import {
BlockFormControlLabel,
ValidatedPasswordField,
ValidatedTextField
} from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { UserType } from 'types'; import type { UserType } from 'types';
import { updateValue } from 'utils'; import { updateValue } from 'utils';
@@ -26,7 +37,14 @@ interface UserFormProps {
onCancelEditing: () => void; onCancelEditing: () => void;
} }
const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEditing, onCancelEditing }) => { const User: FC<UserFormProps> = ({
creating,
validator,
user,
setUser,
onDoneEditing,
onCancelEditing
}) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const updateFormValue = updateValue(setUser); const updateFormValue = updateValue(setUser);
@@ -52,7 +70,13 @@ const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEdi
}; };
return ( return (
<Dialog sx={dialogStyle} onClose={onCancelEditing} open={!!user} fullWidth maxWidth="sm"> <Dialog
sx={dialogStyle}
onClose={onCancelEditing}
open={!!user}
fullWidth
maxWidth="sm"
>
{user && ( {user && (
<> <>
<DialogTitle id="user-form-dialog-title"> <DialogTitle id="user-form-dialog-title">
@@ -81,12 +105,23 @@ const User: FC<UserFormProps> = ({ creating, validator, user, setUser, onDoneEdi
margin="normal" margin="normal"
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox name="admin" checked={user.admin} onChange={updateFormValue} />} control={
<Checkbox
name="admin"
checked={user.admin}
onChange={updateFormValue}
/>
}
label={LL.IS_ADMIN(1)} label={LL.IS_ADMIN(1)}
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onCancelEditing} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onCancelEditing}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button <Button

View File

@@ -8,7 +8,16 @@ import MemoryIcon from '@mui/icons-material/Memory';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import SdCardAlertIcon from '@mui/icons-material/SdCardAlert'; import SdCardAlertIcon from '@mui/icons-material/SdCardAlert';
import SdStorageIcon from '@mui/icons-material/SdStorage'; import SdStorageIcon from '@mui/icons-material/SdStorage';
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@mui/material'; import {
Avatar,
Box,
Button,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
@@ -25,7 +34,11 @@ const ESPSystemStatus: FC = () => {
useLayoutTitle(LL.STATUS_OF('ESP32')); useLayoutTitle(LL.STATUS_OF('ESP32'));
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true }); const {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readESPSystemStatus, { force: true });
const content = () => { const content = () => {
if (!data) { if (!data) {
@@ -41,7 +54,10 @@ const ESPSystemStatus: FC = () => {
<DevicesIcon /> <DevicesIcon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary="SDK" secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version} /> <ListItemText
primary="SDK"
secondary={data.arduino_version + ' / ESP-IDF ' + data.sdk_version}
/>
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
<ListItem> <ListItem>
@@ -75,7 +91,12 @@ const ESPSystemStatus: FC = () => {
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={LL.HEAP()} primary={LL.HEAP()}
secondary={formatNumber(data.free_heap) + ' KB / ' + formatNumber(data.max_alloc_heap) + ' KB '} secondary={
formatNumber(data.free_heap) +
' KB / ' +
formatNumber(data.max_alloc_heap) +
' KB '
}
/> />
</ListItem> </ListItem>
{data.psram_size !== undefined && data.free_psram !== undefined && ( {data.psram_size !== undefined && data.free_psram !== undefined && (
@@ -89,7 +110,12 @@ const ESPSystemStatus: FC = () => {
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={LL.PSRAM()} primary={LL.PSRAM()}
secondary={formatNumber(data.psram_size) + ' KB / ' + formatNumber(data.free_psram) + ' KB'} secondary={
formatNumber(data.psram_size) +
' KB / ' +
formatNumber(data.free_psram) +
' KB'
}
/> />
</ListItem> </ListItem>
</> </>
@@ -104,7 +130,10 @@ const ESPSystemStatus: FC = () => {
<ListItemText <ListItemText
primary={LL.FLASH()} primary={LL.FLASH()}
secondary={ secondary={
formatNumber(data.flash_chip_size) + ' KB / ' + (data.flash_chip_speed / 1000000).toFixed(0) + ' MHz' formatNumber(data.flash_chip_size) +
' KB / ' +
(data.flash_chip_speed / 1000000).toFixed(0) +
' MHz'
} }
/> />
</ListItem> </ListItem>
@@ -118,7 +147,12 @@ const ESPSystemStatus: FC = () => {
<ListItemText <ListItemText
primary={LL.APPSIZE()} primary={LL.APPSIZE()}
secondary={ secondary={
data.partition + ': ' + formatNumber(data.app_used) + ' KB / ' + formatNumber(data.app_free) + ' KB' data.partition +
': ' +
formatNumber(data.app_used) +
' KB / ' +
formatNumber(data.app_free) +
' KB'
} }
/> />
</ListItem> </ListItem>
@@ -131,7 +165,12 @@ const ESPSystemStatus: FC = () => {
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={LL.FILESYSTEM()} primary={LL.FILESYSTEM()}
secondary={formatNumber(data.fs_used) + ' KB / ' + formatNumber(data.fs_free) + ' KB'} secondary={
formatNumber(data.fs_used) +
' KB / ' +
formatNumber(data.fs_free) +
' KB'
}
/> />
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
@@ -139,7 +178,12 @@ const ESPSystemStatus: FC = () => {
<Box display="flex" flexWrap="wrap"> <Box display="flex" flexWrap="wrap">
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}> <Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
<ButtonRow> <ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -36,7 +36,12 @@ const RestartMonitor: FC = () => {
useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]); useEffect(() => () => timeoutId && clearTimeout(timeoutId), [timeoutId]);
return <FormLoader message={LL.APPLICATION_RESTARTING() + '...'} errorMessage={failed ? 'Timed out' : undefined} />; return (
<FormLoader
message={LL.APPLICATION_RESTARTING() + '...'}
errorMessage={failed ? 'Timed out' : undefined}
/>
);
}; };
export default RestartMonitor; export default RestartMonitor;

View File

@@ -24,7 +24,11 @@ const System: FC = () => {
<RouterTabs value={routerTab}> <RouterTabs value={routerTab}>
<Tab value="status" label={LL.STATUS_OF('')} /> <Tab value="status" label={LL.STATUS_OF('')} />
<Tab value="activity" label={LL.ACTIVITY()} /> <Tab value="activity" label={LL.ACTIVITY()} />
<Tab disabled={!me.admin} value="log" label={me.admin ? LL.LOG_OF('') : ''} /> <Tab
disabled={!me.admin}
value="log"
label={me.admin ? LL.LOG_OF('') : ''}
/>
</RouterTabs> </RouterTabs>
<Routes> <Routes>
<Route path="status" element={<SystemStatus />} /> <Route path="status" element={<SystemStatus />} />

View File

@@ -4,14 +4,28 @@ import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Checkbox, Grid, MenuItem, TextField, styled } from '@mui/material'; import {
Box,
Button,
Checkbox,
Grid,
MenuItem,
TextField,
styled
} from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { fetchLogES } from 'api/system'; import { fetchLogES } from 'api/system';
import { useSSE } from '@alova/scene-react'; import { useSSE } from '@alova/scene-react';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { BlockFormControlLabel, BlockNavigation, FormLoader, SectionContent, useLayoutTitle } from 'components'; import {
BlockFormControlLabel,
BlockNavigation,
FormLoader,
SectionContent,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { LogEntry, LogSettings } from 'types'; import type { LogEntry, LogSettings } from 'types';
import { LogLevel } from 'types'; import { LogLevel } from 'types';
@@ -25,8 +39,10 @@ const LogEntryLine = styled('div')(() => ({
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
})); }));
const topOffset = () => document.getElementById('log-window')?.getBoundingClientRect().bottom || 0; const topOffset = () =>
const leftOffset = () => document.getElementById('log-window')?.getBoundingClientRect().left || 0; document.getElementById('log-window')?.getBoundingClientRect().bottom || 0;
const leftOffset = () =>
document.getElementById('log-window')?.getBoundingClientRect().left || 0;
const levelLabel = (level: LogLevel) => { const levelLabel = (level: LogLevel) => {
switch (level) { switch (level) {
@@ -50,16 +66,30 @@ const SystemLog: FC = () => {
useLayoutTitle(LL.LOG_OF('')); useLayoutTitle(LL.LOG_OF(''));
const { loadData, data, updateDataValue, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } = const {
useRest<LogSettings>({ loadData,
read: SystemApi.readLogSettings, data,
update: SystemApi.updateLogSettings updateDataValue,
}); origData,
dirtyFlags,
setDirtyFlags,
blocker,
saveData,
errorMessage
} = useRest<LogSettings>({
read: SystemApi.readLogSettings,
update: SystemApi.updateLogSettings
});
const [logEntries, setLogEntries] = useState<LogEntry[]>([]); const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
const [lastIndex, setLastIndex] = useState<number>(0); const [lastIndex, setLastIndex] = useState<number>(0);
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
const { onMessage, onError } = useSSE(fetchLogES, { const { onMessage, onError } = useSSE(fetchLogES, {
@@ -102,10 +132,14 @@ const SystemLog: FC = () => {
const onDownload = () => { const onDownload = () => {
let result = ''; let result = '';
for (const i of logEntries) { for (const i of logEntries) {
result += i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n'; result +=
i.t + ' ' + levelLabel(i.l) + ' ' + i.i + ': [' + i.n + '] ' + i.m + '\n';
} }
const a = document.createElement('a'); const a = document.createElement('a');
a.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(result)); a.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(result)
);
a.setAttribute('download', 'log.txt'); a.setAttribute('download', 'log.txt');
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
@@ -134,7 +168,13 @@ const SystemLog: FC = () => {
return ( return (
<> <>
<Grid container spacing={3} direction="row" justifyContent="flex-start" alignItems="center"> <Grid
container
spacing={3}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<Grid item xs={2}> <Grid item xs={2}>
<TextField <TextField
name="level" name="level"
@@ -173,7 +213,13 @@ const SystemLog: FC = () => {
</Grid> </Grid>
<Grid item> <Grid item>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.compact} onChange={updateFormValue} name="compact" />} control={
<Checkbox
checked={data.compact}
onChange={updateFormValue}
name="compact"
/>
}
label={LL.COMPACT()} label={LL.COMPACT()}
/> />
</Grid> </Grid>
@@ -185,7 +231,12 @@ const SystemLog: FC = () => {
} }
}} }}
> >
<Button startIcon={<DownloadIcon />} variant="outlined" color="secondary" onClick={onDownload}> <Button
startIcon={<DownloadIcon />}
variant="outlined"
color="secondary"
onClick={onDownload}
>
{LL.EXPORT()} {LL.EXPORT()}
</Button> </Button>
{dirtyFlags && dirtyFlags.length !== 0 && ( {dirtyFlags && dirtyFlags.length !== 0 && (

View File

@@ -49,7 +49,11 @@ const SystemStatus: FC = () => {
const [confirmScan, setConfirmScan] = useState<boolean>(false); const [confirmScan, setConfirmScan] = useState<boolean>(false);
const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus, { force: true }); const {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readSystemStatus, { force: true });
const { send: scanDevices } = useRequest(EMSESP.scanDevices, { const { send: scanDevices } = useRequest(EMSESP.scanDevices, {
immediate: false immediate: false
@@ -134,7 +138,8 @@ const SystemStatus: FC = () => {
} }
}; };
const activeHighlight = (value: boolean) => (value ? theme.palette.success.main : theme.palette.info.main); const activeHighlight = (value: boolean) =>
value ? theme.palette.success.main : theme.palette.info.main;
const scan = async () => { const scan = async () => {
await scanDevices() await scanDevices()
@@ -148,14 +153,28 @@ const SystemStatus: FC = () => {
}; };
const renderScanDialog = () => ( const renderScanDialog = () => (
<Dialog sx={dialogStyle} open={confirmScan} onClose={() => setConfirmScan(false)}> <Dialog
sx={dialogStyle}
open={confirmScan}
onClose={() => setConfirmScan(false)}
>
<DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle> <DialogTitle>{LL.SCAN_DEVICES()}</DialogTitle>
<DialogContent dividers>{LL.EMS_SCAN()}</DialogContent> <DialogContent dividers>{LL.EMS_SCAN()}</DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmScan(false)} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmScan(false)}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={<PermScanWifiIcon />} variant="outlined" onClick={scan} color="primary"> <Button
startIcon={<PermScanWifiIcon />}
variant="outlined"
onClick={scan}
color="primary"
>
{LL.SCAN()} {LL.SCAN()}
</Button> </Button>
</DialogActions> </DialogActions>
@@ -282,7 +301,12 @@ const SystemStatus: FC = () => {
{renderScanDialog()} {renderScanDialog()}
<Box mt={2} display="flex" flexWrap="wrap"> <Box mt={2} display="flex" flexWrap="wrap">
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</Box> </Box>

View File

@@ -8,7 +8,12 @@ import * as SystemApi from 'api/system';
import * as EMSESP from 'project/api'; import * as EMSESP from 'project/api';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { FormLoader, SectionContent, SingleUpload, useLayoutTitle } from 'components'; import {
FormLoader,
SectionContent,
SingleUpload,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { APIcall } from 'project/types'; import type { APIcall } from 'project/types';
@@ -19,23 +24,40 @@ const UploadDownload: FC = () => {
const [restarting, setRestarting] = useState<boolean>(); const [restarting, setRestarting] = useState<boolean>();
const [md5, setMd5] = useState<string>(); const [md5, setMd5] = useState<string>();
const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(EMSESP.getSettings(), { const { send: getSettings, onSuccess: onSuccessGetSettings } = useRequest(
immediate: false EMSESP.getSettings(),
}); {
const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } = useRequest(EMSESP.getCustomizations(), { immediate: false
immediate: false }
}); );
const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(EMSESP.getEntities(), { const { send: getCustomizations, onSuccess: onSuccessGetCustomizations } =
immediate: false useRequest(EMSESP.getCustomizations(), {
}); immediate: false
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(EMSESP.getSchedule(), { });
immediate: false const { send: getEntities, onSuccess: onSuccessGetEntities } = useRequest(
}); EMSESP.getEntities(),
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data: APIcall) => EMSESP.API(data), { {
immediate: false immediate: false
}); }
);
const { send: getSchedule, onSuccess: onSuccessGetSchedule } = useRequest(
EMSESP.getSchedule(),
{
immediate: false
}
);
const { send: getAPI, onSuccess: onGetAPI } = useRequest(
(data: APIcall) => EMSESP.API(data),
{
immediate: false
}
);
const { data: data, send: loadData, error } = useRequest(SystemApi.readESPSystemStatus, { force: true }); const {
data: data,
send: loadData,
error
} = useRequest(SystemApi.readESPSystemStatus, { force: true });
const { data: latestVersion } = useRequest(SystemApi.getStableVersion, { const { data: latestVersion } = useRequest(SystemApi.getStableVersion, {
immediate: true, immediate: true,
@@ -50,11 +72,17 @@ const UploadDownload: FC = () => {
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/'; const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/'; const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const STABLE_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md'; const STABLE_RELNOTES_URL =
const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md'; 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
const getBinURL = (v: string) => const getBinURL = (v: string) =>
'EMS-ESP-' + v.replaceAll('.', '_') + '-' + data.esp_platform.replaceAll('-', '_') + '.bin'; 'EMS-ESP-' +
v.replaceAll('.', '_') +
'-' +
data.esp_platform.replaceAll('-', '_') +
'.bin';
const { const {
loading: isUploading, loading: isUploading,
@@ -115,8 +143,11 @@ const UploadDownload: FC = () => {
saveFile(event.data, 'schedule.json'); saveFile(event.data, 'schedule.json');
}); });
onGetAPI((event) => { onGetAPI((event) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access saveFile(
saveFile(event.data, event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'); event.data,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'
);
}); });
const downloadSettings = async () => { const downloadSettings = async () => {
@@ -170,7 +201,8 @@ const UploadDownload: FC = () => {
<b>{data.emsesp_version}</b>&nbsp;({data.esp_platform}) <b>{data.emsesp_version}</b>&nbsp;({data.esp_platform})
{latestVersion && ( {latestVersion && (
<Box mt={2}> <Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.OFFICIAL()}&nbsp;{LL.RELEASE_IS()}&nbsp;<b>{latestVersion}</b> {LL.THE_LATEST()}&nbsp;{LL.OFFICIAL()}&nbsp;{LL.RELEASE_IS()}
&nbsp;<b>{latestVersion}</b>
&nbsp;( &nbsp;(
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary"> <Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()} {LL.RELEASE_NOTES()}
@@ -178,7 +210,13 @@ const UploadDownload: FC = () => {
)&nbsp;( )&nbsp;(
<Link <Link
target="_blank" target="_blank"
href={STABLE_URL + 'v' + latestVersion + '/' + getBinURL(latestVersion as string)} href={
STABLE_URL +
'v' +
latestVersion +
'/' +
getBinURL(latestVersion as string)
}
color="primary" color="primary"
> >
{LL.DOWNLOAD(1)} {LL.DOWNLOAD(1)}
@@ -188,14 +226,19 @@ const UploadDownload: FC = () => {
)} )}
{latestDevVersion && ( {latestDevVersion && (
<Box mt={2}> <Box mt={2}>
{LL.THE_LATEST()}&nbsp;{LL.DEVELOPMENT()}&nbsp;{LL.RELEASE_IS()}&nbsp; {LL.THE_LATEST()}&nbsp;{LL.DEVELOPMENT()}&nbsp;{LL.RELEASE_IS()}
&nbsp;
<b>{latestDevVersion}</b> <b>{latestDevVersion}</b>
&nbsp;( &nbsp;(
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary"> <Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
{LL.RELEASE_NOTES()} {LL.RELEASE_NOTES()}
</Link> </Link>
)&nbsp;( )&nbsp;(
<Link target="_blank" href={DEV_URL + getBinURL(latestDevVersion as string)} color="primary"> <Link
target="_blank"
href={DEV_URL + getBinURL(latestDevVersion as string)}
color="primary"
>
{LL.DOWNLOAD(1)} {LL.DOWNLOAD(1)}
</Link> </Link>
) )
@@ -219,7 +262,12 @@ const UploadDownload: FC = () => {
<Typography variant="body2">{'MD5: ' + md5}</Typography> <Typography variant="body2">{'MD5: ' + md5}</Typography>
</Box> </Box>
)} )}
<SingleUpload onDrop={startUpload} onCancel={cancelUpload} isUploading={isUploading} progress={progress} /> <SingleUpload
onDrop={startUpload}
onCancel={cancelUpload}
isUploading={isUploading}
progress={progress}
/>
{!isUploading && ( {!isUploading && (
<> <>
<Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 4, pb: 2 }} variant="h6" color="primary">
@@ -307,7 +355,9 @@ const UploadDownload: FC = () => {
); );
}; };
return <SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>; return (
<SectionContent>{restarting ? <RestartMonitor /> : content()}</SectionContent>
);
}; };
export default UploadDownload; export default UploadDownload;

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const de: Translation = { const de: Translation = {
LANGUAGE: 'Sprache', LANGUAGE: 'Sprache',
RETRY: 'Neuer Versuch', RETRY: 'Neuer Versuch',
@@ -208,7 +206,8 @@ const de: Translation = {
USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren', USER_WARNING: 'Sie müssen mindestens einen Admin-Nutzer konfigurieren',
ADD: 'Hinzufügen', ADD: 'Hinzufügen',
ACCESS_TOKEN_FOR: 'Zugangs-Token für', 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.', 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', GENERATING_TOKEN: 'Erzeuge Token',
USER: 'Nutzer', USER: 'Nutzer',
MODIFY: 'Ändern', MODIFY: 'Ändern',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const en: Translation = { const en: Translation = {
LANGUAGE: 'Language', LANGUAGE: 'Language',
RETRY: 'Retry', RETRY: 'Retry',
@@ -208,7 +206,8 @@ const en: Translation = {
USER_WARNING: 'You must have at least one admin user configured', USER_WARNING: 'You must have at least one admin user configured',
ADD: 'Add', ADD: 'Add',
ACCESS_TOKEN_FOR: 'Access Token for', 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.', 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', GENERATING_TOKEN: 'Generating token',
USER: 'User', USER: 'User',
MODIFY: 'Modify', MODIFY: 'Modify',
@@ -325,10 +324,10 @@ const en: Translation = {
ALWAYS: 'Always', ALWAYS: 'Always',
ACTIVITY: 'Activity', ACTIVITY: 'Activity',
CONFIGURE: 'Configure {0}', CONFIGURE: 'Configure {0}',
SYSTEM_MEMORY: 'System Memory', SYSTEM_MEMORY: 'System Memory',
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings',
SECURITY_1: 'Add or remove users', SECURITY_1: 'Add or remove users',
UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware', UPLOAD_DOWNLOAD_1: 'Upload/Download Settings and Firmware',
MODULE: 'Module' // TODO translate MODULE: 'Module' // TODO translate
}; };

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const fr: Translation = { const fr: Translation = {
LANGUAGE: 'Langue', LANGUAGE: 'Langue',
RETRY: 'Réessayer', RETRY: 'Réessayer',
@@ -9,7 +7,7 @@ const fr: Translation = {
IS_REQUIRED: '{0} est requis', IS_REQUIRED: '{0} est requis',
SIGN_IN: 'Se connecter', SIGN_IN: 'Se connecter',
SIGN_OUT: 'Se déconnecter', SIGN_OUT: 'Se déconnecter',
USERNAME: 'Nom d\'utilisateur', USERNAME: "Nom d'utilisateur",
PASSWORD: 'Mot de passe', PASSWORD: 'Mot de passe',
SU_PASSWORD: 'Mot de passe su', SU_PASSWORD: 'Mot de passe su',
SETTINGS_OF: 'Paramètres {0}', SETTINGS_OF: 'Paramètres {0}',
@@ -28,13 +26,13 @@ const fr: Translation = {
ENTITIES: 'Entités', ENTITIES: 'Entités',
REFRESH: 'Rafraîchir', REFRESH: 'Rafraîchir',
EXPORT: 'Exporter', EXPORT: 'Exporter',
DEVICE_DETAILS: 'Détails de l\'appareil', DEVICE_DETAILS: "Détails de l'appareil",
ID_OF: 'ID {0}', ID_OF: 'ID {0}',
DEVICE: 'Appareil', DEVICE: 'Appareil',
PRODUCT: 'Produit', PRODUCT: 'Produit',
VERSION: 'Version', VERSION: 'Version',
BRAND: 'Marque', BRAND: 'Marque',
ENTITY_NAME: 'Nom de l\'entité', ENTITY_NAME: "Nom de l'entité",
VALUE: 'Valeur', VALUE: 'Valeur',
DEVICES: 'Appareils', DEVICES: 'Appareils',
SENSORS: 'Capteurs', SENSORS: 'Capteurs',
@@ -88,7 +86,7 @@ const fr: Translation = {
'Lectures capteurs de température', 'Lectures capteurs de température',
'Lectures capteurs analogiques', 'Lectures capteurs analogiques',
'Publications MQTT', 'Publications MQTT',
'Appels à l\'API', "Appels à l'API",
'Messages Syslog' 'Messages Syslog'
], ],
NUM_DEVICES: '{num} Appareil{{s}}', NUM_DEVICES: '{num} Appareil{{s}}',
@@ -98,11 +96,11 @@ const fr: Translation = {
NUM_SECONDS: '{num} seconde{{s}}', NUM_SECONDS: '{num} seconde{{s}}',
NUM_HOURS: '{num} heure{{s}}', NUM_HOURS: '{num} heure{{s}}',
NUM_MINUTES: '{num} minute{{s}}', NUM_MINUTES: '{num} minute{{s}}',
APPLICATION_SETTINGS: 'Paramètres de l\'application', APPLICATION_SETTINGS: "Paramètres de l'application",
CUSTOMIZATIONS: 'Personnalisation', CUSTOMIZATIONS: 'Personnalisation',
APPLICATION_RESTARTING: 'EMS-ESP redémarre', APPLICATION_RESTARTING: 'EMS-ESP redémarre',
INTERFACE_BOARD_PROFILE: 'Profile de carte d\'interface', INTERFACE_BOARD_PROFILE: "Profile de carte d'interface",
BOARD_PROFILE_TEXT: 'Sélectionnez un profil de carte d\'interface préconfiguré dans la liste ci-dessous ou choisissez Personnalisé pour configurer vos propres paramètres matériels', BOARD_PROFILE_TEXT: "Sélectionnez un profil de carte d'interface préconfiguré dans la liste ci-dessous ou choisissez Personnalisé pour configurer vos propres paramètres matériels",
BOARD_PROFILE: 'Profil de carte', BOARD_PROFILE: 'Profil de carte',
CUSTOM: 'Personnalisé', CUSTOM: 'Personnalisé',
GPIO_OF: 'GPIO {0}', GPIO_OF: 'GPIO {0}',
@@ -119,14 +117,14 @@ const fr: Translation = {
ENABLE_TELNET: 'Activer la console Telnet', ENABLE_TELNET: 'Activer la console Telnet',
ENABLE_ANALOG: 'Activer les capteurs analogiques', ENABLE_ANALOG: 'Activer les capteurs analogiques',
CONVERT_FAHRENHEIT: 'Convertir les températures en Fahrenheit', CONVERT_FAHRENHEIT: 'Convertir les températures en Fahrenheit',
BYPASS_TOKEN: 'Contourner l\'autorisation du jeton d\'accès sur les appels API', BYPASS_TOKEN: "Contourner l'autorisation du jeton d'accès sur les appels API",
READONLY: 'Activer le mode lecture uniquement (bloque toutes les commandes EMS sortantes en écriture Tx)', READONLY: 'Activer le mode lecture uniquement (bloque toutes les commandes EMS sortantes en écriture Tx)',
UNDERCLOCK_CPU: 'Underclock du CPU', UNDERCLOCK_CPU: 'Underclock du CPU',
HEATINGOFF: 'Start boiler with forced heating off', // TODO translate HEATINGOFF: 'Start boiler with forced heating off', // TODO translate
ENABLE_SHOWER_TIMER: 'Activer la minuterie de la douche', ENABLE_SHOWER_TIMER: 'Activer la minuterie de la douche',
ENABLE_SHOWER_ALERT: 'Activer les alertes de durée de douche', ENABLE_SHOWER_ALERT: 'Activer les alertes de durée de douche',
TRIGGER_TIME: 'Durée avant déclenchement', TRIGGER_TIME: 'Durée avant déclenchement',
COLD_SHOT_DURATION: 'Durée du coup d\'eau froide', COLD_SHOT_DURATION: "Durée du coup d'eau froide",
FORMATTING_OPTIONS: 'Options de mise en forme', FORMATTING_OPTIONS: 'Options de mise en forme',
BOOLEAN_FORMAT_DASHBOARD: 'Tableau de bord du format booléen', BOOLEAN_FORMAT_DASHBOARD: 'Tableau de bord du format booléen',
BOOLEAN_FORMAT_API: 'Format booléen API/MQTT', BOOLEAN_FORMAT_API: 'Format booléen API/MQTT',
@@ -150,8 +148,8 @@ const fr: Translation = {
CUSTOMIZATIONS_SAVED: 'Personnalisations enregistrées', CUSTOMIZATIONS_SAVED: 'Personnalisations enregistrées',
CUSTOMIZATIONS_HELP_1: 'Sélectionnez un appareil et personnalisez les options des entités ou cliquez pour renommer', CUSTOMIZATIONS_HELP_1: 'Sélectionnez un appareil et personnalisez les options des entités ou cliquez pour renommer',
CUSTOMIZATIONS_HELP_2: 'marquer comme favori', CUSTOMIZATIONS_HELP_2: 'marquer comme favori',
CUSTOMIZATIONS_HELP_3: 'désactiver l\'action d\'écriture', CUSTOMIZATIONS_HELP_3: "désactiver l'action d'écriture",
CUSTOMIZATIONS_HELP_4: 'exclure de MQTT et de l\'API', CUSTOMIZATIONS_HELP_4: "exclure de MQTT et de l'API",
CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord', CUSTOMIZATIONS_HELP_5: 'cacher du Tableau de bord',
CUSTOMIZATIONS_HELP_6: 'remove from memory', // TODO translate CUSTOMIZATIONS_HELP_6: 'remove from memory', // TODO translate
SELECT_DEVICE: 'Sélectionnez un appareil', SELECT_DEVICE: 'Sélectionnez un appareil',
@@ -163,7 +161,7 @@ const fr: Translation = {
HELP_INFORMATION_1: 'Visitez le wiki en ligne pour obtenir des instructions sur la façon de configurer EMS-ESP.', HELP_INFORMATION_1: 'Visitez le wiki en ligne pour obtenir des instructions sur la façon de configurer EMS-ESP.',
HELP_INFORMATION_2: 'Pour une discussion en direct avec la communauté, rejoignez notre serveur Discord', HELP_INFORMATION_2: 'Pour une discussion en direct avec la communauté, rejoignez notre serveur Discord',
HELP_INFORMATION_3: 'Pour demander une fonctionnalité ou signaler un problème', HELP_INFORMATION_3: 'Pour demander une fonctionnalité ou signaler un problème',
HELP_INFORMATION_4: 'N\'oubliez pas de télécharger et de joindre les informations relatives à votre système pour obtenir une réponse plus rapide lorsque vous signalez un problème', HELP_INFORMATION_4: "N'oubliez pas de télécharger et de joindre les informations relatives à votre système pour obtenir une réponse plus rapide lorsque vous signalez un problème",
HELP_INFORMATION_5: 'EMS-ESP est un projet libre et open-source. Merci de soutenir son développement futur en lui donnant une étoile sur Github !', HELP_INFORMATION_5: 'EMS-ESP est un projet libre et open-source. Merci de soutenir son développement futur en lui donnant une étoile sur Github !',
UPLOAD: 'Upload', UPLOAD: 'Upload',
DOWNLOAD: '{{D|d|d}}ownload', DOWNLOAD: '{{D|d|d}}ownload',
@@ -178,8 +176,8 @@ const fr: Translation = {
CLOSE: 'Fermer', CLOSE: 'Fermer',
USE: 'Utiliser', USE: 'Utiliser',
FACTORY_RESET: 'Réinitialisation', FACTORY_RESET: 'Réinitialisation',
SYSTEM_FACTORY_TEXT: 'L\'appareil a été réinitialisé et va maintenant redémarrer', SYSTEM_FACTORY_TEXT: "L'appareil a été réinitialisé et va maintenant redémarrer",
SYSTEM_FACTORY_TEXT_DIALOG: 'Êtes-vous sûr de vouloir réinitialiser l\'appareil à ses paramètres d\'usine ?', SYSTEM_FACTORY_TEXT_DIALOG: "Êtes-vous sûr de vouloir réinitialiser l'appareil à ses paramètres d'usine ?",
THE_LATEST: 'La dernière', THE_LATEST: 'La dernière',
OFFICIAL: 'officielle', OFFICIAL: 'officielle',
DEVELOPMENT: 'développement', DEVELOPMENT: 'développement',
@@ -195,10 +193,12 @@ const fr: Translation = {
BUFFER_SIZE: 'Max taille du buffer', BUFFER_SIZE: 'Max taille du buffer',
COMPACT: 'Compact', COMPACT: 'Compact',
ENABLE_OTA: 'Activer les updates OTA', ENABLE_OTA: 'Activer les updates OTA',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Télécharger les personnalisations d\'entités', DOWNLOAD_CUSTOMIZATION_TEXT: "Télécharger les personnalisations d'entités",
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', // TODO translate DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', // TODO translate
DOWNLOAD_SETTINGS_TEXT: 'Téléchargez les paramètres de l\'application. Soyez prudent lorsque vous partagez vos paramètres car ce fichier contient des mots de passe et d\'autres informations système sensibles.', DOWNLOAD_SETTINGS_TEXT:
UPLOAD_TEXT: 'Téléchargez un nouveau fichier de firmware (.bin), un fichier de paramètres ou de personnalisations (.json) ci-dessous, pour une validation optionnelle téléchargez d\'abord un fichier (.md5)', "Téléchargez les paramètres de l'application. Soyez prudent lorsque vous partagez vos paramètres car ce fichier contient des mots de passe et d'autres informations système sensibles.",
UPLOAD_TEXT:
"Téléchargez un nouveau fichier de firmware (.bin), un fichier de paramètres ou de personnalisations (.json) ci-dessous, pour une validation optionnelle téléchargez d'abord un fichier (.md5)",
UPLOADING: 'Téléchargement', UPLOADING: 'Téléchargement',
UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici', UPLOAD_DROP_TEXT: 'Déposer le fichier ou cliquer ici',
ERROR: 'Erreur inattendue, veuillez réessayer', ERROR: 'Erreur inattendue, veuillez réessayer',
@@ -207,12 +207,13 @@ const fr: Translation = {
IS_ADMIN: 'admin', IS_ADMIN: 'admin',
USER_WARNING: 'Vous devez avoir au moins un utilisateur admin configuré', USER_WARNING: 'Vous devez avoir au moins un utilisateur admin configuré',
ADD: 'Ajouter', ADD: 'Ajouter',
ACCESS_TOKEN_FOR: 'Jeton d\'accès pour', ACCESS_TOKEN_FOR: "Jeton d'accès pour",
ACCESS_TOKEN_TEXT: 'Le jeton ci-dessous est utilisé avec les appels d\'API REST qui nécessitent une autorisation. Il peut être passé soit en tant que jeton Bearer dans l\'en-tête Authorization, soit dans le paramètre de requête URL access_token.', ACCESS_TOKEN_TEXT:
"Le jeton ci-dessous est utilisé avec les appels d'API REST qui nécessitent une autorisation. Il peut être passé soit en tant que jeton Bearer dans l'en-tête Authorization, soit dans le paramètre de requête URL access_token.",
GENERATING_TOKEN: 'Génération de jeton', GENERATING_TOKEN: 'Génération de jeton',
USER: 'Utilisateur', USER: 'Utilisateur',
MODIFY: 'Modifier', MODIFY: 'Modifier',
SU_TEXT: 'Le mot de passe su (super utilisateur) est utilisé pour signer les jetons d\'authentification et activer les privilèges d\'administrateur dans la console.', SU_TEXT: "Le mot de passe su (super utilisateur) est utilisé pour signer les jetons d'authentification et activer les privilèges d'administrateur dans la console.",
NOT_ENABLED: 'Non activé', NOT_ENABLED: 'Non activé',
ERRORS_OF: 'Erreurs {0}', ERRORS_OF: 'Erreurs {0}',
DISCONNECT_REASON: 'Raison de la déconnexion', DISCONNECT_REASON: 'Raison de la déconnexion',
@@ -240,7 +241,7 @@ const fr: Translation = {
MQTT_QUEUE: 'Queue MQTT', MQTT_QUEUE: 'Queue MQTT',
DEFAULT: 'Défaut', DEFAULT: 'Défaut',
MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate MQTT_ENTITY_FORMAT: 'Entity ID format', // TODO translate
MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)',// TODO translate MQTT_ENTITY_FORMAT_0: 'Single instance, long name (v3.4)', // TODO translate
MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate MQTT_ENTITY_FORMAT_1: 'Single instance, short name', // TODO translate
MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate MQTT_ENTITY_FORMAT_2: 'Multiple instances, short name', // TODO translate
MQTT_CLEAN_SESSION: 'Flag Clean Session', MQTT_CLEAN_SESSION: 'Flag Clean Session',
@@ -248,15 +249,15 @@ const fr: Translation = {
INACTIVE: 'Inactif', INACTIVE: 'Inactif',
ACTIVE: 'Actif', ACTIVE: 'Actif',
UNKNOWN: 'Inconnu', UNKNOWN: 'Inconnu',
SET_TIME: 'Définir l\'heure', SET_TIME: "Définir l'heure",
SET_TIME_TEXT: 'Entrer la date et l\'heure locale ci-dessous pour régler l\'heure', SET_TIME_TEXT: "Entrer la date et l'heure locale ci-dessous pour régler l'heure",
LOCAL_TIME: 'Heure locale', LOCAL_TIME: 'Heure locale',
UTC_TIME: 'Heure UTC', UTC_TIME: 'Heure UTC',
ENABLE_NTP: 'Activer le NTP', ENABLE_NTP: 'Activer le NTP',
NTP_SERVER: 'Serveur NTP', NTP_SERVER: 'Serveur NTP',
TIME_ZONE: 'Fuseau horaire', TIME_ZONE: 'Fuseau horaire',
ACCESS_POINT: 'Point d\'accès', ACCESS_POINT: "Point d'accès",
AP_PROVIDE: 'Activer le Point d\'Accès', AP_PROVIDE: "Activer le Point d'Accès",
AP_PROVIDE_TEXT_1: 'toujours', AP_PROVIDE_TEXT_1: 'toujours',
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté', AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
AP_PROVIDE_TEXT_3: 'jamais', AP_PROVIDE_TEXT_3: 'jamais',
@@ -275,13 +276,13 @@ const fr: Translation = {
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi', // and enable ETH // TODO translate NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi', // and enable ETH // TODO translate
NETWORK_BLANK_BSSID: 'leave blank to use only SSID', // TODO translate NETWORK_BLANK_BSSID: 'leave blank to use only SSID', // TODO translate
TX_POWER: 'Puissance Tx', TX_POWER: 'Puissance Tx',
HOSTNAME: 'Nom d\'hôte', HOSTNAME: "Nom d'hôte",
NETWORK_DISABLE_SLEEP: 'Désactiver le mode veille du WiFi', NETWORK_DISABLE_SLEEP: 'Désactiver le mode veille du WiFi',
NETWORK_LOW_BAND: 'Utiliser une bande passante WiFi plus faible', NETWORK_LOW_BAND: 'Utiliser une bande passante WiFi plus faible',
NETWORK_USE_DNS: 'Activer le service mDNS', NETWORK_USE_DNS: 'Activer le service mDNS',
NETWORK_ENABLE_CORS: 'Activer CORS', NETWORK_ENABLE_CORS: 'Activer CORS',
NETWORK_CORS_ORIGIN: 'Origine CORS', NETWORK_CORS_ORIGIN: 'Origine CORS',
NETWORK_ENABLE_IPV6: 'Activer le support de l\'IPv6', NETWORK_ENABLE_IPV6: "Activer le support de l'IPv6",
NETWORK_FIXED_IP: 'Utiliser une adresse IP fixe', NETWORK_FIXED_IP: 'Utiliser une adresse IP fixe',
NETWORK_GATEWAY: 'Passerelle', NETWORK_GATEWAY: 'Passerelle',
NETWORK_SUBNET: 'Masque de sous-réseau', NETWORK_SUBNET: 'Masque de sous-réseau',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const it: Translation = { const it: Translation = {
LANGUAGE: 'Lingua', LANGUAGE: 'Lingua',
RETRY: 'Riprovare', RETRY: 'Riprovare',
@@ -102,7 +100,8 @@ const it: Translation = {
CUSTOMIZATIONS: 'Personalizzazione', CUSTOMIZATIONS: 'Personalizzazione',
APPLICATION_RESTARTING: 'EMS-ESP sta riavviando', APPLICATION_RESTARTING: 'EMS-ESP sta riavviando',
INTERFACE_BOARD_PROFILE: 'Profilo scheda di interfaccia', INTERFACE_BOARD_PROFILE: 'Profilo scheda di interfaccia',
BOARD_PROFILE_TEXT: 'Selezionare un profilo di interfaccia pre-configurato dalla lista sottostante o scegliere un profilo personalizzato per configurare le impostazioni del tuo hardware', BOARD_PROFILE_TEXT:
'Selezionare un profilo di interfaccia pre-configurato dalla lista sottostante o scegliere un profilo personalizzato per configurare le impostazioni del tuo hardware',
BOARD_PROFILE: 'Profilo Scheda', BOARD_PROFILE: 'Profilo Scheda',
CUSTOM: 'Personalizzazione', CUSTOM: 'Personalizzazione',
GPIO_OF: 'GPIO {0}', GPIO_OF: 'GPIO {0}',
@@ -197,8 +196,10 @@ const it: Translation = {
ENABLE_OTA: 'Abilita aggiornamenti OTA', ENABLE_OTA: 'Abilita aggiornamenti OTA',
DOWNLOAD_CUSTOMIZATION_TEXT: 'Scarica personalizzazioni entità', DOWNLOAD_CUSTOMIZATION_TEXT: 'Scarica personalizzazioni entità',
DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events', DOWNLOAD_SCHEDULE_TEXT: 'Download Scheduler Events',
DOWNLOAD_SETTINGS_TEXT: 'Scarica le impostazioni dell applicazione. Fai attenzione quando condividi le tue impostazioni poiché questo file contiene password e altre informazioni di sistema riservate', DOWNLOAD_SETTINGS_TEXT:
UPLOAD_TEXT: 'Carica un nuovo file firmware (.bin) , file delle impostazioni o delle personalizzazioni (.json) di seguito, per un opzione di convalida scaricare dapprima un file "*.MD5" ', 'Scarica le impostazioni dell applicazione. Fai attenzione quando condividi le tue impostazioni poiché questo file contiene password e altre informazioni di sistema riservate',
UPLOAD_TEXT:
'Carica un nuovo file firmware (.bin) , file delle impostazioni o delle personalizzazioni (.json) di seguito, per un opzione di convalida scaricare dapprima un file "*.MD5" ',
UPLOADING: 'Caricamento', UPLOADING: 'Caricamento',
UPLOAD_DROP_TEXT: 'Trascina il file o clicca qui', UPLOAD_DROP_TEXT: 'Trascina il file o clicca qui',
ERROR: 'Errore Inaspettato, prego tenta ancora', ERROR: 'Errore Inaspettato, prego tenta ancora',
@@ -208,7 +209,8 @@ const it: Translation = {
USER_WARNING: 'Devi avere configurato almeno un utente amministratore', USER_WARNING: 'Devi avere configurato almeno un utente amministratore',
ADD: 'Aggiungi', ADD: 'Aggiungi',
ACCESS_TOKEN_FOR: 'Token di accesso per', ACCESS_TOKEN_FOR: 'Token di accesso per',
ACCESS_TOKEN_TEXT: 'Il token seguente viene utilizzato con le chiamate API REST che richiedono l autorizzazione. Può essere passato come token Bearer nell intestazione di autorizzazione o nel parametro di query URL access_token.', ACCESS_TOKEN_TEXT:
'Il token seguente viene utilizzato con le chiamate API REST che richiedono l autorizzazione. Può essere passato come token Bearer nell intestazione di autorizzazione o nel parametro di query URL access_token.',
GENERATING_TOKEN: 'Generazione token', GENERATING_TOKEN: 'Generazione token',
USER: 'Utente', USER: 'Utente',
MODIFY: 'Modifica', MODIFY: 'Modifica',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const nl: Translation = { const nl: Translation = {
LANGUAGE: 'Taal', LANGUAGE: 'Taal',
RETRY: 'Opnieuw proberen', RETRY: 'Opnieuw proberen',
@@ -208,7 +206,8 @@ const nl: Translation = {
USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren', USER_WARNING: 'U dient tenminste 1 admin gebruiker te configureren',
ADD: 'Toevoegen', ADD: 'Toevoegen',
ACCESS_TOKEN_FOR: 'Access Token voor', 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', 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', GENERATING_TOKEN: 'Token aan het genereren',
USER: 'Gebruiker', USER: 'Gebruiker',
MODIFY: 'Aanpassen', MODIFY: 'Aanpassen',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const no: Translation = { const no: Translation = {
LANGUAGE: 'Språk', LANGUAGE: 'Språk',
RETRY: 'Forsøk igjen', RETRY: 'Forsøk igjen',
@@ -208,7 +206,8 @@ const no: Translation = {
USER_WARNING: 'Du må ha minst en admin bruker konfigurert', USER_WARNING: 'Du må ha minst en admin bruker konfigurert',
ADD: 'Legg til', ADD: 'Legg til',
ACCESS_TOKEN_FOR: 'Aksess Token for', ACCESS_TOKEN_FOR: 'Aksess Token for',
ACCESS_TOKEN_TEXT: 'Token nedenfor benyttes med REST API-kall som krever autorisering. Den kan sendes med enten som en Bearer token i Authorization-headern eller i access_token URL query-parameter.', ACCESS_TOKEN_TEXT:
'Token nedenfor benyttes med REST API-kall som krever autorisering. Den kan sendes med enten som en Bearer token i Authorization-headern eller i access_token URL query-parameter.',
GENERATING_TOKEN: 'Generer token', GENERATING_TOKEN: 'Generer token',
USER: 'Bruker', USER: 'Bruker',
MODIFY: 'Endre', MODIFY: 'Endre',

View File

@@ -1,7 +1,5 @@
import type { BaseTranslation } from '../i18n-types'; import type { BaseTranslation } from '../i18n-types';
/* prettier-ignore */
const pl: BaseTranslation = { const pl: BaseTranslation = {
LANGUAGE: 'Język', LANGUAGE: 'Język',
RETRY: 'Ponów', RETRY: 'Ponów',
@@ -122,7 +120,7 @@ const pl: BaseTranslation = {
BYPASS_TOKEN: 'Pomiń autoryzację tokenem w wywołaniach API', BYPASS_TOKEN: 'Pomiń autoryzację tokenem w wywołaniach API',
READONLY: 'Tryb pracy "tylko do odczytu" (blokuje wszystkie komendy zapisu na magistralę EMS)', READONLY: 'Tryb pracy "tylko do odczytu" (blokuje wszystkie komendy zapisu na magistralę EMS)',
UNDERCLOCK_CPU: 'Obniż taktowanie CPU', UNDERCLOCK_CPU: 'Obniż taktowanie CPU',
HEATINGOFF: 'Uruchom kocioł z wymuszonym wyłączonym grzaniem', HEATINGOFF: 'Uruchom kocioł z wymuszonym wyłączonym grzaniem',
ENABLE_SHOWER_TIMER: 'Aktywuj minutnik prysznica', ENABLE_SHOWER_TIMER: 'Aktywuj minutnik prysznica',
ENABLE_SHOWER_ALERT: 'Aktywuj alarm prysznica', ENABLE_SHOWER_ALERT: 'Aktywuj alarm prysznica',
TRIGGER_TIME: 'Wyzwalaj po czasie', TRIGGER_TIME: 'Wyzwalaj po czasie',
@@ -158,7 +156,7 @@ const pl: BaseTranslation = {
SET_ALL: 'Ustaw wszystko jako', SET_ALL: 'Ustaw wszystko jako',
OPTIONS: 'Opcje', OPTIONS: 'Opcje',
NAME: '{{Nazwa|nazwa|}}', NAME: '{{Nazwa|nazwa|}}',
CUSTOMIZATIONS_RESET: 'Na pewno chcesz usunąć wszystkie personalizacje łącznie z ustawieniami dla czujników temperatury 1-Wire® i urządzeń podłączonych do EMS-ESP?', CUSTOMIZATIONS_RESET: 'Na pewno chcesz usunąć wszystkie personalizacje łącznie z ustawieniami dla czujników temperatury 1-Wire® i urządzeń podłączonych do EMS-ESP?',
SUPPORT_INFORMATION: '{{I|i|}}nformacj{{e|i|}} o systemie', SUPPORT_INFORMATION: '{{I|i|}}nformacj{{e|i|}} o systemie',
HELP_INFORMATION_1: 'Aby uzyskać instrukcje dotyczące konfiguracji EMS-ESP, skorzystaj z wiki w internecie', HELP_INFORMATION_1: 'Aby uzyskać instrukcje dotyczące konfiguracji EMS-ESP, skorzystaj z wiki w internecie',
HELP_INFORMATION_2: 'Aby dołączyć do naszego serwera Discord i komunikować się na żywo ze społecznością', HELP_INFORMATION_2: 'Aby dołączyć do naszego serwera Discord i komunikować się na żywo ze społecznością',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const sk: Translation = { const sk: Translation = {
LANGUAGE: 'Jazyk', LANGUAGE: 'Jazyk',
RETRY: 'Opakovať', RETRY: 'Opakovať',
@@ -209,7 +207,8 @@ const sk: Translation = {
USER_WARNING: 'Musíte mať nakonfigurovaného aspoň jedného používateľa administrátora', USER_WARNING: 'Musíte mať nakonfigurovaného aspoň jedného používateľa administrátora',
ADD: 'Pridať', ADD: 'Pridať',
ACCESS_TOKEN_FOR: 'Prístupový token pre', ACCESS_TOKEN_FOR: 'Prístupový token pre',
ACCESS_TOKEN_TEXT: 'Nižšie uvedený token sa používa pri volaniach REST API, ktoré vyžadujú autorizáciu. Môže byť odovzdaný buď ako token Bearer v hlavičke Authorization (Autorizácia), alebo v parametri dotazu URL access_token.', ACCESS_TOKEN_TEXT:
'Nižšie uvedený token sa používa pri volaniach REST API, ktoré vyžadujú autorizáciu. Môže byť odovzdaný buď ako token Bearer v hlavičke Authorization (Autorizácia), alebo v parametri dotazu URL access_token.',
GENERATING_TOKEN: 'Generovanie tokenu', GENERATING_TOKEN: 'Generovanie tokenu',
USER: 'Užívateľ', USER: 'Užívateľ',
MODIFY: 'Upraviť', MODIFY: 'Upraviť',
@@ -324,7 +323,7 @@ const sk: Translation = {
ACTIVELOW: 'Aktívny Nízky', ACTIVELOW: 'Aktívny Nízky',
UNCHANGED: 'Nezmenené', UNCHANGED: 'Nezmenené',
ALWAYS: 'Vždy', ALWAYS: 'Vždy',
ACTIVITY: 'Aktivita', ACTIVITY: 'Aktivita',
CONFIGURE: 'Konfiguracia {0}', CONFIGURE: 'Konfiguracia {0}',
SYSTEM_MEMORY: 'System Memory', // TODO translate SYSTEM_MEMORY: 'System Memory', // TODO translate
APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate APPLICATION_SETTINGS_1: 'Modify EMS-ESP Application Settings', // TODO translate

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const sv: Translation = { const sv: Translation = {
LANGUAGE: 'Språk', LANGUAGE: 'Språk',
RETRY: 'Försök igen', RETRY: 'Försök igen',
@@ -208,7 +206,8 @@ const sv: Translation = {
USER_WARNING: 'Du måste ha minst en admin konfigurerad', USER_WARNING: 'Du måste ha minst en admin konfigurerad',
ADD: 'Lägg till', ADD: 'Lägg till',
ACCESS_TOKEN_FOR: 'Access Token för', 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.', 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', GENERATING_TOKEN: 'Genererar token',
USER: 'Användare', USER: 'Användare',
MODIFY: 'Ändra', MODIFY: 'Ändra',

View File

@@ -1,7 +1,5 @@
import type { Translation } from '../i18n-types'; import type { Translation } from '../i18n-types';
/* prettier-ignore */
const tr: Translation = { const tr: Translation = {
LANGUAGE: 'Dil', LANGUAGE: 'Dil',
RETRY: 'Tekrar Dene', RETRY: 'Tekrar Dene',
@@ -208,7 +206,8 @@ const tr: Translation = {
USER_WARNING: 'En az bir yönetici kullanıcısı ayarlamanız gerekmektedir', USER_WARNING: 'En az bir yönetici kullanıcısı ayarlamanız gerekmektedir',
ADD: 'Ekle', ADD: 'Ekle',
ACCESS_TOKEN_FOR: 'Erişim Jetonunun sahibi', ACCESS_TOKEN_FOR: 'Erişim Jetonunun sahibi',
ACCESS_TOKEN_TEXT: 'Aşağıdaki Jeton yetki gerektiren REST API çağrıları ile kullanılmaktadır. Taşıyıcı Jeton olarak yetkilendirme başlığında yada erişim jetonu olarak URL sorgu parametresinde kullanılabilir.', ACCESS_TOKEN_TEXT:
'Aşağıdaki Jeton yetki gerektiren REST API çağrıları ile kullanılmaktadır. Taşıyıcı Jeton olarak yetkilendirme başlığında yada erişim jetonu olarak URL sorgu parametresinde kullanılabilir.',
GENERATING_TOKEN: 'Jeton oluşturuluyor', GENERATING_TOKEN: 'Jeton oluşturuluyor',
USER: 'Kullanıcı', USER: 'Kullanıcı',
MODIFY: 'Düzenle', MODIFY: 'Düzenle',

View File

@@ -1,10 +1,17 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom'; import {
Route,
RouterProvider,
createBrowserRouter,
createRoutesFromElements
} from 'react-router-dom';
import App from 'App'; import App from 'App';
const router = createBrowserRouter(createRoutesFromElements(<Route path="/*" element={<App />} />)); const router = createBrowserRouter(
createRoutesFromElements(<Route path="/*" element={<App />} />)
);
createRoot(document.getElementById('root') as HTMLElement).render( createRoot(document.getElementById('root') as HTMLElement).render(
<StrictMode> <StrictMode>

View File

@@ -5,7 +5,17 @@ import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew'; import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Checkbox, Divider, Grid, InputAdornment, MenuItem, TextField, Typography } from '@mui/material'; import {
Box,
Button,
Checkbox,
Divider,
Grid,
InputAdornment,
MenuItem,
TextField,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
@@ -61,7 +71,12 @@ const ApplicationSettings: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const updateFormValue = updateValueDirty(origData, dirtyFlags, setDirtyFlags, updateDataValue); const updateFormValue = updateValueDirty(
origData,
dirtyFlags,
setDirtyFlags,
updateDataValue
);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -220,7 +235,9 @@ const ApplicationSettings: FC = () => {
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
name="dallas_gpio" name="dallas_gpio"
label={LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'} label={
LL.GPIO_OF(LL.TEMPERATURE()) + ' (0=' + LL.DISABLED(1) + ')'
}
fullWidth fullWidth
variant="outlined" variant="outlined"
value={numberValue(data.dallas_gpio)} value={numberValue(data.dallas_gpio)}
@@ -322,7 +339,13 @@ const ApplicationSettings: FC = () => {
<Typography sx={{ pt: 2 }} variant="h6" color="primary"> <Typography sx={{ pt: 2 }} variant="h6" color="primary">
{LL.SETTINGS_OF(LL.EMS_BUS(0))} {LL.SETTINGS_OF(LL.EMS_BUS(0))}
</Typography> </Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start"> <Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
name="tx_mode" name="tx_mode"
@@ -396,54 +419,120 @@ const ApplicationSettings: FC = () => {
</Grid> </Grid>
{data.led_gpio !== 0 && ( {data.led_gpio !== 0 && (
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.hide_led} onChange={updateFormValue} name="hide_led" />} control={
<Checkbox
checked={data.hide_led}
onChange={updateFormValue}
name="hide_led"
/>
}
label={LL.HIDE_LED()} label={LL.HIDE_LED()}
disabled={saving} disabled={saving}
/> />
)} )}
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.telnet_enabled} onChange={updateFormValue} name="telnet_enabled" />} control={
<Checkbox
checked={data.telnet_enabled}
onChange={updateFormValue}
name="telnet_enabled"
/>
}
label={LL.ENABLE_TELNET()} label={LL.ENABLE_TELNET()}
disabled={saving} disabled={saving}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.analog_enabled} onChange={updateFormValue} name="analog_enabled" />} control={
<Checkbox
checked={data.analog_enabled}
onChange={updateFormValue}
name="analog_enabled"
/>
}
label={LL.ENABLE_ANALOG()} label={LL.ENABLE_ANALOG()}
disabled={saving} disabled={saving}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.fahrenheit} onChange={updateFormValue} name="fahrenheit" />} control={
<Checkbox
checked={data.fahrenheit}
onChange={updateFormValue}
name="fahrenheit"
/>
}
label={LL.CONVERT_FAHRENHEIT()} label={LL.CONVERT_FAHRENHEIT()}
disabled={saving} disabled={saving}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.notoken_api} onChange={updateFormValue} name="notoken_api" />} control={
<Checkbox
checked={data.notoken_api}
onChange={updateFormValue}
name="notoken_api"
/>
}
label={LL.BYPASS_TOKEN()} label={LL.BYPASS_TOKEN()}
disabled={saving} disabled={saving}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.readonly_mode} onChange={updateFormValue} name="readonly_mode" />} control={
<Checkbox
checked={data.readonly_mode}
onChange={updateFormValue}
name="readonly_mode"
/>
}
label={LL.READONLY()} label={LL.READONLY()}
disabled={saving} disabled={saving}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.low_clock} onChange={updateFormValue} name="low_clock" />} control={
<Checkbox
checked={data.low_clock}
onChange={updateFormValue}
name="low_clock"
/>
}
label={LL.UNDERCLOCK_CPU()} label={LL.UNDERCLOCK_CPU()}
disabled={saving} disabled={saving}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.boiler_heatingoff} onChange={updateFormValue} name="boiler_heatingoff" />} control={
<Checkbox
checked={data.boiler_heatingoff}
onChange={updateFormValue}
name="boiler_heatingoff"
/>
}
label={LL.HEATINGOFF()} label={LL.HEATINGOFF()}
disabled={saving} disabled={saving}
/> />
<Grid container spacing={0} direction="row" justifyContent="flex-start" alignItems="flex-start"> <Grid
container
spacing={0}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.shower_timer} onChange={updateFormValue} name="shower_timer" />} control={
<Checkbox
checked={data.shower_timer}
onChange={updateFormValue}
name="shower_timer"
/>
}
label={LL.ENABLE_SHOWER_TIMER()} label={LL.ENABLE_SHOWER_TIMER()}
disabled={saving} disabled={saving}
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.shower_alert} onChange={updateFormValue} name="shower_alert" />} control={
<Checkbox
checked={data.shower_alert}
onChange={updateFormValue}
name="shower_alert"
/>
}
label={LL.ENABLE_SHOWER_ALERT()} label={LL.ENABLE_SHOWER_ALERT()}
disabled={!data.shower_timer} disabled={!data.shower_timer}
/> />
@@ -465,7 +554,9 @@ const ApplicationSettings: FC = () => {
name="shower_alert_trigger" name="shower_alert_trigger"
label={LL.TRIGGER_TIME()} label={LL.TRIGGER_TIME()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
)
}} }}
variant="outlined" variant="outlined"
value={numberValue(data.shower_alert_trigger)} value={numberValue(data.shower_alert_trigger)}
@@ -481,7 +572,9 @@ const ApplicationSettings: FC = () => {
name="shower_alert_coldshot" name="shower_alert_coldshot"
label={LL.COLD_SHOT_DURATION()} label={LL.COLD_SHOT_DURATION()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
variant="outlined" variant="outlined"
value={numberValue(data.shower_alert_coldshot)} value={numberValue(data.shower_alert_coldshot)}
@@ -497,7 +590,13 @@ const ApplicationSettings: FC = () => {
<Typography sx={{ pt: 3 }} variant="h6" color="primary"> <Typography sx={{ pt: 3 }} variant="h6" color="primary">
{LL.FORMATTING_OPTIONS()} {LL.FORMATTING_OPTIONS()}
</Typography> </Typography>
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start"> <Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6} md={4}> <Grid item xs={12} sm={6} md={4}>
<TextField <TextField
name="bool_dashboard" name="bool_dashboard"
@@ -556,7 +655,13 @@ const ApplicationSettings: FC = () => {
{LL.TEMP_SENSORS()} {LL.TEMP_SENSORS()}
</Typography> </Typography>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.dallas_parasite} onChange={updateFormValue} name="dallas_parasite" />} control={
<Checkbox
checked={data.dallas_parasite}
onChange={updateFormValue}
name="dallas_parasite"
/>
}
label={LL.ENABLE_PARASITE()} label={LL.ENABLE_PARASITE()}
disabled={saving} disabled={saving}
/> />
@@ -566,7 +671,13 @@ const ApplicationSettings: FC = () => {
{LL.LOGGING()} {LL.LOGGING()}
</Typography> </Typography>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={data.trace_raw} onChange={updateFormValue} name="trace_raw" />} control={
<Checkbox
checked={data.trace_raw}
onChange={updateFormValue}
name="trace_raw"
/>
}
label={LL.LOG_HEX()} label={LL.LOG_HEX()}
disabled={saving} disabled={saving}
/> />
@@ -582,7 +693,13 @@ const ApplicationSettings: FC = () => {
label={LL.ENABLE_SYSLOG()} label={LL.ENABLE_SYSLOG()}
/> />
{data.syslog_enabled && ( {data.syslog_enabled && (
<Grid container spacing={1} direction="row" justifyContent="flex-start" alignItems="flex-start"> <Grid
container
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors} fieldErrors={fieldErrors}
@@ -636,7 +753,9 @@ const ApplicationSettings: FC = () => {
name="syslog_mark_interval" name="syslog_mark_interval"
label={LL.MARK_INTERVAL()} label={LL.MARK_INTERVAL()}
InputProps={{ InputProps={{
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> endAdornment: (
<InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
)
}} }}
fullWidth fullWidth
variant="outlined" variant="outlined"
@@ -651,7 +770,12 @@ const ApplicationSettings: FC = () => {
)} )}
{restartNeeded && ( {restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}> <MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}> <Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()} {LL.RESTART()}
</Button> </Button>
</MessageBox> </MessageBox>

View File

@@ -10,10 +10,24 @@ import RefreshIcon from '@mui/icons-material/Refresh';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Typography } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { updateState, useRequest } from 'alova'; import { updateState, useRequest } from 'alova';
import { BlockNavigation, ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components'; import {
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api'; import * as EMSESP from './api';
@@ -171,8 +185,13 @@ const CustomEntities: FC = () => {
updateState('entities', (data: EntityItem[]) => { updateState('entities', (data: EntityItem[]) => {
const new_data = creating const new_data = creating
? [...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem] ? [
: data.map((ei) => (ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei)); ...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
updatedItem
]
: data.map((ei) =>
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
);
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length); setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
return new_data; return new_data;
}); });
@@ -201,7 +220,8 @@ const CustomEntities: FC = () => {
return value === undefined return value === undefined
? '' ? ''
: typeof value === 'number' : typeof value === 'number'
? new Intl.NumberFormat().format(value) + (uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom]) ? new Intl.NumberFormat().format(value) +
(uom === 0 ? '' : ' ' + DeviceValueUOM_s[uom])
: (value as string); : (value as string);
} }
@@ -215,7 +235,11 @@ const CustomEntities: FC = () => {
} }
return ( return (
<Table data={{ nodes: entities.filter((ei) => !ei.deleted) }} theme={entity_theme} layout={{ custom: true }}> <Table
data={{ nodes: entities.filter((ei) => !ei.deleted) }}
theme={entity_theme}
layout={{ custom: true }}
>
{(tableList: EntityItem[]) => ( {(tableList: EntityItem[]) => (
<> <>
<Header> <Header>
@@ -233,12 +257,18 @@ const CustomEntities: FC = () => {
<Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}> <Row key={ei.name} item={ei} onClick={() => editEntityItem(ei)}>
<Cell> <Cell>
{ei.name}&nbsp; {ei.name}&nbsp;
{ei.writeable && <EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />} {ei.writeable && (
<EditOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</Cell>
<Cell>
{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}
</Cell> </Cell>
<Cell>{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}</Cell>
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell> <Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell> <Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
<Cell>{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}</Cell> <Cell>
{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}
</Cell>
<Cell>{formatValue(ei.value, ei.uom)}</Cell> <Cell>{formatValue(ei.value, ei.uom)}</Cell>
</Row> </Row>
))} ))}
@@ -273,7 +303,12 @@ const CustomEntities: FC = () => {
<Box flexGrow={1}> <Box flexGrow={1}>
{numChanges > 0 && ( {numChanges > 0 && (
<ButtonRow> <ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onDialogCancel}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button <Button
@@ -289,10 +324,20 @@ const CustomEntities: FC = () => {
</Box> </Box>
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow> <ButtonRow>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchEntities}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={fetchEntities}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
<Button startIcon={<AddIcon />} variant="outlined" color="primary" onClick={addEntityItem}> <Button
startIcon={<AddIcon />}
variant="outlined"
color="primary"
onClick={addEntityItem}
>
{LL.ADD(0)} {LL.ADD(0)}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -142,7 +142,13 @@ const CustomEntitiesDialog = ({
<> <>
<Grid item xs={4} mt={3}> <Grid item xs={4} mt={3}>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={editItem.writeable} onChange={updateFormValue} name="writeable" />} control={
<Checkbox
checked={editItem.writeable}
onChange={updateFormValue}
name="writeable"
/>
}
label={LL.WRITEABLE()} label={LL.WRITEABLE()}
/> />
</Grid> </Grid>
@@ -157,7 +163,11 @@ const CustomEntitiesDialog = ({
value={editItem.device_id as string} value={editItem.device_id as string}
onChange={updateFormValue} onChange={updateFormValue}
inputProps={{ style: { textTransform: 'uppercase' } }} inputProps={{ style: { textTransform: 'uppercase' } }}
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }} InputProps={{
startAdornment: (
<InputAdornment position="start">0x</InputAdornment>
)
}}
/> />
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
@@ -170,7 +180,11 @@ const CustomEntitiesDialog = ({
value={editItem.type_id} value={editItem.type_id}
onChange={updateFormValue} onChange={updateFormValue}
inputProps={{ style: { textTransform: 'uppercase' } }} inputProps={{ style: { textTransform: 'uppercase' } }}
InputProps={{ startAdornment: <InputAdornment position="start">0x</InputAdornment> }} InputProps={{
startAdornment: (
<InputAdornment position="start">0x</InputAdornment>
)
}}
/> />
</Grid> </Grid>
<Grid item xs={4}> <Grid item xs={4}>
@@ -207,55 +221,57 @@ const CustomEntitiesDialog = ({
</TextField> </TextField>
</Grid> </Grid>
{editItem.value_type !== DeviceValueType.BOOL && editItem.value_type !== DeviceValueType.STRING && ( {editItem.value_type !== DeviceValueType.BOOL &&
<> editItem.value_type !== DeviceValueType.STRING && (
<>
<Grid item xs={4}>
<TextField
name="factor"
label={LL.FACTOR()}
value={numberValue(editItem.factor)}
variant="outlined"
onChange={updateFormValue}
fullWidth
margin="normal"
type="number"
inputProps={{ step: '0.001' }}
/>
</Grid>
<Grid item xs={4}>
<TextField
name="uom"
label={LL.UNIT()}
value={editItem.uom}
margin="normal"
fullWidth
onChange={updateFormValue}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
</MenuItem>
))}
</TextField>
</Grid>
</>
)}
{editItem.value_type === DeviceValueType.STRING &&
editItem.device_id !== '0' && (
<Grid item xs={4}> <Grid item xs={4}>
<TextField <TextField
name="factor" name="factor"
label={LL.FACTOR()} label="Bytes"
value={numberValue(editItem.factor)} value={editItem.factor}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
fullWidth fullWidth
margin="normal" margin="normal"
type="number" type="number"
inputProps={{ step: '0.001' }} inputProps={{ min: '1', max: '27', step: '1' }}
/> />
</Grid> </Grid>
<Grid item xs={4}> )}
<TextField
name="uom"
label={LL.UNIT()}
value={editItem.uom}
margin="normal"
fullWidth
onChange={updateFormValue}
select
>
{DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}>
{val}
</MenuItem>
))}
</TextField>
</Grid>
</>
)}
{editItem.value_type === DeviceValueType.STRING && editItem.device_id !== '0' && (
<Grid item xs={4}>
<TextField
name="factor"
label="Bytes"
value={editItem.factor}
variant="outlined"
onChange={updateFormValue}
fullWidth
margin="normal"
type="number"
inputProps={{ min: '1', max: '27', step: '1' }}
/>
</Grid>
)}
</> </>
)} )}
</Grid> </Grid>
@@ -264,15 +280,30 @@ const CustomEntitiesDialog = ({
<DialogActions> <DialogActions>
{!creating && ( {!creating && (
<Box flexGrow={1}> <Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}> <Button
startIcon={<RemoveIcon />}
variant="outlined"
color="warning"
onClick={remove}
>
{LL.REMOVE()} {LL.REMOVE()}
</Button> </Button>
</Box> </Box>
)} )}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary"> <Button
startIcon={creating ? <AddIcon /> : <DoneIcon />}
variant="outlined"
onClick={save}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()} {creating ? LL.ADD(0) : LL.UPDATE()}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -27,11 +27,25 @@ import {
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { BlockNavigation, ButtonRow, MessageBox, SectionContent, useLayoutTitle } from 'components'; import {
BlockNavigation,
ButtonRow,
MessageBox,
SectionContent,
useLayoutTitle
} from 'components';
import RestartMonitor from 'framework/system/RestartMonitor'; import RestartMonitor from 'framework/system/RestartMonitor';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
@@ -63,7 +77,9 @@ const Customization: FC = () => {
// fetch devices first // fetch devices first
const { data: devices } = useRequest(EMSESP.readDevices); const { data: devices } = useRequest(EMSESP.readDevices);
const [selectedDevice, setSelectedDevice] = useState<number>(Number(useLocation().state) || -1); const [selectedDevice, setSelectedDevice] = useState<number>(
Number(useLocation().state) || -1
);
const [selectedDeviceName, setSelectedDeviceName] = useState<string>(''); const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), { const { send: resetCustomizations } = useRequest(EMSESP.resetCustomizations(), {
@@ -71,7 +87,8 @@ const Customization: FC = () => {
}); });
const { send: writeCustomizationEntities } = useRequest( const { send: writeCustomizationEntities } = useRequest(
(data: { id: number; entity_ids: string[] }) => EMSESP.writeCustomizationEntities(data), (data: { id: number; entity_ids: string[] }) =>
EMSESP.writeCustomizationEntities(data),
{ {
immediate: false immediate: false
} }
@@ -86,7 +103,15 @@ const Customization: FC = () => {
); );
const setOriginalSettings = (data: DeviceEntity[]) => { const setOriginalSettings = (data: DeviceEntity[]) => {
setDeviceEntities(data.map((de) => ({ ...de, o_m: de.m, o_cn: de.cn, o_mi: de.mi, o_ma: de.ma }))); setDeviceEntities(
data.map((de) => ({
...de,
o_m: de.m,
o_cn: de.cn,
o_mi: de.mi,
o_ma: de.ma
}))
);
}; };
onSuccess((event) => { onSuccess((event) => {
@@ -166,7 +191,12 @@ const Customization: FC = () => {
}); });
function hasEntityChanged(de: DeviceEntity) { function hasEntityChanged(de: DeviceEntity) {
return (de?.cn || '') !== (de?.o_cn || '') || de.m !== de.o_m || de.ma !== de.o_ma || de.mi !== de.o_mi; return (
(de?.cn || '') !== (de?.o_cn || '') ||
de.m !== de.o_m ||
de.ma !== de.o_ma ||
de.mi !== de.o_mi
);
} }
useEffect(() => { useEffect(() => {
@@ -221,8 +251,11 @@ const Customization: FC = () => {
} }
const formatName = (de: DeviceEntity, withShortname: boolean) => const formatName = (de: DeviceEntity, withShortname: boolean) =>
(de.n && de.n[0] === '!' ? LL.COMMAND(1) + ': ' + de.n.slice(1) : de.cn && de.cn !== '' ? de.cn : de.n) + (de.n && de.n[0] === '!'
(withShortname ? ' ' + de.id : ''); ? LL.COMMAND(1) + ': ' + de.n.slice(1)
: de.cn && de.cn !== ''
? de.cn
: de.n) + (withShortname ? ' ' + de.id : '');
const getMaskNumber = (newMask: string[]) => { const getMaskNumber = (newMask: string[]) => {
let new_mask = 0; let new_mask = 0;
@@ -253,7 +286,8 @@ const Customization: FC = () => {
}; };
const filter_entity = (de: DeviceEntity) => const filter_entity = (de: DeviceEntity) =>
(de.m & selectedFilters || !selectedFilters) && formatName(de, true).includes(search.toLocaleLowerCase()); (de.m & selectedFilters || !selectedFilters) &&
formatName(de, true).includes(search.toLocaleLowerCase());
const maskDisabled = (set: boolean) => { const maskDisabled = (set: boolean) => {
setDeviceEntities( setDeviceEntities(
@@ -262,8 +296,14 @@ const Customization: FC = () => {
return { return {
...de, ...de,
m: set m: set
? de.m | (DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE) ? de.m |
: de.m & ~(DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE) (DeviceEntityMask.DV_API_MQTT_EXCLUDE |
DeviceEntityMask.DV_WEB_EXCLUDE)
: de.m &
~(
DeviceEntityMask.DV_API_MQTT_EXCLUDE |
DeviceEntityMask.DV_WEB_EXCLUDE
)
}; };
} else { } else {
return de; return de;
@@ -288,7 +328,11 @@ const Customization: FC = () => {
}; };
const updateDeviceEntity = (updatedItem: DeviceEntity) => { const updateDeviceEntity = (updatedItem: DeviceEntity) => {
setDeviceEntities(deviceEntities?.map((de) => (de.id === updatedItem.id ? { ...de, ...updatedItem } : de))); setDeviceEntities(
deviceEntities?.map((de) =>
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
)
);
}; };
const onDialogSave = (updatedItem: DeviceEntity) => { const onDialogSave = (updatedItem: DeviceEntity) => {
@@ -330,7 +374,10 @@ const Customization: FC = () => {
return; return;
} }
await writeCustomizationEntities({ id: selectedDevice, entity_ids: masked_entities }).catch((error: Error) => { await writeCustomizationEntities({
id: selectedDevice,
entity_ids: masked_entities
}).catch((error: Error) => {
if (error.message === 'Reboot required') { if (error.message === 'Reboot required') {
setRestartNeeded(true); setRestartNeeded(true);
} else { } else {
@@ -376,14 +423,26 @@ const Customization: FC = () => {
<> <>
<Box color="warning.main"> <Box color="warning.main">
<Typography variant="body2" mt={1}> <Typography variant="body2" mt={1}>
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}&nbsp;&nbsp; <OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}&nbsp;&nbsp; &nbsp;&nbsp;
<OptionIcon type="api_mqtt_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_4()}&nbsp;&nbsp; <OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}&nbsp;&nbsp; &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()}&nbsp;&nbsp;
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()} <OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
</Typography> </Typography>
</Box> </Box>
<Grid container mb={1} mt={0} spacing={1} direction="row" justifyContent="flex-start" alignItems="center"> <Grid
container
mb={1}
mt={0}
spacing={1}
direction="row"
justifyContent="flex-start"
alignItems="center"
>
<Grid item xs={2}> <Grid item xs={2}>
<TextField <TextField
size="small" size="small"
@@ -455,11 +514,16 @@ const Customization: FC = () => {
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="subtitle2" color="primary"> <Typography variant="subtitle2" color="primary">
{LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length}&nbsp;{LL.ENTITIES(deviceEntities.length)} {LL.SHOWING()}&nbsp;{shown_data.length}/{deviceEntities.length}
&nbsp;{LL.ENTITIES(deviceEntities.length)}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}> <Table
data={{ nodes: shown_data }}
theme={entities_theme}
layout={{ custom: true }}
>
{(tableList: DeviceEntity[]) => ( {(tableList: DeviceEntity[]) => (
<> <>
<Header> <Header>
@@ -479,13 +543,20 @@ const Customization: FC = () => {
</Cell> </Cell>
<Cell> <Cell>
{formatName(de, false)}&nbsp;( {formatName(de, false)}&nbsp;(
<Link target="_blank" href={APIURL + selectedDeviceName + '/' + de.id}> <Link
target="_blank"
href={APIURL + selectedDeviceName + '/' + de.id}
>
{de.id} {de.id}
</Link> </Link>
) )
</Cell> </Cell>
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}</Cell> <Cell>
<Cell>{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}</Cell> {!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.mi)}
</Cell>
<Cell>
{!(de.m & DeviceEntityMask.DV_READONLY) && formatValue(de.ma)}
</Cell>
<Cell>{formatValue(de.v)}</Cell> <Cell>{formatValue(de.v)}</Cell>
</Row> </Row>
))} ))}
@@ -498,14 +569,28 @@ const Customization: FC = () => {
}; };
const renderResetDialog = () => ( const renderResetDialog = () => (
<Dialog sx={dialogStyle} open={confirmReset} onClose={() => setConfirmReset(false)}> <Dialog
sx={dialogStyle}
open={confirmReset}
onClose={() => setConfirmReset(false)}
>
<DialogTitle>{LL.RESET(1)}</DialogTitle> <DialogTitle>{LL.RESET(1)}</DialogTitle>
<DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent> <DialogContent dividers>{LL.CUSTOMIZATIONS_RESET()}</DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={() => setConfirmReset(false)} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={() => setConfirmReset(false)}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={<SettingsBackupRestoreIcon />} variant="outlined" onClick={resetCustomization} color="error"> <Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={resetCustomization}
color="error"
>
{LL.RESET(0)} {LL.RESET(0)}
</Button> </Button>
</DialogActions> </DialogActions>
@@ -518,7 +603,12 @@ const Customization: FC = () => {
{deviceEntities && renderDeviceData()} {deviceEntities && renderDeviceData()}
{restartNeeded && ( {restartNeeded && (
<MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}> <MessageBox my={2} level="warning" message={LL.RESTART_TEXT(0)}>
<Button startIcon={<PowerSettingsNewIcon />} variant="contained" color="error" onClick={restart}> <Button
startIcon={<PowerSettingsNewIcon />}
variant="contained"
color="error"
onClick={restart}
>
{LL.RESTART()} {LL.RESTART()}
</Button> </Button>
</MessageBox> </MessageBox>

View File

@@ -30,7 +30,12 @@ interface SettingsCustomizationDialogProps {
selectedItem: DeviceEntity; selectedItem: DeviceEntity;
} }
const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCustomizationDialogProps) => { const CustomizationDialog = ({
open,
onClose,
onSave,
selectedItem
}: SettingsCustomizationDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem); const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
@@ -38,7 +43,9 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
const updateFormValue = updateValue(setEditItem); const updateFormValue = updateValue(setEditItem);
const isWriteableNumber = const isWriteableNumber =
typeof editItem.v === 'number' && editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY); typeof editItem.v === 'number' &&
editItem.w &&
!(editItem.m & DeviceEntityMask.DV_READONLY);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@@ -52,7 +59,12 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
}; };
const save = () => { const save = () => {
if (isWriteableNumber && editItem.mi && editItem.ma && editItem.mi > editItem?.ma) { if (
isWriteableNumber &&
editItem.mi &&
editItem.ma &&
editItem.mi > editItem?.ma
) {
setError(true); setError(true);
} else { } else {
onSave(editItem); onSave(editItem);
@@ -140,10 +152,20 @@ const CustomizationDialog = ({ open, onClose, onSave, selectedItem }: SettingsCu
)} )}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={<DoneIcon />} variant="outlined" onClick={save} color="primary"> <Button
startIcon={<DoneIcon />}
variant="outlined"
onClick={save}
color="primary"
>
{LL.UPDATE()} {LL.UPDATE()}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -3,7 +3,12 @@ import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/
import { CgSmartHomeBoiler } from 'react-icons/cg'; import { CgSmartHomeBoiler } from 'react-icons/cg';
import { FaSolarPanel } from 'react-icons/fa'; import { FaSolarPanel } from 'react-icons/fa';
import { GiHeatHaze, GiTap } from 'react-icons/gi'; import { GiHeatHaze, GiTap } from 'react-icons/gi';
import { MdOutlineDevices, MdOutlinePool, MdOutlineSensors, MdThermostatAuto } from 'react-icons/md'; import {
MdOutlineDevices,
MdOutlinePool,
MdOutlineSensors,
MdThermostatAuto
} from 'react-icons/md';
import { TiFlowSwitch } from 'react-icons/ti'; import { TiFlowSwitch } from 'react-icons/ti';
import { VscVmConnect } from 'react-icons/vsc'; import { VscVmConnect } from 'react-icons/vsc';
@@ -47,7 +52,11 @@ const DeviceIcon: FC<DeviceIconProps> = ({ type_id }) => {
case DeviceType.POOL: case DeviceType.POOL:
return <MdOutlinePool />; return <MdOutlinePool />;
case DeviceType.CUSTOM: case DeviceType.CUSTOM:
return <PlaylistAddIcon sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }} />; return (
<PlaylistAddIcon
sx={{ color: 'lightblue', fontSize: 22, verticalAlign: 'middle' }}
/>
);
default: default:
return null; return null;
} }

View File

@@ -1,4 +1,10 @@
import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'react'; import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useState
} from 'react';
import type { FC } from 'react'; import type { FC } from 'react';
import { IconContext } from 'react-icons'; import { IconContext } from 'react-icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -35,7 +41,15 @@ import {
import { useRowSelect } from '@table-library/react-table-library/select'; import { useRowSelect } from '@table-library/react-table-library/select';
import { SortToggleType, useSort } from '@table-library/react-table-library/sort'; import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import type { Action, State } from '@table-library/react-table-library/types/common'; import type { Action, State } from '@table-library/react-table-library/types/common';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
@@ -67,19 +81,25 @@ const Devices: FC = () => {
useLayoutTitle(LL.DEVICES()); useLayoutTitle(LL.DEVICES());
const { data: coreData, send: readCoreData } = useRequest(() => EMSESP.readCoreData(), { const { data: coreData, send: readCoreData } = useRequest(
initialData: { () => EMSESP.readCoreData(),
connected: true, {
devices: [] initialData: {
connected: true,
devices: []
}
} }
}); );
const { data: deviceData, send: readDeviceData } = useRequest((id: number) => EMSESP.readDeviceData(id), { const { data: deviceData, send: readDeviceData } = useRequest(
initialData: { (id: number) => EMSESP.readDeviceData(id),
data: [] {
}, initialData: {
immediate: false data: []
}); },
immediate: false
}
);
const { loading: submitting, send: writeDeviceValue } = useRequest( const { loading: submitting, send: writeDeviceValue } = useRequest(
(data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data), (data: { id: number; c: string; v: unknown }) => EMSESP.writeDeviceValue(data),
@@ -235,9 +255,14 @@ const Devices: FC = () => {
}, },
sortToggleType: SortToggleType.AlternateWithReset, sortToggleType: SortToggleType.AlternateWithReset,
sortFns: { sortFns: {
NAME: (array) => array.sort((a, b) => a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))), NAME: (array) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call array.sort((a, b) =>
VALUE: (array) => array.sort((a, b) => a.v.toString().localeCompare(b.v.toString())) a.id.toString().slice(2).localeCompare(b.id.toString().slice(2))
),
VALUE: (array) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
array.sort((a, b) => a.v.toString().localeCompare(b.v.toString()))
} }
} }
); );
@@ -300,35 +325,59 @@ const Devices: FC = () => {
if (sc === '' || sc === '""') { if (sc === '' || sc === '""') {
return sc; return sc;
} }
if (sc.includes('"') || sc.includes(';') || sc.includes('\n') || sc.includes('\r')) { if (
sc.includes('"') ||
sc.includes(';') ||
sc.includes('\n') ||
sc.includes('\r')
) {
return '"' + sc.replace(/"/g, '""') + '"'; return '"' + sc.replace(/"/g, '""') + '"';
} }
return sc; return sc;
}; };
const hasMask = (id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask; const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask;
const handleDownloadCsv = () => { const handleDownloadCsv = () => {
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id
);
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return;
} }
const filename = coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n; const filename =
coreData.devices[deviceIndex].tn + '_' + coreData.devices[deviceIndex].n;
const columns = [ const columns = [
{ accessor: (dv: DeviceValue) => dv.id.slice(2), name: LL.ENTITY_NAME(0) },
{ {
accessor: (dv: DeviceValue) => (typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v), accessor: (dv: DeviceValue) => dv.id.slice(2),
name: LL.ENTITY_NAME(0)
},
{
accessor: (dv: DeviceValue) =>
typeof dv.v === 'number' ? new Intl.NumberFormat().format(dv.v) : dv.v,
name: LL.VALUE(1) name: LL.VALUE(1)
}, },
{ accessor: (dv: DeviceValue) => DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, ''), name: 'UoM' },
{ {
accessor: (dv: DeviceValue) => (dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) ? 'yes' : 'no'), accessor: (dv: DeviceValue) =>
DeviceValueUOM_s[dv.u].replace(/[^a-zA-Z0-9]/g, ''),
name: 'UoM'
},
{
accessor: (dv: DeviceValue) =>
dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) ? 'yes' : 'no',
name: LL.WRITEABLE() name: LL.WRITEABLE()
}, },
{ {
accessor: (dv: DeviceValue) => accessor: (dv: DeviceValue) =>
dv.h ? dv.h : dv.l ? dv.l.join(' | ') : dv.m !== undefined && dv.x !== undefined ? dv.m + ', ' + dv.x : '', dv.h
? dv.h
: dv.l
? dv.l.join(' | ')
: dv.m !== undefined && dv.x !== undefined
? dv.m + ', ' + dv.x
: '',
name: 'Range' name: 'Range'
} }
]; ];
@@ -341,10 +390,13 @@ const Devices: FC = () => {
(csvString: string, rowItem: DeviceValue) => (csvString: string, rowItem: DeviceValue) =>
csvString + csvString +
columns columns
.map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) => escapeCsvCell(accessor(rowItem) as string)) .map(({ accessor }: { accessor: (dv: DeviceValue) => unknown }) =>
escapeCsvCell(accessor(rowItem) as string)
)
.join(';') + .join(';') +
'\r\n', '\r\n',
columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') + '\r\n' columns.map(({ name }: { name: string }) => escapeCsvCell(name)).join(';') +
'\r\n'
); );
const csvFile = new Blob([csvData], { type: 'text/csv;charset:utf-8' }); const csvFile = new Blob([csvData], { type: 'text/csv;charset:utf-8' });
@@ -381,45 +433,76 @@ const Devices: FC = () => {
const renderDeviceDetails = () => { const renderDeviceDetails = () => {
if (showDeviceInfo) { if (showDeviceInfo) {
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id
);
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return;
} }
return ( return (
<Dialog sx={dialogStyle} open={showDeviceInfo} onClose={() => setShowDeviceInfo(false)}> <Dialog
sx={dialogStyle}
open={showDeviceInfo}
onClose={() => setShowDeviceInfo(false)}
>
<DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle> <DialogTitle>{LL.DEVICE_DETAILS()}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<List dense={true}> <List dense={true}>
<ListItem> <ListItem>
<ListItemText primary={LL.TYPE(0)} secondary={coreData.devices[deviceIndex].tn} /> <ListItemText
primary={LL.TYPE(0)}
secondary={coreData.devices[deviceIndex].tn}
/>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText primary={LL.NAME(0)} secondary={coreData.devices[deviceIndex].n} /> <ListItemText
primary={LL.NAME(0)}
secondary={coreData.devices[deviceIndex].n}
/>
</ListItem> </ListItem>
{coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && ( {coreData.devices[deviceIndex].t !== DeviceType.CUSTOM && (
<> <>
<ListItem> <ListItem>
<ListItemText primary={LL.BRAND()} secondary={coreData.devices[deviceIndex].b} /> <ListItemText
primary={LL.BRAND()}
secondary={coreData.devices[deviceIndex].b}
/>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText <ListItemText
primary={LL.ID_OF(LL.DEVICE())} primary={LL.ID_OF(LL.DEVICE())}
secondary={'0x' + ('00' + coreData.devices[deviceIndex].d.toString(16).toUpperCase()).slice(-2)} secondary={
'0x' +
(
'00' +
coreData.devices[deviceIndex].d.toString(16).toUpperCase()
).slice(-2)
}
/> />
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText primary={LL.ID_OF(LL.PRODUCT())} secondary={coreData.devices[deviceIndex].p} /> <ListItemText
primary={LL.ID_OF(LL.PRODUCT())}
secondary={coreData.devices[deviceIndex].p}
/>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemText primary={LL.VERSION()} secondary={coreData.devices[deviceIndex].v} /> <ListItemText
primary={LL.VERSION()}
secondary={coreData.devices[deviceIndex].v}
/>
</ListItem> </ListItem>
</> </>
)} )}
</List> </List>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button variant="outlined" onClick={() => setShowDeviceInfo(false)} color="secondary"> <Button
variant="outlined"
onClick={() => setShowDeviceInfo(false)}
color="secondary"
>
{LL.CLOSE()} {LL.CLOSE()}
</Button> </Button>
</DialogActions> </DialogActions>
@@ -429,11 +512,24 @@ const Devices: FC = () => {
}; };
const renderCoreData = () => ( const renderCoreData = () => (
<IconContext.Provider value={{ color: 'lightblue', size: '18', style: { verticalAlign: 'middle' } }}> <IconContext.Provider
{!coreData.connected && <MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />} value={{
color: 'lightblue',
size: '18',
style: { verticalAlign: 'middle' }
}}
>
{!coreData.connected && (
<MessageBox my={2} level="error" message={LL.EMS_BUS_WARNING()} />
)}
{coreData.connected && ( {coreData.connected && (
<Table data={{ nodes: coreData.devices }} select={device_select} theme={device_theme} layout={{ custom: true }}> <Table
data={{ nodes: coreData.devices }}
select={device_select}
theme={device_theme}
layout={{ custom: true }}
>
{(tableList: Device[]) => ( {(tableList: Device[]) => (
<> <>
<Header> <Header>
@@ -451,7 +547,9 @@ const Devices: FC = () => {
</Cell> </Cell>
<Cell> <Cell>
{device.n} {device.n}
<span style={{ color: 'lightblue' }}>&nbsp;&nbsp;({device.e})</span> <span style={{ color: 'lightblue' }}>
&nbsp;&nbsp;({device.e})
</span>
</Cell> </Cell>
<Cell stiff>{device.tn}</Cell> <Cell stiff>{device.tn}</Cell>
</Row> </Row>
@@ -481,8 +579,12 @@ const Devices: FC = () => {
const renderNameCell = (dv: DeviceValue) => ( const renderNameCell = (dv: DeviceValue) => (
<> <>
{dv.id.slice(2)}&nbsp; {dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && <StarIcon color="primary" sx={{ fontSize: 12 }} />} {hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && <EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />} <StarIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && ( {hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} /> <CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)} )}
@@ -493,7 +595,9 @@ const Devices: FC = () => {
? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE)) ? deviceData.data.filter((dv) => hasMask(dv.id, DeviceEntityMask.DV_FAVORITE))
: deviceData.data; : deviceData.data;
const deviceIndex = coreData.devices.findIndex((d) => d.id === device_select.state.id); const deviceIndex = coreData.devices.findIndex(
(d) => d.id === device_select.state.id
);
if (deviceIndex === -1) { if (deviceIndex === -1) {
return; return;
} }
@@ -514,7 +618,8 @@ const Devices: FC = () => {
> >
<Box sx={{ border: '1px solid #177ac9' }}> <Box sx={{ border: '1px solid #177ac9' }}>
<Typography noWrap variant="subtitle1" color="warning.main" sx={{ ml: 1 }}> <Typography noWrap variant="subtitle1" color="warning.main" sx={{ ml: 1 }}>
{coreData.devices[deviceIndex].tn}&nbsp;&#124;&nbsp;{coreData.devices[deviceIndex].n} {coreData.devices[deviceIndex].tn}&nbsp;&#124;&nbsp;
{coreData.devices[deviceIndex].n}
</Typography> </Typography>
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
@@ -527,30 +632,50 @@ const Devices: FC = () => {
' ' + ' ' +
LL.ENTITIES(shown_data.length)} LL.ENTITIES(shown_data.length)}
<IconButton onClick={() => setShowDeviceInfo(true)}> <IconButton onClick={() => setShowDeviceInfo(true)}>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <InfoOutlinedIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton> </IconButton>
{me.admin && ( {me.admin && (
<IconButton onClick={customize}> <IconButton onClick={customize}>
<FormatListNumberedIcon sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <FormatListNumberedIcon
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton> </IconButton>
)} )}
<IconButton onClick={handleDownloadCsv}> <IconButton onClick={handleDownloadCsv}>
<DownloadIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <DownloadIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton> </IconButton>
<IconButton onClick={() => setOnlyFav(!onlyFav)}> <IconButton onClick={() => setOnlyFav(!onlyFav)}>
{onlyFav ? ( {onlyFav ? (
<StarIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <StarIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
) : ( ) : (
<StarBorderOutlinedIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <StarBorderOutlinedIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
)} )}
</IconButton> </IconButton>
<IconButton onClick={refreshData}> <IconButton onClick={refreshData}>
<RefreshIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <RefreshIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton> </IconButton>
</Typography> </Typography>
<Grid item zeroMinWidth justifyContent="flex-end"> <Grid item zeroMinWidth justifyContent="flex-end">
<IconButton onClick={resetDeviceSelect}> <IconButton onClick={resetDeviceSelect}>
<HighlightOffIcon color="primary" sx={{ fontSize: 18, verticalAlign: 'middle' }} /> <HighlightOffIcon
color="primary"
sx={{ fontSize: 18, verticalAlign: 'middle' }}
/>
</IconButton> </IconButton>
</Grid> </Grid>
</Grid> </Grid>
@@ -595,15 +720,20 @@ const Devices: FC = () => {
<Cell>{renderNameCell(dv)}</Cell> <Cell>{renderNameCell(dv)}</Cell>
<Cell>{formatValue(LL, dv.v, dv.u)}</Cell> <Cell>{formatValue(LL, dv.v, dv.u)}</Cell>
<Cell stiff> <Cell stiff>
{me.admin && dv.c && !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( {me.admin &&
<IconButton size="small" onClick={() => showDeviceValue(dv)}> dv.c &&
{dv.v === '' && dv.c ? ( !hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<PlayArrowIcon color="primary" sx={{ fontSize: 16 }} /> <IconButton
) : ( size="small"
<EditIcon color="primary" sx={{ fontSize: 16 }} /> onClick={() => showDeviceValue(dv)}
)} >
</IconButton> {dv.v === '' && dv.c ? (
)} <PlayArrowIcon color="primary" sx={{ fontSize: 16 }} />
) : (
<EditIcon color="primary" sx={{ fontSize: 16 }} />
)}
</IconButton>
)}
</Cell> </Cell>
</Row> </Row>
))} ))}
@@ -627,14 +757,20 @@ const Devices: FC = () => {
onSave={deviceValueDialogSave} onSave={deviceValueDialogSave}
selectedItem={selectedDeviceValue} selectedItem={selectedDeviceValue}
writeable={ writeable={
selectedDeviceValue.c !== undefined && !hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY) selectedDeviceValue.c !== undefined &&
!hasMask(selectedDeviceValue.id, DeviceEntityMask.DV_READONLY)
} }
validator={deviceValueItemValidation(selectedDeviceValue)} validator={deviceValueItemValidation(selectedDeviceValue)}
progress={submitting} progress={submitting}
/> />
)} )}
<ButtonRow mt={1}> <ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={refreshData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={refreshData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -102,7 +102,11 @@ const DevicesDialog = ({
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={close}> <Dialog sx={dialogStyle} open={open} onClose={close}>
<DialogTitle> <DialogTitle>
{selectedItem.v === '' && selectedItem.c ? LL.RUN_COMMAND() : writeable ? LL.CHANGE_VALUE() : LL.VALUE(1)} {selectedItem.v === '' && selectedItem.c
? LL.RUN_COMMAND()
: writeable
? LL.CHANGE_VALUE()
: LL.VALUE(1)}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}> <Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
@@ -138,9 +142,17 @@ const DevicesDialog = ({
type="number" type="number"
sx={{ width: '30ch' }} sx={{ width: '30ch' }}
onChange={updateFormValue} onChange={updateFormValue}
inputProps={editItem.s ? { min: editItem.m, max: editItem.x, step: editItem.s } : {}} inputProps={
editItem.s
? { min: editItem.m, max: editItem.x, step: editItem.s }
: {}
}
InputProps={{ InputProps={{
startAdornment: <InputAdornment position="start">{setUom(editItem.u)}</InputAdornment> startAdornment: (
<InputAdornment position="start">
{setUom(editItem.u)}
</InputAdornment>
)
}} }}
/> />
) : ( ) : (
@@ -175,10 +187,20 @@ const DevicesDialog = ({
position: 'relative' position: 'relative'
}} }}
> >
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info"> <Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
onClick={save}
color="info"
>
{selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()} {selectedItem.v === '' && selectedItem.c ? LL.EXECUTE() : LL.UPDATE()}
</Button> </Button>
{progress && ( {progress && (

View File

@@ -55,25 +55,46 @@ const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
}} }}
> >
<ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}> <ToggleButton value="8" disabled={(de.m & 0x81) !== 0 || de.n === undefined}>
<OptionIcon type="favorite" isSet={(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE} /> <OptionIcon
type="favorite"
isSet={
(de.m & DeviceEntityMask.DV_FAVORITE) === DeviceEntityMask.DV_FAVORITE
}
/>
</ToggleButton> </ToggleButton>
<ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}> <ToggleButton value="4" disabled={!de.w || (de.m & 0x83) >= 3}>
<OptionIcon type="readonly" isSet={(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY} /> <OptionIcon
type="readonly"
isSet={
(de.m & DeviceEntityMask.DV_READONLY) === DeviceEntityMask.DV_READONLY
}
/>
</ToggleButton> </ToggleButton>
<ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}> <ToggleButton value="2" disabled={de.n === '' || (de.m & 0x80) !== 0}>
<OptionIcon <OptionIcon
type="api_mqtt_exclude" type="api_mqtt_exclude"
isSet={(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) === DeviceEntityMask.DV_API_MQTT_EXCLUDE} isSet={
(de.m & DeviceEntityMask.DV_API_MQTT_EXCLUDE) ===
DeviceEntityMask.DV_API_MQTT_EXCLUDE
}
/> />
</ToggleButton> </ToggleButton>
<ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}> <ToggleButton value="1" disabled={de.n === undefined || (de.m & 0x80) !== 0}>
<OptionIcon <OptionIcon
type="web_exclude" type="web_exclude"
isSet={(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) === DeviceEntityMask.DV_WEB_EXCLUDE} isSet={
(de.m & DeviceEntityMask.DV_WEB_EXCLUDE) ===
DeviceEntityMask.DV_WEB_EXCLUDE
}
/> />
</ToggleButton> </ToggleButton>
<ToggleButton value="128"> <ToggleButton value="128">
<OptionIcon type="deleted" isSet={(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED} /> <OptionIcon
type="deleted"
isSet={
(de.m & DeviceEntityMask.DV_DELETED) === DeviceEntityMask.DV_DELETED
}
/>
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
); );

View File

@@ -29,9 +29,12 @@ const Help: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.HELP_OF('')); useLayoutTitle(LL.HELP_OF(''));
const { send: getAPI, onSuccess: onGetAPI } = useRequest((data: APIcall) => EMSESP.API(data), { const { send: getAPI, onSuccess: onGetAPI } = useRequest(
immediate: false (data: APIcall) => EMSESP.API(data),
}); {
immediate: false
}
);
onGetAPI((event) => { onGetAPI((event) => {
const anchor = document.createElement('a'); const anchor = document.createElement('a');
@@ -40,8 +43,10 @@ const Help: FC = () => {
type: 'text/plain' type: 'text/plain'
}) })
); );
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
anchor.download = 'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt'; anchor.download =
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
'emsesp_' + event.sendArgs[0].device + '_' + event.sendArgs[0].entity + '.txt';
anchor.click(); anchor.click();
URL.revokeObjectURL(anchor.href); URL.revokeObjectURL(anchor.href);
toast.info(LL.DOWNLOAD_SUCCESSFUL()); toast.info(LL.DOWNLOAD_SUCCESSFUL());
@@ -79,7 +84,10 @@ const Help: FC = () => {
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemButton component="a" href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"> <ListItemButton
component="a"
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={{ bgcolor: '#72caf9' }}> <Avatar sx={{ bgcolor: '#72caf9' }}>
<GitHubIcon /> <GitHubIcon />
@@ -119,7 +127,11 @@ const Help: FC = () => {
<b>{LL.HELP_INFORMATION_5()}</b> <b>{LL.HELP_INFORMATION_5()}</b>
</Typography> </Typography>
<Typography align="center"> <Typography align="center">
<Link target="_blank" href="https://github.com/emsesp/EMS-ESP32" color="primary"> <Link
target="_blank"
href="https://github.com/emsesp/EMS-ESP32"
color="primary"
>
{'github.com/emsesp/EMS-ESP32'} {'github.com/emsesp/EMS-ESP32'}
</Link> </Link>
</Typography> </Typography>

View File

@@ -12,9 +12,19 @@ import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import type { SvgIconProps } from '@mui/material'; import type { SvgIconProps } from '@mui/material';
type OptionType = 'deleted' | 'readonly' | 'web_exclude' | 'api_mqtt_exclude' | 'favorite'; type OptionType =
| 'deleted'
| 'readonly'
| 'web_exclude'
| 'api_mqtt_exclude'
| 'favorite';
const OPTION_ICONS: { [type in OptionType]: [React.ComponentType<SvgIconProps>, React.ComponentType<SvgIconProps>] } = { const OPTION_ICONS: {
[type in OptionType]: [
React.ComponentType<SvgIconProps>,
React.ComponentType<SvgIconProps>
];
} = {
deleted: [DeleteForeverIcon, DeleteOutlineIcon], deleted: [DeleteForeverIcon, DeleteOutlineIcon],
readonly: [EditOffOutlinedIcon, EditOutlinedIcon], readonly: [EditOffOutlinedIcon, EditOutlinedIcon],
web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon], web_exclude: [VisibilityOffOutlinedIcon, VisibilityOutlinedIcon],

View File

@@ -9,10 +9,24 @@ import CircleIcon from '@mui/icons-material/Circle';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { Box, Button, Divider, Stack, Typography } from '@mui/material'; import { Box, Button, Divider, Stack, Typography } from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import { updateState, useRequest } from 'alova'; import { updateState, useRequest } from 'alova';
import { BlockNavigation, ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components'; import {
BlockNavigation,
ButtonRow,
FormLoader,
SectionContent,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api'; import * as EMSESP from './api';
@@ -39,9 +53,12 @@ const Scheduler: FC = () => {
force: true force: true
}); });
const { send: writeSchedule } = useRequest((data: Schedule) => EMSESP.writeSchedule(data), { const { send: writeSchedule } = useRequest(
immediate: false (data: Schedule) => EMSESP.writeSchedule(data),
}); {
immediate: false
}
);
function hasScheduleChanged(si: ScheduleItem) { function hasScheduleChanged(si: ScheduleItem) {
return ( return (
@@ -57,7 +74,10 @@ const Scheduler: FC = () => {
} }
useEffect(() => { useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }); const formatter = new Intl.DateTimeFormat(locale, {
weekday: 'short',
timeZone: 'UTC'
});
const days = [1, 2, 3, 4, 5, 6, 7].map((day) => { const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
const dd = day < 10 ? `0${day}` : day; const dd = day < 10 ? `0${day}` : day;
return new Date(`2017-01-${dd}T00:00:00+00:00`); return new Date(`2017-01-${dd}T00:00:00+00:00`);
@@ -157,8 +177,13 @@ const Scheduler: FC = () => {
updateState('schedule', (data: ScheduleItem[]) => { updateState('schedule', (data: ScheduleItem[]) => {
const new_data = creating const new_data = creating
? [...data.filter((si) => creating || si.o_id !== updatedItem.o_id), updatedItem] ? [
: data.map((si) => (si.id === updatedItem.id ? { ...si, ...updatedItem } : si)); ...data.filter((si) => creating || si.o_id !== updatedItem.o_id),
updatedItem
]
: data.map((si) =>
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
);
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length); setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
@@ -189,8 +214,13 @@ const Scheduler: FC = () => {
const dayBox = (si: ScheduleItem, flag: number) => ( const dayBox = (si: ScheduleItem, flag: number) => (
<> <>
<Box> <Box>
<Typography sx={{ fontSize: 11 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}> <Typography
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]} sx={{ fontSize: 11 }}
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
>
{flag === ScheduleFlag.SCHEDULE_TIMER
? LL.TIMER(0)
: dow[Math.log(flag) / Math.log(2)]}
</Typography> </Typography>
</Box> </Box>
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
@@ -201,7 +231,11 @@ const Scheduler: FC = () => {
return ( return (
<Table <Table
data={{ nodes: schedule.filter((si) => !si.deleted).sort((a, b) => a.time.localeCompare(b.time)) }} data={{
nodes: schedule
.filter((si) => !si.deleted)
.sort((a, b) => a.time.localeCompare(b.time))
}}
theme={schedule_theme} theme={schedule_theme}
layout={{ custom: true }} layout={{ custom: true }}
> >
@@ -222,9 +256,15 @@ const Scheduler: FC = () => {
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}> <Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
<Cell stiff> <Cell stiff>
{si.active ? ( {si.active ? (
<CircleIcon color="success" sx={{ fontSize: 16, verticalAlign: 'middle' }} /> <CircleIcon
color="success"
sx={{ fontSize: 16, verticalAlign: 'middle' }}
/>
) : ( ) : (
<CircleIcon color="error" sx={{ fontSize: 16, verticalAlign: 'middle' }} /> <CircleIcon
color="error"
sx={{ fontSize: 16, verticalAlign: 'middle' }}
/>
)} )}
</Cell> </Cell>
<Cell stiff> <Cell stiff>
@@ -277,7 +317,12 @@ const Scheduler: FC = () => {
<Box flexGrow={1}> <Box flexGrow={1}>
{numChanges !== 0 && ( {numChanges !== 0 && (
<ButtonRow> <ButtonRow>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={onDialogCancel} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onDialogCancel}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button <Button
@@ -293,7 +338,12 @@ const Scheduler: FC = () => {
</Box> </Box>
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow> <ButtonRow>
<Button startIcon={<AddIcon />} variant="outlined" color="secondary" onClick={addScheduleItem}> <Button
startIcon={<AddIcon />}
variant="outlined"
color="secondary"
onClick={addScheduleItem}
>
{LL.ADD(0)} {LL.ADD(0)}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -40,7 +40,15 @@ interface SchedulerDialogProps {
dow: string[]; dow: string[];
} }
const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, validator, dow }: SchedulerDialogProps) => { const SchedulerDialog = ({
open,
creating,
onClose,
onSave,
selectedItem,
validator,
dow
}: SchedulerDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem); const [editItem, setEditItem] = useState<ScheduleItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
@@ -111,8 +119,14 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
}; };
const showFlag = (si: ScheduleItem, flag: number) => ( const showFlag = (si: ScheduleItem, flag: number) => (
<Typography variant="button" sx={{ fontSize: 10 }} color={(si.flags & flag) === flag ? 'primary' : 'grey'}> <Typography
{flag === ScheduleFlag.SCHEDULE_TIMER ? LL.TIMER(0) : dow[Math.log(flag) / Math.log(2)]} variant="button"
sx={{ fontSize: 10 }}
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
>
{flag === ScheduleFlag.SCHEDULE_TIMER
? LL.TIMER(0)
: dow[Math.log(flag) / Math.log(2)]}
</Typography> </Typography>
); );
@@ -121,7 +135,8 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={close}> <Dialog sx={dialogStyle} open={open} onClose={close}>
<DialogTitle> <DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;{LL.SCHEDULE(1)} {creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;
{LL.SCHEDULE(1)}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Box display="flex" flexWrap="wrap" mb={1}> <Box display="flex" flexWrap="wrap" mb={1}>
@@ -134,13 +149,27 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 }); setEditItem({ ...editItem, flags: getFlagNumber(flag) & 127 });
}} }}
> >
<ToggleButton value="2">{showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}</ToggleButton> <ToggleButton value="2">
<ToggleButton value="4">{showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}</ToggleButton> {showFlag(editItem, ScheduleFlag.SCHEDULE_MON)}
<ToggleButton value="8">{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}</ToggleButton> </ToggleButton>
<ToggleButton value="16">{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}</ToggleButton> <ToggleButton value="4">
<ToggleButton value="32">{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}</ToggleButton> {showFlag(editItem, ScheduleFlag.SCHEDULE_TUE)}
<ToggleButton value="64">{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}</ToggleButton> </ToggleButton>
<ToggleButton value="1">{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}</ToggleButton> <ToggleButton value="8">
{showFlag(editItem, ScheduleFlag.SCHEDULE_WED)}
</ToggleButton>
<ToggleButton value="16">
{showFlag(editItem, ScheduleFlag.SCHEDULE_THU)}
</ToggleButton>
<ToggleButton value="32">
{showFlag(editItem, ScheduleFlag.SCHEDULE_FRI)}
</ToggleButton>
<ToggleButton value="64">
{showFlag(editItem, ScheduleFlag.SCHEDULE_SAT)}
</ToggleButton>
<ToggleButton value="1">
{showFlag(editItem, ScheduleFlag.SCHEDULE_SUN)}
</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</Box> </Box>
<Box flexWrap="nowrap" whiteSpace="nowrap"> <Box flexWrap="nowrap" whiteSpace="nowrap">
@@ -160,7 +189,10 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
size="large" size="large"
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER }); setEditItem({
...editItem,
flags: ScheduleFlag.SCHEDULE_TIMER
});
}} }}
> >
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)} {showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
@@ -170,7 +202,13 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
</Box> </Box>
<Grid container> <Grid container>
<BlockFormControlLabel <BlockFormControlLabel
control={<Checkbox checked={editItem.active} onChange={updateFormValue} name="active" />} control={
<Checkbox
checked={editItem.active}
onChange={updateFormValue}
name="active"
/>
}
label={LL.ACTIVE()} label={LL.ACTIVE()}
/> />
</Grid> </Grid>
@@ -220,15 +258,30 @@ const SchedulerDialog = ({ open, creating, onClose, onSave, selectedItem, valida
<DialogActions> <DialogActions>
{!creating && ( {!creating && (
<Box flexGrow={1}> <Box flexGrow={1}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="warning" onClick={remove}> <Button
startIcon={<RemoveIcon />}
variant="outlined"
color="warning"
onClick={remove}
>
{LL.REMOVE()} {LL.REMOVE()}
</Button> </Button>
</Box> </Box>
)} )}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={creating ? <AddIcon /> : <DoneIcon />} variant="outlined" onClick={save} color="primary"> <Button
startIcon={creating ? <AddIcon /> : <DoneIcon />}
variant="outlined"
onClick={save}
color="primary"
>
{creating ? LL.ADD(0) : LL.UPDATE()} {creating ? LL.ADD(0) : LL.UPDATE()}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -10,7 +10,15 @@ import UnfoldMoreOutlinedIcon from '@mui/icons-material/UnfoldMoreOutlined';
import { Box, Button, Typography } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
import { SortToggleType, useSort } from '@table-library/react-table-library/sort'; import { SortToggleType, useSort } from '@table-library/react-table-library/sort';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme } from '@table-library/react-table-library/theme'; import { useTheme } from '@table-library/react-table-library/theme';
import type { State } from '@table-library/react-table-library/types/common'; import type { State } from '@table-library/react-table-library/types/common';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
@@ -21,28 +29,45 @@ import { useI18nContext } from 'i18n/i18n-react';
import * as EMSESP from './api'; import * as EMSESP from './api';
import DashboardSensorsAnalogDialog from './SensorsAnalogDialog'; import DashboardSensorsAnalogDialog from './SensorsAnalogDialog';
import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog'; import DashboardSensorsTemperatureDialog from './SensorsTemperatureDialog';
import { AnalogType, AnalogTypeNames, DeviceValueUOM, DeviceValueUOM_s } from './types'; import {
import type { AnalogSensor, TemperatureSensor, WriteAnalogSensor, WriteTemperatureSensor } from './types'; AnalogType,
import { analogSensorItemValidation, temperatureSensorItemValidation } from './validators'; AnalogTypeNames,
DeviceValueUOM,
DeviceValueUOM_s
} from './types';
import type {
AnalogSensor,
TemperatureSensor,
WriteAnalogSensor,
WriteTemperatureSensor
} from './types';
import {
analogSensorItemValidation,
temperatureSensorItemValidation
} from './validators';
const Sensors: FC = () => { const Sensors: FC = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
const [selectedTemperatureSensor, setSelectedTemperatureSensor] = useState<TemperatureSensor>(); const [selectedTemperatureSensor, setSelectedTemperatureSensor] =
useState<TemperatureSensor>();
const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>(); const [selectedAnalogSensor, setSelectedAnalogSensor] = useState<AnalogSensor>();
const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false); const [temperatureDialogOpen, setTemperatureDialogOpen] = useState<boolean>(false);
const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false); const [analogDialogOpen, setAnalogDialogOpen] = useState<boolean>(false);
const [creating, setCreating] = useState<boolean>(false); const [creating, setCreating] = useState<boolean>(false);
const { data: sensorData, send: fetchSensorData } = useRequest(() => EMSESP.readSensorData(), { const { data: sensorData, send: fetchSensorData } = useRequest(
initialData: { () => EMSESP.readSensorData(),
ts: [], {
as: [], initialData: {
analog_enabled: false, ts: [],
platform: 'ESP32' as: [],
analog_enabled: false,
platform: 'ESP32'
}
} }
}); );
const { send: writeTemperatureSensor } = useRequest( const { send: writeTemperatureSensor } = useRequest(
(data: WriteTemperatureSensor) => EMSESP.writeTemperatureSensor(data), (data: WriteTemperatureSensor) => EMSESP.writeTemperatureSensor(data),
@@ -51,9 +76,12 @@ const Sensors: FC = () => {
} }
); );
const { send: writeAnalogSensor } = useRequest((data: WriteAnalogSensor) => EMSESP.writeAnalogSensor(data), { const { send: writeAnalogSensor } = useRequest(
immediate: false (data: WriteAnalogSensor) => EMSESP.writeAnalogSensor(data),
}); {
immediate: false
}
);
const common_theme = useTheme({ const common_theme = useTheme({
BaseRow: ` BaseRow: `
@@ -304,7 +332,12 @@ const Sensors: FC = () => {
}; };
const RenderTemperatureSensors = () => ( const RenderTemperatureSensors = () => (
<Table data={{ nodes: sensorData.ts }} theme={temperature_theme} sort={temperature_sort} layout={{ custom: true }}> <Table
data={{ nodes: sensorData.ts }}
theme={temperature_theme}
sort={temperature_sort}
layout={{ custom: true }}
>
{(tableList: TemperatureSensor[]) => ( {(tableList: TemperatureSensor[]) => (
<> <>
<Header> <Header>
@@ -314,7 +347,9 @@ const Sensors: FC = () => {
fullWidth fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-start' }} style={{ fontSize: '14px', justifyContent: 'flex-start' }}
endIcon={getSortIcon(temperature_sort.state, 'NAME')} endIcon={getSortIcon(temperature_sort.state, 'NAME')}
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })} onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
}
> >
{LL.NAME(0)} {LL.NAME(0)}
</Button> </Button>
@@ -324,7 +359,9 @@ const Sensors: FC = () => {
fullWidth fullWidth
style={{ fontSize: '14px', justifyContent: 'flex-end' }} style={{ fontSize: '14px', justifyContent: 'flex-end' }}
endIcon={getSortIcon(temperature_sort.state, 'VALUE')} endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
onClick={() => temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })} onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
}
> >
{LL.VALUE(0)} {LL.VALUE(0)}
</Button> </Button>
@@ -345,7 +382,12 @@ const Sensors: FC = () => {
); );
const RenderAnalogSensors = () => ( const RenderAnalogSensors = () => (
<Table data={{ nodes: sensorData.as }} theme={analog_theme} sort={analog_sort} layout={{ custom: true }}> <Table
data={{ nodes: sensorData.as }}
theme={analog_theme}
sort={analog_sort}
layout={{ custom: true }}
>
{(tableList: AnalogSensor[]) => ( {(tableList: AnalogSensor[]) => (
<> <>
<Header> <Header>
@@ -439,7 +481,11 @@ const Sensors: FC = () => {
onSave={onAnalogDialogSave} onSave={onAnalogDialogSave}
creating={creating} creating={creating}
selectedItem={selectedAnalogSensor} selectedItem={selectedAnalogSensor}
validator={analogSensorItemValidation(sensorData.as, creating, sensorData.platform)} validator={analogSensorItemValidation(
sensorData.as,
creating,
sensorData.platform
)}
/> />
)} )}
</> </>
@@ -447,7 +493,12 @@ const Sensors: FC = () => {
<ButtonRow> <ButtonRow>
<Box mt={1} display="flex" flexWrap="wrap"> <Box mt={1} display="flex" flexWrap="wrap">
<Box flexGrow={1}> <Box flexGrow={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={fetchSensorData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={fetchSensorData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</Box> </Box>

View File

@@ -79,7 +79,8 @@ const SensorsAnalogDialog = ({
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={close}> <Dialog sx={dialogStyle} open={open} onClose={close}>
<DialogTitle> <DialogTitle>
{creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;{LL.ANALOG_SENSOR(0)} {creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()}&nbsp;
{LL.ANALOG_SENSOR(0)}
</DialogTitle> </DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Grid container spacing={2}> <Grid container spacing={2}>
@@ -113,7 +114,14 @@ const SensorsAnalogDialog = ({
/> />
</Grid> </Grid>
<Grid item xs={8}> <Grid item xs={8}>
<TextField name="t" label={LL.TYPE(0)} value={editItem.t} fullWidth select onChange={updateFormValue}> <TextField
name="t"
label={LL.TYPE(0)}
value={editItem.t}
fullWidth
select
onChange={updateFormValue}
>
{AnalogTypeNames.map((val, i) => ( {AnalogTypeNames.map((val, i) => (
<MenuItem key={i} value={i}> <MenuItem key={i} value={i}>
{val} {val}
@@ -123,7 +131,14 @@ const SensorsAnalogDialog = ({
</Grid> </Grid>
{editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && ( {editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE && (
<Grid item xs={4}> <Grid item xs={4}>
<TextField name="u" label={LL.UNIT()} value={editItem.u} fullWidth select onChange={updateFormValue}> <TextField
name="u"
label={LL.UNIT()}
value={editItem.u}
fullWidth
select
onChange={updateFormValue}
>
{DeviceValueUOM_s.map((val, i) => ( {DeviceValueUOM_s.map((val, i) => (
<MenuItem key={i} value={i}> <MenuItem key={i} value={i}>
{val} {val}
@@ -144,7 +159,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
inputProps={{ min: '0', max: '3300', step: '1' }} inputProps={{ min: '0', max: '3300', step: '1' }}
InputProps={{ InputProps={{
startAdornment: <InputAdornment position="start">mV</InputAdornment> startAdornment: (
<InputAdornment position="start">mV</InputAdornment>
)
}} }}
/> />
</Grid> </Grid>
@@ -177,70 +194,75 @@ const SensorsAnalogDialog = ({
/> />
</Grid> </Grid>
)} )}
{editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26) && ( {editItem.t === AnalogType.DIGITAL_OUT &&
<Grid item xs={4}> (editItem.g === 25 || editItem.g === 26) && (
<TextField
name="o"
label={LL.VALUE(1)}
value={numberValue(editItem.o)}
fullWidth
type="number"
variant="outlined"
onChange={updateFormValue}
inputProps={{ min: '0', max: '255', step: '1' }}
/>
</Grid>
)}
{editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26 && (
<>
<Grid item xs={4}> <Grid item xs={4}>
<TextField <TextField
name="o" name="o"
label={LL.VALUE(1)} label={LL.VALUE(1)}
value={numberValue(editItem.o)} value={numberValue(editItem.o)}
fullWidth fullWidth
select type="number"
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
> inputProps={{ min: '0', max: '255', step: '1' }}
<MenuItem value={0}>{LL.OFF()}</MenuItem> />
<MenuItem value={1}>{LL.ON()}</MenuItem>
</TextField>
</Grid> </Grid>
<Grid item xs={4}> )}
<TextField {editItem.t === AnalogType.DIGITAL_OUT &&
name="f" editItem.g !== 25 &&
label={LL.POLARITY()} editItem.g !== 26 && (
value={editItem.f} <>
fullWidth <Grid item xs={4}>
select <TextField
onChange={updateFormValue} name="o"
> label={LL.VALUE(1)}
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem> value={numberValue(editItem.o)}
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem> fullWidth
</TextField> select
</Grid> variant="outlined"
<Grid item xs={4}> onChange={updateFormValue}
<TextField >
name="u" <MenuItem value={0}>{LL.OFF()}</MenuItem>
label={LL.STARTVALUE()} <MenuItem value={1}>{LL.ON()}</MenuItem>
value={editItem.u} </TextField>
fullWidth </Grid>
select <Grid item xs={4}>
onChange={updateFormValue} <TextField
> name="f"
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem> label={LL.POLARITY()}
<MenuItem value={1}> value={editItem.f}
{LL.ALWAYS()}&nbsp;{LL.OFF()} fullWidth
</MenuItem> select
<MenuItem value={2}> onChange={updateFormValue}
{LL.ALWAYS()}&nbsp;{LL.ON()} >
</MenuItem> <MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
</TextField> <MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
</Grid> </TextField>
</> </Grid>
)} <Grid item xs={4}>
{(editItem.t === AnalogType.PWM_0 || editItem.t === AnalogType.PWM_1 || editItem.t === AnalogType.PWM_2) && ( <TextField
name="u"
label={LL.STARTVALUE()}
value={editItem.u}
fullWidth
select
onChange={updateFormValue}
>
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
<MenuItem value={1}>
{LL.ALWAYS()}&nbsp;{LL.OFF()}
</MenuItem>
<MenuItem value={2}>
{LL.ALWAYS()}&nbsp;{LL.ON()}
</MenuItem>
</TextField>
</Grid>
</>
)}
{(editItem.t === AnalogType.PWM_0 ||
editItem.t === AnalogType.PWM_1 ||
editItem.t === AnalogType.PWM_2) && (
<> <>
<Grid item xs={4}> <Grid item xs={4}>
<TextField <TextField
@@ -253,7 +275,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
inputProps={{ min: '1', max: '5000', step: '1' }} inputProps={{ min: '1', max: '5000', step: '1' }}
InputProps={{ InputProps={{
startAdornment: <InputAdornment position="start">Hz</InputAdornment> startAdornment: (
<InputAdornment position="start">Hz</InputAdornment>
)
}} }}
/> />
</Grid> </Grid>
@@ -268,7 +292,9 @@ const SensorsAnalogDialog = ({
onChange={updateFormValue} onChange={updateFormValue}
inputProps={{ min: '0', max: '100', step: '0.1' }} inputProps={{ min: '0', max: '100', step: '0.1' }}
InputProps={{ InputProps={{
startAdornment: <InputAdornment position="start">%</InputAdornment> startAdornment: (
<InputAdornment position="start">%</InputAdornment>
)
}} }}
/> />
</Grid> </Grid>
@@ -279,15 +305,30 @@ const SensorsAnalogDialog = ({
<DialogActions> <DialogActions>
{!creating && ( {!creating && (
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}> <Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
<Button startIcon={<RemoveIcon />} variant="outlined" color="error" onClick={remove}> <Button
startIcon={<RemoveIcon />}
variant="outlined"
color="error"
onClick={remove}
>
{LL.REMOVE()} {LL.REMOVE()}
</Button> </Button>
</Box> </Box>
)} )}
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info"> <Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
onClick={save}
color="info"
>
{creating ? LL.ADD(0) : LL.UPDATE()} {creating ? LL.ADD(0) : LL.UPDATE()}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -107,10 +107,20 @@ const SensorsTemperatureDialog = ({
</Grid> </Grid>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button startIcon={<CancelIcon />} variant="outlined" onClick={close} color="secondary"> <Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={close}
color="secondary"
>
{LL.CANCEL()} {LL.CANCEL()}
</Button> </Button>
<Button startIcon={<WarningIcon color="warning" />} variant="contained" onClick={save} color="info"> <Button
startIcon={<WarningIcon color="warning" />}
variant="contained"
onClick={save}
color="info"
>
{LL.UPDATE()} {LL.UPDATE()}
</Button> </Button>
</DialogActions> </DialogActions>

View File

@@ -4,7 +4,15 @@ import type { FC } from 'react';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { Body, Cell, Header, HeaderCell, HeaderRow, Row, Table } from '@table-library/react-table-library/table'; import {
Body,
Cell,
Header,
HeaderCell,
HeaderRow,
Row,
Table
} from '@table-library/react-table-library/table';
import { useTheme as tableTheme } from '@table-library/react-table-library/theme'; import { useTheme as tableTheme } from '@table-library/react-table-library/theme';
import { useRequest } from 'alova'; import { useRequest } from 'alova';
import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components'; import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components';
@@ -93,7 +101,11 @@ const SystemActivity: FC = () => {
return ( return (
<> <>
<Table data={{ nodes: data.stats }} theme={stats_theme} layout={{ custom: true }}> <Table
data={{ nodes: data.stats }}
theme={stats_theme}
layout={{ custom: true }}
>
{(tableList: Stat[]) => ( {(tableList: Stat[]) => (
<> <>
<Header> <Header>
@@ -118,7 +130,12 @@ const SystemActivity: FC = () => {
)} )}
</Table> </Table>
<ButtonRow mt={1}> <ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="outlined" color="secondary" onClick={loadData}> <Button
startIcon={<RefreshIcon />}
variant="outlined"
color="secondary"
onClick={loadData}
>
{LL.REFRESH()} {LL.REFRESH()}
</Button> </Button>
</ButtonRow> </ButtonRow>

View File

@@ -30,17 +30,20 @@ export const writeDeviceValue = (data: { id: number; c: string; v: unknown }) =>
// Application Settings // Application Settings
export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings'); export const readSettings = () => alovaInstance.Get<Settings>('/rest/settings');
export const writeSettings = (data: Settings) => alovaInstance.Post('/rest/settings', data); export const writeSettings = (data: Settings) =>
alovaInstance.Post('/rest/settings', data);
export const getBoardProfile = (boardProfile: string) => export const getBoardProfile = (boardProfile: string) =>
alovaInstance.Get('/rest/boardProfile', { alovaInstance.Get('/rest/boardProfile', {
params: { boardProfile } params: { boardProfile }
}); });
// Sensors // Sensors
export const readSensorData = () => alovaInstance.Get<SensorData>('/rest/sensorData'); export const readSensorData = () =>
alovaInstance.Get<SensorData>('/rest/sensorData');
export const writeTemperatureSensor = (ts: WriteTemperatureSensor) => export const writeTemperatureSensor = (ts: WriteTemperatureSensor) =>
alovaInstance.Post('/rest/writeTemperatureSensor', ts); alovaInstance.Post('/rest/writeTemperatureSensor', ts);
export const writeAnalogSensor = (as: WriteAnalogSensor) => alovaInstance.Post('/rest/writeAnalogSensor', as); export const writeAnalogSensor = (as: WriteAnalogSensor) =>
alovaInstance.Post('/rest/writeAnalogSensor', as);
// Activity // Activity
export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity'); export const readActivity = () => alovaInstance.Get<Activity>('/rest/activity');
@@ -73,9 +76,12 @@ export const readDeviceEntities = (id: number) =>
} }
}); });
export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices'); export const readDevices = () => alovaInstance.Get<Devices>('/rest/devices');
export const resetCustomizations = () => alovaInstance.Post('/rest/resetCustomizations'); export const resetCustomizations = () =>
export const writeCustomizationEntities = (data: { id: number; entity_ids: string[] }) => alovaInstance.Post('/rest/resetCustomizations');
alovaInstance.Post('/rest/customizationEntities', data); export const writeCustomizationEntities = (data: {
id: number;
entity_ids: string[];
}) => alovaInstance.Post('/rest/customizationEntities', data);
// SettingsScheduler // SettingsScheduler
export const readSchedule = () => export const readSchedule = () =>
@@ -95,7 +101,8 @@ export const readSchedule = () =>
})); }));
} }
}); });
export const writeSchedule = (data: Schedule) => alovaInstance.Post('/rest/schedule', data); export const writeSchedule = (data: Schedule) =>
alovaInstance.Post('/rest/schedule', data);
// SettingsEntities // SettingsEntities
export const readCustomEntities = () => export const readCustomEntities = () =>

View File

@@ -25,7 +25,11 @@ const formatDurationMin = (LL: TranslationFunctions, duration_min: number) => {
return formatted; return formatted;
}; };
export function formatValue(LL: TranslationFunctions, value: unknown, uom: DeviceValueUOM) { export function formatValue(
LL: TranslationFunctions,
value: unknown,
uom: DeviceValueUOM
) {
if (typeof value !== 'number') { if (typeof value !== 'number') {
return ''; return '';
} }

View File

@@ -5,7 +5,11 @@ import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types'; import type { AnalogSensor, DeviceValue, ScheduleItem, Settings } from './types';
export const GPIO_VALIDATOR = { export const GPIO_VALIDATOR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if ( if (
value && value &&
(value === 1 || (value === 1 ||
@@ -24,7 +28,11 @@ export const GPIO_VALIDATOR = {
}; };
export const GPIO_VALIDATORR = { export const GPIO_VALIDATORR = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if ( if (
value && value &&
(value === 1 || (value === 1 ||
@@ -44,7 +52,11 @@ export const GPIO_VALIDATORR = {
}; };
export const GPIO_VALIDATORC3 = { export const GPIO_VALIDATORC3 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) { if (value && ((value >= 11 && value <= 19) || value > 21 || value < 0)) {
callback('Must be an valid GPIO port'); callback('Must be an valid GPIO port');
} else { } else {
@@ -54,8 +66,18 @@ export const GPIO_VALIDATORC3 = {
}; };
export const GPIO_VALIDATORS2 = { export const GPIO_VALIDATORS2 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(
if (value && ((value >= 19 && value <= 20) || (value >= 22 && value <= 32) || value > 40 || value < 0)) { rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (
value &&
((value >= 19 && value <= 20) ||
(value >= 22 && value <= 32) ||
value > 40 ||
value < 0)
) {
callback('Must be an valid GPIO port'); callback('Must be an valid GPIO port');
} else { } else {
callback(); callback();
@@ -64,7 +86,11 @@ export const GPIO_VALIDATORS2 = {
}; };
export const GPIO_VALIDATORS3 = { export const GPIO_VALIDATORS3 = {
validator(rule: InternalRuleItem, value: number, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if ( if (
value && value &&
((value >= 19 && value <= 20) || ((value >= 19 && value <= 20) ||
@@ -84,46 +110,121 @@ export const createSettingsValidator = (settings: Settings) =>
new Schema({ new Schema({
...(settings.board_profile === 'CUSTOM' && ...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32' && { settings.platform === 'ESP32' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATOR], led_gpio: [
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATOR], { required: true, message: 'LED GPIO is required' },
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATOR], GPIO_VALIDATOR
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATOR], ],
dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATOR
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATOR
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATOR
],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR] rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATOR]
}), }),
...(settings.board_profile === 'CUSTOM' && ...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32R' && { settings.platform === 'ESP32R' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORR], led_gpio: [
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORR], { required: true, message: 'LED GPIO is required' },
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORR], GPIO_VALIDATORR
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORR], ],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORR] dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORR
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORR
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORR
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORR
]
}), }),
...(settings.board_profile === 'CUSTOM' && ...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-C3' && { settings.platform === 'ESP32-C3' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORC3], led_gpio: [
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORC3], { required: true, message: 'LED GPIO is required' },
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORC3], GPIO_VALIDATORC3
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORC3], ],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORC3] dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORC3
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORC3
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORC3
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORC3
]
}), }),
...(settings.board_profile === 'CUSTOM' && ...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-S2' && { settings.platform === 'ESP32-S2' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS2], led_gpio: [
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS2], { required: true, message: 'LED GPIO is required' },
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS2], GPIO_VALIDATORS2
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS2], ],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS2] dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORS2
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORS2
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORS2
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORS2
]
}), }),
...(settings.board_profile === 'CUSTOM' && ...(settings.board_profile === 'CUSTOM' &&
settings.platform === 'ESP32-S3' && { settings.platform === 'ESP32-S3' && {
led_gpio: [{ required: true, message: 'LED GPIO is required' }, GPIO_VALIDATORS3], led_gpio: [
dallas_gpio: [{ required: true, message: 'GPIO is required' }, GPIO_VALIDATORS3], { required: true, message: 'LED GPIO is required' },
pbutton_gpio: [{ required: true, message: 'Button GPIO is required' }, GPIO_VALIDATORS3], GPIO_VALIDATORS3
tx_gpio: [{ required: true, message: 'Tx GPIO is required' }, GPIO_VALIDATORS3], ],
rx_gpio: [{ required: true, message: 'Rx GPIO is required' }, GPIO_VALIDATORS3] dallas_gpio: [
{ required: true, message: 'GPIO is required' },
GPIO_VALIDATORS3
],
pbutton_gpio: [
{ required: true, message: 'Button GPIO is required' },
GPIO_VALIDATORS3
],
tx_gpio: [
{ required: true, message: 'Tx GPIO is required' },
GPIO_VALIDATORS3
],
rx_gpio: [
{ required: true, message: 'Rx GPIO is required' },
GPIO_VALIDATORS3
]
}), }),
...(settings.syslog_enabled && { ...(settings.syslog_enabled && {
syslog_host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR], syslog_host: [
{ required: true, message: 'Host is required' },
IP_OR_HOSTNAME_VALIDATOR
],
syslog_port: [ syslog_port: [
{ required: true, message: 'Port is required' }, { required: true, message: 'Port is required' },
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' } { type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
@@ -134,14 +235,35 @@ export const createSettingsValidator = (settings: Settings) =>
] ]
}), }),
...(settings.shower_alert && { ...(settings.shower_alert && {
shower_alert_trigger: [{ type: 'number', min: 1, max: 20, message: 'Time must be between 1 and 20 minutes' }], shower_alert_trigger: [
shower_alert_coldshot: [{ type: 'number', min: 1, max: 10, message: 'Time must be between 1 and 10 seconds' }] {
type: 'number',
min: 1,
max: 20,
message: 'Time must be between 1 and 20 minutes'
}
],
shower_alert_coldshot: [
{
type: 'number',
min: 1,
max: 10,
message: 'Time must be between 1 and 10 seconds'
}
]
}) })
}); });
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({ export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) => ({
validator(rule: InternalRuleItem, name: string, callback: (error?: string) => void) { validator(
if ((o_name === undefined || o_name !== name) && schedule.find((si) => si.name === name)) { rule: InternalRuleItem,
name: string,
callback: (error?: string) => void
) {
if (
(o_name === undefined || o_name !== name) &&
schedule.find((si) => si.name === name)
) {
callback('Name already in use'); callback('Name already in use');
} else { } else {
callback(); callback();
@@ -149,7 +271,10 @@ export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) =
} }
}); });
export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem: ScheduleItem) => export const schedulerItemValidation = (
schedule: ScheduleItem[],
scheduleItem: ScheduleItem
) =>
new Schema({ new Schema({
name: [ name: [
{ {
@@ -162,7 +287,12 @@ export const schedulerItemValidation = (schedule: ScheduleItem[], scheduleItem:
], ],
cmd: [ cmd: [
{ required: true, message: 'Command is required' }, { required: true, message: 'Command is required' },
{ type: 'string', min: 1, max: 64, message: 'Command must be 1-64 characters' } {
type: 'string',
min: 1,
max: 64,
message: 'Command must be 1-64 characters'
}
] ]
}); });
@@ -178,7 +308,11 @@ export const entityItemValidation = () =>
], ],
device_id: [ device_id: [
{ {
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (isNaN(parseInt(value, 16))) { if (isNaN(parseInt(value, 16))) {
callback('Is required and must be in hex format'); callback('Is required and must be in hex format');
} }
@@ -188,7 +322,11 @@ export const entityItemValidation = () =>
], ],
type_id: [ type_id: [
{ {
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (isNaN(parseInt(value, 16))) { if (isNaN(parseInt(value, 16))) {
callback('Is required and must be in hex format'); callback('Is required and must be in hex format');
} }
@@ -208,7 +346,11 @@ export const temperatureSensorItemValidation = () =>
}); });
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({ export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
validator(rule: InternalRuleItem, gpio: number, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
gpio: number,
callback: (error?: string) => void
) {
if (sensors.find((as) => as.g === gpio)) { if (sensors.find((as) => as.g === gpio)) {
callback('GPIO already in use'); callback('GPIO already in use');
} else { } else {
@@ -217,7 +359,11 @@ export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
} }
}); });
export const analogSensorItemValidation = (sensors: AnalogSensor[], creating: boolean, platform: string) => export const analogSensorItemValidation = (
sensors: AnalogSensor[],
creating: boolean,
platform: string
) =>
new Schema({ new Schema({
n: [{ required: true, message: 'Name is required' }], n: [{ required: true, message: 'Name is required' }],
g: [ g: [
@@ -240,8 +386,17 @@ export const deviceValueItemValidation = (dv: DeviceValue) =>
v: [ v: [
{ required: true, message: 'Value is required' }, { required: true, message: 'Value is required' },
{ {
validator(rule: InternalRuleItem, value: unknown, callback: (error?: string) => void) { validator(
if (typeof value === 'number' && dv.m && dv.x && (value < dv.m || value > dv.x)) { rule: InternalRuleItem,
value: unknown,
callback: (error?: string) => void
) {
if (
typeof value === 'number' &&
dv.m &&
dv.x &&
(value < dv.m || value > dv.x)
) {
callback('Value out of range'); callback('Value out of range');
} }
callback(); callback();

View File

@@ -1 +1,2 @@
export const routeMatches = (route: string, pathname: string) => pathname.startsWith(route + '/') || pathname === route; export const routeMatches = (route: string, pathname: string) =>
pathname.startsWith(route + '/') || pathname === route;

View File

@@ -8,7 +8,11 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
hour12: false hour12: false
}); });
export const formatDateTime = (dateTime: string) => LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19))); export const formatDateTime = (dateTime: string) =>
LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19)));
export const formatLocalDateTime = (date: Date) => export const formatLocalDateTime = (date: Date) =>
new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, -1).substring(0, 19); new Date(date.getTime() - date.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, -1)
.substring(0, 19);

View File

@@ -22,7 +22,12 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
const [dirtyFlags, setDirtyFlags] = useState<string[]>([]); const [dirtyFlags, setDirtyFlags] = useState<string[]>([]);
const blocker = useBlocker(dirtyFlags.length !== 0); const blocker = useBlocker(dirtyFlags.length !== 0);
const { data, send: readData, update: updateData, onComplete: onReadComplete } = useRequest(read()); const {
data,
send: readData,
update: updateData,
onComplete: onReadComplete
} = useRequest(read());
const { const {
loading: saving, loading: saving,

View File

@@ -6,15 +6,27 @@ import { IP_ADDRESS_VALIDATOR } from './shared';
export const createAPSettingsValidator = (apSettings: APSettingsType) => export const createAPSettingsValidator = (apSettings: APSettingsType) =>
new Schema({ new Schema({
provision_mode: { required: true, message: 'Please provide a provision mode' }, provision_mode: {
required: true,
message: 'Please provide a provision mode'
},
...(isAPEnabled(apSettings) && { ...(isAPEnabled(apSettings) && {
ssid: [ ssid: [
{ required: true, message: 'Please provide an SSID' }, { required: true, message: 'Please provide an SSID' },
{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' } {
type: 'string',
max: 32,
message: 'SSID must be 32 characters or less'
}
], ],
password: [ password: [
{ required: true, message: 'Please provide an access point password' }, { required: true, message: 'Please provide an access point password' },
{ type: 'string', min: 8, max: 64, message: 'Password must be 8-64 characters' } {
type: 'string',
min: 8,
max: 64,
message: 'Password must be 8-64 characters'
}
], ],
channel: [ channel: [
{ required: true, message: 'Please provide a network channel' }, { required: true, message: 'Please provide a network channel' },
@@ -22,10 +34,24 @@ export const createAPSettingsValidator = (apSettings: APSettingsType) =>
], ],
max_clients: [ max_clients: [
{ required: true, message: 'Please specify a value for max clients' }, { required: true, message: 'Please specify a value for max clients' },
{ type: 'number', min: 1, max: 9, message: 'Max clients must be between 1 and 9' } {
type: 'number',
min: 1,
max: 9,
message: 'Max clients must be between 1 and 9'
}
], ],
local_ip: [{ required: true, message: 'Local IP address is required' }, IP_ADDRESS_VALIDATOR], local_ip: [
gateway_ip: [{ required: true, message: 'Gateway IP address is required' }, IP_ADDRESS_VALIDATOR], { required: true, message: 'Local IP address is required' },
subnet_mask: [{ required: true, message: 'Subnet mask is required' }, IP_ADDRESS_VALIDATOR] IP_ADDRESS_VALIDATOR
],
gateway_ip: [
{ required: true, message: 'Gateway IP address is required' },
IP_ADDRESS_VALIDATOR
],
subnet_mask: [
{ required: true, message: 'Subnet mask is required' },
IP_ADDRESS_VALIDATOR
]
}) })
}); });

View File

@@ -6,7 +6,10 @@ import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) => export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) =>
new Schema({ new Schema({
...(mqttSettings.enabled && { ...(mqttSettings.enabled && {
host: [{ required: true, message: 'Host is required' }, IP_OR_HOSTNAME_VALIDATOR], host: [
{ required: true, message: 'Host is required' },
IP_OR_HOSTNAME_VALIDATOR
],
base: { required: true, message: 'Base is required' }, base: { required: true, message: 'Base is required' },
port: [ port: [
{ required: true, message: 'Port is required' }, { required: true, message: 'Port is required' },
@@ -14,11 +17,21 @@ export const createMqttSettingsValidator = (mqttSettings: MqttSettingsType) =>
], ],
keep_alive: [ keep_alive: [
{ required: true, message: 'Keep alive is required' }, { required: true, message: 'Keep alive is required' },
{ type: 'number', min: 1, max: 86400, message: 'Keep alive must be between 1 and 86400' } {
type: 'number',
min: 1,
max: 86400,
message: 'Keep alive must be between 1 and 86400'
}
], ],
publish_time_heartbeat: [ publish_time_heartbeat: [
{ required: true, message: 'Heartbeat is required' }, { required: true, message: 'Heartbeat is required' },
{ type: 'number', min: 10, max: 86400, message: 'Heartbeat must be between 10 and 86400' } {
type: 'number',
min: 10,
max: 86400,
message: 'Heartbeat must be between 10 and 86400'
}
] ]
}) })
}); });

View File

@@ -3,16 +3,42 @@ import type { NetworkSettingsType } from 'types';
import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared'; import { HOSTNAME_VALIDATOR, IP_ADDRESS_VALIDATOR } from './shared';
export const createNetworkSettingsValidator = (networkSettings: NetworkSettingsType) => export const createNetworkSettingsValidator = (
networkSettings: NetworkSettingsType
) =>
new Schema({ new Schema({
ssid: [{ type: 'string', max: 32, message: 'SSID must be 32 characters or less' }], ssid: [
bssid: [{ type: 'string', max: 17, message: 'BSSID must be 17 characters or empty' }], { type: 'string', max: 32, message: 'SSID must be 32 characters or less' }
password: { type: 'string', max: 64, message: 'Password must be 64 characters or less' }, ],
hostname: [{ required: true, message: 'Hostname is required' }, HOSTNAME_VALIDATOR], bssid: [
{
type: 'string',
max: 17,
message: 'BSSID must be 17 characters or empty'
}
],
password: {
type: 'string',
max: 64,
message: 'Password must be 64 characters or less'
},
hostname: [
{ required: true, message: 'Hostname is required' },
HOSTNAME_VALIDATOR
],
...(networkSettings.static_ip_config && { ...(networkSettings.static_ip_config && {
local_ip: [{ required: true, message: 'Local IP is required' }, IP_ADDRESS_VALIDATOR], local_ip: [
gateway_ip: [{ required: true, message: 'Gateway IP is required' }, IP_ADDRESS_VALIDATOR], { required: true, message: 'Local IP is required' },
subnet_mask: [{ required: true, message: 'Subnet mask is required' }, IP_ADDRESS_VALIDATOR], IP_ADDRESS_VALIDATOR
],
gateway_ip: [
{ required: true, message: 'Gateway IP is required' },
IP_ADDRESS_VALIDATOR
],
subnet_mask: [
{ required: true, message: 'Subnet mask is required' },
IP_ADDRESS_VALIDATOR
],
dns_ip_1: IP_ADDRESS_VALIDATOR, dns_ip_1: IP_ADDRESS_VALIDATOR,
dns_ip_2: IP_ADDRESS_VALIDATOR dns_ip_2: IP_ADDRESS_VALIDATOR
}) })

View File

@@ -3,7 +3,10 @@ import Schema from 'async-validator';
import { IP_OR_HOSTNAME_VALIDATOR } from './shared'; import { IP_OR_HOSTNAME_VALIDATOR } from './shared';
export const NTP_SETTINGS_VALIDATOR = new Schema({ export const NTP_SETTINGS_VALIDATOR = new Schema({
server: [{ required: true, message: 'Server is required' }, IP_OR_HOSTNAME_VALIDATOR], server: [
{ required: true, message: 'Server is required' },
IP_OR_HOSTNAME_VALIDATOR
],
tz_label: { tz_label: {
required: true, required: true,
message: 'Time zone is required' message: 'Time zone is required'

View File

@@ -5,12 +5,21 @@ import type { UserType } from 'types';
export const SECURITY_SETTINGS_VALIDATOR = new Schema({ export const SECURITY_SETTINGS_VALIDATOR = new Schema({
jwt_secret: [ jwt_secret: [
{ required: true, message: 'JWT secret is required' }, { required: true, message: 'JWT secret is required' },
{ type: 'string', min: 1, max: 64, message: 'JWT secret must be between 1 and 64 characters' } {
type: 'string',
min: 1,
max: 64,
message: 'JWT secret must be between 1 and 64 characters'
}
] ]
}); });
export const createUniqueUsernameValidator = (users: UserType[]) => ({ export const createUniqueUsernameValidator = (users: UserType[]) => ({
validator(rule: InternalRuleItem, username: string, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
username: string,
callback: (error?: string) => void
) {
if (username && users.find((u) => u.username === username)) { if (username && users.find((u) => u.username === username)) {
callback('Username already in use'); callback('Username already in use');
} else { } else {
@@ -32,6 +41,11 @@ export const createUserValidator = (users: UserType[], creating: boolean) =>
], ],
password: [ password: [
{ required: true, message: 'Please provide a password' }, { required: true, message: 'Please provide a password' },
{ type: 'string', min: 1, max: 64, message: 'Password must be 1-64 characters' } {
type: 'string',
min: 1,
max: 64,
message: 'Password must be 1-64 characters'
}
] ]
}); });

View File

@@ -7,13 +7,17 @@ export const validate = <T extends object>(
options?: ValidateOption options?: ValidateOption
): Promise<T> => ): Promise<T> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
void validator.validate(source, options ? options : {}, (errors, fieldErrors) => { void validator.validate(
if (errors) { source,
reject(fieldErrors); options ? options : {},
} else { (errors, fieldErrors) => {
resolve(source as T); if (errors) {
reject(fieldErrors);
} else {
resolve(source as T);
}
} }
}); );
}); });
// updated to support both IPv4 and IPv6 // updated to support both IPv4 and IPv6
@@ -23,7 +27,11 @@ const IP_ADDRESS_REGEXP =
const isValidIpAddress = (value: string) => IP_ADDRESS_REGEXP.test(value); const isValidIpAddress = (value: string) => IP_ADDRESS_REGEXP.test(value);
export const IP_ADDRESS_VALIDATOR = { export const IP_ADDRESS_VALIDATOR = {
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (value && !isValidIpAddress(value)) { if (value && !isValidIpAddress(value)) {
callback('Must be an IP address'); callback('Must be an IP address');
} else { } else {
@@ -36,10 +44,15 @@ const HOSTNAME_LENGTH_REGEXP = /^.{0,200}$/;
const HOSTNAME_PATTERN_REGEXP = const HOSTNAME_PATTERN_REGEXP =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/; /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/;
const isValidHostname = (value: string) => HOSTNAME_LENGTH_REGEXP.test(value) && HOSTNAME_PATTERN_REGEXP.test(value); const isValidHostname = (value: string) =>
HOSTNAME_LENGTH_REGEXP.test(value) && HOSTNAME_PATTERN_REGEXP.test(value);
export const HOSTNAME_VALIDATOR = { export const HOSTNAME_VALIDATOR = {
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (value && !isValidHostname(value)) { if (value && !isValidHostname(value)) {
callback('Must be a valid hostname'); callback('Must be a valid hostname');
} else { } else {
@@ -49,7 +62,11 @@ export const HOSTNAME_VALIDATOR = {
}; };
export const IP_OR_HOSTNAME_VALIDATOR = { export const IP_OR_HOSTNAME_VALIDATOR = {
validator(rule: InternalRuleItem, value: string, callback: (error?: string) => void) { validator(
rule: InternalRuleItem,
value: string,
callback: (error?: string) => void
) {
if (value && !(isValidIpAddress(value) || isValidHostname(value))) { if (value && !(isValidIpAddress(value) || isValidHostname(value))) {
callback('Must be a valid IP address or hostname'); callback('Must be a valid IP address or hostname');
} else { } else {

View File

@@ -3,10 +3,20 @@ import Schema from 'async-validator';
export const OTA_SETTINGS_VALIDATOR = new Schema({ export const OTA_SETTINGS_VALIDATOR = new Schema({
port: [ port: [
{ required: true, message: 'Port is required' }, { required: true, message: 'Port is required' },
{ type: 'number', min: 1025, max: 65535, message: 'Port must be between 1025 and 65535' } {
type: 'number',
min: 1025,
max: 65535,
message: 'Port must be between 1025 and 65535'
}
], ],
password: [ password: [
{ required: true, message: 'Password is required' }, { required: true, message: 'Password is required' },
{ type: 'string', min: 1, max: 64, message: 'Password must be between 1 and 64 characters' } {
type: 'string',
min: 1,
max: 64,
message: 'Password must be between 1 and 64 characters'
}
] ]
}); });

View File

@@ -121,7 +121,11 @@ export default defineConfig(({ command, mode }) => {
manualChunks(id: string) { manualChunks(id: string) {
if (id.includes('node_modules')) { if (id.includes('node_modules')) {
// creating a chunk to react routes deps. Reducing the vendor chunk size // creating a chunk to react routes deps. Reducing the vendor chunk size
if (id.includes('react-router-dom') || id.includes('@remix-run') || id.includes('react-router')) { if (
id.includes('react-router-dom') ||
id.includes('@remix-run') ||
id.includes('react-router')
) {
return '@react-router'; return '@react-router';
} }
return 'vendor'; return 'vendor';